持续更新(或许)
环境:x64 musl-1.2.2
FSOP
FILE 结构
./src/internal/stdio_impl.h1 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.c1 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 的两个函数 write
和 seek
FSOP 条件
- f->lock == 0
- flags == “/bin/sh\x00”
- 调用的第一个参数都是 FILE 指针,在劫持为
system
时,将 flags 改为 /bin/sh\x00
即可
- 调用 write
- 调用 seek
exit hijack
🐧师傅提及的
笔者自己起的名(
./src/exit/atexit.c1 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