Musl libc Exploration

持续更新(或许)

环境:x64 musl-1.2.2

FSOP

FILE 结构

./src/internal/stdio_impl.h
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;
};

相比 glibc 的 FILE 结构,musl libc 的 FILE 结构更加简单,也更容易利用

有四类 FILE 指针:ofl_head、stdin、stdout、stderr

  • ofl_head
    • 类似 glibc 的 _IO_list_all,打开的文件链表头,为全局变量
    • 可以直接劫持到伪造的 FILE 结构
  • stdin、stdout、stderr
    • 固定的三个 FILE 指针,不可劫持
    • 可以更改其指向的内存空间

利用

./src/stdio/__stdio_exit.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void close_file(FILE *f)
{
if (!f) return;
FFINALLOCK(f);
if (f->wpos != f->wbase) f->write(f, 0, 0);
if (f->rpos != f->rend) f->seek(f, f->rpos-f->rend, SEEK_CUR);
}

void __stdio_exit(void)
{
FILE *f;
for (f=*__ofl_lock(); f; f=f->next) close_file(f);
close_file(__stdin_used);
close_file(__stdout_used);
close_file(__stderr_used);
}

exit() 时会调用 __stdio_exit() ,其中 close_file() 会调用 FILE 的两个函数 writeseek

FSOP 条件

  • f->lock == 0
    • 不为 0 会调用 futex 系统调用,然后寄了
  • flags == “/bin/sh\x00”
    • 调用的第一个参数都是 FILE 指针,在劫持为 system 时,将 flags 改为 /bin/sh\x00 即可
  • 调用 write
    • wpo != wbase
  • 调用 seek
    • rpos != rend

exit hijack

🐧师傅提及的

笔者自己起的名(

./src/exit/atexit.c
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
#define COUNT 32

static struct fl
{
struct fl *next;
void (*f[COUNT])(void *);
void *a[COUNT];
} builtin, *head;

static int slot;
static volatile int lock[1];
volatile int *const __atexit_lockptr = lock;

void __funcs_on_exit()
{
void (*func)(void *), *arg;
LOCK(lock);
for (; head; head=head->next, slot=COUNT) while(slot-->0) {
func = head->f[slot];
arg = head->a[slot];
UNLOCK(lock);
func(arg);
LOCK(lock);
}
}

exit() 时,会调用 __funs_on_exit() 通过 head 指针执行注册的终止函数

利用条件

  • 将 head 劫持到可控内存空间
    • 第一个循环因为 slot == 0,会直接跳过
    • 从而 head = head->next
  • *(head->next + 0x100) == addr_system
  • *(head->next + 0x200) == addr_binsh

在理想的堆风水情况下,只需要任意写一次,即可通过 exit() 拿到 shell

作者

Humoooor

发布于

2022-10-11

更新于

2023-10-04

许可协议

评论