Pwnable kr Note
前言
竟然在开组会的时候不小心写出来了,感谢无私分享解法的一位外国小哥。
程序保证不开 ASLR ,也就是说栈地址固定。
漏洞点分析
漏洞点 1
首先,程序有一个明显的 One-byte overflow ,精简代码后如下:
void select_menu(){
// menu
char command[1024];
int menu;
scanf("%d", &menu);
switch(menu){
case 0x31337:
printf("welcome to hacker's secret menu\n");
printf("i'm sure 1byte overflow will be enough for you to pwn this\n");
fgets(command, 1025, stdin);
break;
}
select_menu();
}
然而很遗憾,我发现溢出这个
command 没有一丁点用处,因为检测发现溢出改变的是 menu 变量,而很显然我们看这次溢出后,程序就重新开始重新让用户输入 menu ,因此我们的溢出改变不会有结果。
漏洞点 2
观察上一个漏洞点的代码,有没有发现奇怪的地方? select_menu() 它用了 尾递归 !这种看起来好像没什么毛病,但是他会导致栈位置不断降低。每一次执行 select_menu ,都会用一点栈空间,然后又执行新的 select_menu 。放到 gdb 里面调试了一下,发现每一次栈降低的大小是 0x430 。
漏洞点 3
观察它调用的 mmap_s ,作用就是随机化地址,然后调用 mmap ,但是它错误的调用导致产生了漏洞。
void* mmap_s(void* addr, size_t length, int prot, int flags, int fd, off_t offset){
// 改了许多,不过反正知道函数里有这句话就行
// prot: 期望的内存保护标志(如r-x, rwx ..)
return mmap(addr, length, prot, flags | MAP_FIXED, fd, offset);
}
对于
mmap 函数,其 flags 项里的 MAP_FIXED 是这样的含义:如果我们想要分配的地址在内存中已经被占用了,那么就覆盖掉内存原来的内容。所以这里的滥用可能会导致内存的覆盖。
漏洞点 4
这里不能算作是漏洞点,相当于程序的功能就是如此。
create_note让用户使用mmap_s创建一个块,并且告诉我们这个块的具体的地址write_note让用户往创建的块里面写入数据,需要指定往哪一个块里面写数据,注意这些空间都是可以执行的!delete_notes用户指定删除哪一块数据,即直接 free 掉某一块。
void create_note(){ int i; void* ptr; for(i=0; i<256; i++){ if(mem_arr[i] == NULL){ ptr = mmap_s((void*)NULL, PAGE_SIZE, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); mem_arr[i] = ptr; printf("note created. no %d\n [%08x]", i, (int)ptr); return; } } printf("memory sults are fool\n"); return; } void write_note(){ unsigned int no; printf("note no?\n"); scanf("%d", &no); clear_newlines(); if(no>256){ printf("index out of range\n"); return; } if(mem_arr[no]==NULL){ printf("empty slut!\n"); return; } printf("paste your note (MAX : 4096 byte)\n"); gets(mem_arr[no]); }
流程分析
现在来讲讲具体的利用流程。首先因为程序的功能,如果我们不满意第一次随机分配的空间,还可以进行第二次第三次。又因为每次分配都告诉了我们分配的地址,所以我们可以一直分配,直到分配到我们想让他分配到的地址。
那么分配到哪里呢,首先肯定是有一个空间,里面是放着 shellcode ,最后要跳到那个地址。然后就是本题最精髓的地方了,利用漏洞点二所说的尾递归和漏洞点三所说的可覆盖原有内存,
我们将栈中的一块内存给覆盖掉,要求内存要高于当前 esp (注意栈是从高往低长哦),然后内容全部填充为存 shellcode 的那一块内存地址。
为什么这样可以,想象一下,此时我们选择返回,那么程序开始不断地 (pop ebp, ret) ,这样当 rip 到了我们修改的那一块时,就会执行 shellcode. 而且因为没有开 ASLR,所以栈地址固定,这就更加让覆盖栈地址这一想法非常可靠。
所以程序逻辑是这样:因为没有开 ASLR ,所以程序开始 esp 是一样的。记录下原始的 esp,然后再令当前 esp 等于这个原始的 esp,之后我们会每次执行 select_menu 后都会更新当前 esp.
我们每次进行这样的尝试:首先获取一个随机分配的内存,发现如果有内存块的地址是在 [原始 esp, 当前 esp] 中的,那么这个块就是我们所需要的。如果没有,那就继续获取下一个随机分配的内存,同时因为我们执行了依次 select_menu (程序流: select_menu → create_note),所以我们需要更新当前 esp ,即减去 0x430 (漏洞点二谈到这个值是根据 gdb 调试得出来的)。
如果发现已经分配到 255 了,说明我们必须要把这些内存块都删去,所以要执行 255 次 delete_notes ,所以更新当前 esp ,减去 0x430 * 255 。然后开始新一轮的一次一次获取内存块,直到我们的内存块成功在 [原始esp, 当前esp] 中。
最终我们把某个内存块写入 shellcode ,然后将上述所说在栈中的内存块中数据全部填充为 shellcode 地址,搞定。
总结
- 尾递归可能带来什么危害
- mmap 中一定要慎用 MAP_FIXED