转载

wargame narnia writeup

前言

这一期的 wargame 难度明显比之前的 leviathan 要高,而且已经涉及到相对完善的 Linux溢出 相关知识了。但是在 overthewire 上这才居然只是 2/10 的难度,看来我差得很远啊。

narnia 首页,有如下提示,使用初始账号和密码登陆到目标机器,关于本wargame的所有文件都在 /narnia 文件夹下面。Let’s go

To login to the first level use:  Username: narnia0 Passowrd: narnia0 Data for the levels can be found in /narnia/.

level 0

wargame narnia writeup

从目标机器的文件夹中我们可以看到,每个 level 都给了源代码和编译后的可执行文件,每个可执行文件都有 set-uid 权限。只要溢出该可执行文件,得到下一个 level 的 shell,就可以在 /etc/narnia_pass/ 文件夹下面得到下一个 level 的密码了。

首先正常执行一下 narnia0 这个文件,看看有什么提示没有。

wargame narnia writeup

从执行结果来看,应该是溢出缓冲区,然后修改栈中的另外一个自动变量,以此来过后面的逻辑判断。从下面的源代码文件也可以看到,我的猜想是正确的。

#include            #include               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)     system("/bin/sh");   else {     printf("WAY OFF!!!!/n");     exit(1);   }   return 0; }             

可以看到,输入缓冲区 buf ,和待溢出变量都在 main 函数的栈帧中,这题很简单,只要正确构造输入就可以了。一开始,我只是构造了这样的一个输入:

wargame narnia writeup

很明显,明明正确溢出修改了 val 的值,但是没有得到想要的 shell,后来,发现原来是管道输出给了程序之后,就会自动关闭了,造成程序返回的 shell 无法打开。于是,修改 shellcode 如下,发现正确得到了密码。

wargame narnia writeup

level 1

执行可执行程序,可以得到一个很有用的提示如下。

wargame narnia writeup

好像只要把 shellcode 放到正确的环境变量中就可以了,从程序代码中,看到环境变量 EGG 的地址被作为函数指针调用了。

#include            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; }      

构造如下的带有 shellcode 的环境变量就可以正确溢出了。关于环境变量的地址计算,很多溢出类书籍都会提到,上百度 google 一下就可以得到想要的方法。

wargame narnia writeup

level 2

wargame narnia writeup

从程序执行结果来看,似乎需要给一个输入作为 main 函数的参数,估计是通过这个输入来做为溢出点。

#include              #include                 #include                    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; }                         
 

这也是一个很基础的溢出题目,正确的覆盖 main 函数的返回地址就可以了。解法如下图所示。我在这里,将 shellcode 放在了 EGG 这个环境变量中,所以只要使用 EGG 的地址覆盖 main 函数的返回地址就可以了。

wargame narnia writeup

level 3

从这道题目开始,难度开始慢慢加大了,不过依然都在控制之内。

wargame narnia writeup

从程序执行结果来看,似乎将某个文件作为参数传给程序,然后程序打开输出到 /dev/null 这个设备中去。

#include              #include                 #include                    #include                       #include                          #include                             #include                                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); }                                                                                     
 

很神奇的事情,字符数组 char ofile[16] = "/dev/null";

居然可以这样初始化,我记得当年的谭老师的课本里不是这样写的啊。。。。

从源代码来看,之前的猜测是对的。我一开始的想法是,重定向 /dev/null 设备到某个文件,这样,将密码的存储文件作为参数传给程序,程序将密码文件中的内容输出到我重定向的目标文件中,就可以正确得到了。后来 google 了半天,没有找到有效的重定向的方法。另辟蹊径,我想到可以溢出缓冲区的内容,修改输出文件路径,这样就可以将结果输出到某个文件中去了。我构造了 /tmp/narnia3/AAAAAAAAAAAAAAAAAAA/tmp/pass 这个路径下的一个文件,该文件被软链接到密码文件。同时,该字符串被作为参数输入给了程序, /tmp/pass 这部分子串被溢出到输出文件路径的存储缓冲区中,这样输入是密码文件的一个软链接,输出是 /tmp/pass 这样的一个文件。具体操作如下图所示,因为待溢出程序具有 set-uid 权限,所以执行是的有效用户是下一个 level,注意 /tmp 下文件夹的访问权限问题。

wargame narnia writeup

level 4

wargame narnia writeup

这一关程序执行居然没有输出,看来只能通过分析源代码来找溢出点了。

#include                #include                    #include                        #include                              extern char **environ;   int main(int argc,char **argv){         int i;         char buffer[256];           for(i = 0; environ[i] != NULL; i++)                 memset(environ[i], '', strlen(environ[i]));           if(argc>1)                 strcpy(buffer,argv[1]);           return 0; }                                    
 

从代码来看,程序清空了所有的环境变量,这样在环境变量中存放 shellcode 的方法不可用了,不过,程序将输入的 main 函数参数无限制拷贝到了 buffer 中,这样就给了我们缓冲区溢出的漏洞,很基础的一个缓冲区溢出题目,只要将 shellcode 放入到栈中,然后正确覆盖函数返回地址就可以了。在猜测 shellcode 地址的时候,可能是因为栈偏移的问题,导致 gdb 中的栈地址和 shell 中运行时的实际地址有所偏移,不过添加一些 NOP Sled 就可以了。结果如下图所示。

wargame narnia writeup

level 5

这一关从程序执行来看,也是通过溢出修改某个变量的值,但是从源代码看,并不是简单的溢出就可以修改了。

wargame narnia writeup

#include            #include               #include                  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");     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; }                      

从来看,变量 i 和缓冲区 buffer 在栈中相邻,但是,缓冲区输入的时候使用了安全的 snprintf() 函数,这导致不能通过溢出来覆盖变量 i 的值,但是 snprintf() 在调用的时候的格式化字符串是由用户作为 main() 函数的输入,我们可以控制这个格式化字符串,导致了格式化字符串漏洞。

wargame narnia writeup

验证的确有格式化字符串漏洞,利用这个漏洞,可以读写任意地址的值,所以我们构造一个合适的格式化字符串,就可以修改变量 i 的值,得到一个高权限的 shell 。具体操作如下图所示。

wargame narnia writeup

level 6

这一关需要两个输入作为 main() 函数的参数,猜测应该有很明显的溢出点,难度就在于如何构造合适的溢出字符串。

wargame narnia writeup

#include            #include               #include                  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], '', strlen(environ[i]));  /* clear argz    */  for(i=3; argv[i] != NULL; i++)   memset(argv[i], '', 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);  fp(b1);  exit(1);                      

从源代码中可以看到,环境变量和多余的 main() 函数参数都被清空了,导致无法在其中安放 shellcode 。缓冲区 b1b2 在栈中紧邻,接下来是一个指向 puts() 函数的函数指针,于是有了覆盖这个函数指针的思路。函数指针以 b1 为参数,进行函数调用,于是思路是用 system() 的地址覆盖 fp 的值,然后在缓冲区 b1 中填充 /bin/sh 字符串,这样在程序结束的时候就会有 system("/bin/sh") 这个函数调用,得到一个高一级的 shell 。在程序中,首先 strcpy(b1) ,然后再 strcpy(b2) ,我们在构造带有 /bin/sh 这个子串的字符串时需要考虑到字符串的长度问题,使得字符串能够正常结束。这样,先使用缓冲区 b1 溢出覆盖 fp 的值,使用 system() 的地址覆盖该值,然后使用缓冲区 b2 溢出往 b1 中添加 /bin/sh 这样的子串, b2 的长度需要考虑。实际操作如下。

wargame narnia writeup

level 7

简单的程序输出已经提供不了太多的有效信息了,但是还是可以看到有输入,就有可能有缓冲区溢出问题。

wargame narnia writeup

#include              #include                 #include                    #include                       #include                          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                                   /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);   system("/bin/sh");   return 0; }                                                                   
 

这个代码有点点长,不过思路还是很清楚的,在 vuln() 函数中,有我们的输入,有一个函数指针紧邻着缓冲区,使用了安全的 snprintf() 函数来复制我们的输入到缓冲区中,依然有格式化字符串漏洞。不过这题的难度在于,我们输入的格式化参数没有打印出来结果,导致我们无法根据输出来调整输入的格式化参数,而且由于栈偏移的问题,导致 gdb 中的地址和 shell 中实际执行的地址差距很大,基本上不能利用。好字啊程序打印出来了足够的地址信息,我们知道修改后的值和待修改的地址。于是,我先构造了含有目的地址和目标长度的格式化字符串 /x0c/xd5/xff/xff.%134514432d. ,然后在该字符串后面添加写入的格式化参数 %n ,依次尝试猜测,运气不错,猜到了第六个就得到了 shell

本来是写了一个 Python 脚本来尝试爆破的,但是技术太烂,导致脚本执行的结果不太好,还是人工爆破来做的。其实这里猜测的风险很大,因为如果字符串的存储地址不是四字节对齐的话,这样我们在字符串中存放的地址就需要调整偏移,但是因为没有输出,导致无法知道这个偏移到底存在与否。好在题目设计得不是太难。

wargame narnia writeup

level 8

既然程序执行已经无法提供太多有效的信息了,还是直接看代码吧。

#include              #include                  #include                      // 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, '', sizeof(bok));         for(i=0; blah[i] != ''; 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; }                     

看起来似乎很简单,只是一个很简单的缓冲区溢出,但是实际操作的时候发现,变量 blah 和缓冲区 bok 在栈中是相邻的,导致如果输入的字符串太长的话,就会覆盖 blah 这个变量,这个变量又是我们的输入字符串的基地址指针,如果被修改了,就无法正确访问我们输入的字符串。

wargame narnia writeup

从执行结果来看,输入太长的字符串,都会导致后面的字符串没有复制到缓冲区中,这样也就无法覆盖函数的返回地址,溢出失败。于是改变了思路,既然太长的输入字符串会修改原来的基地址值,那么就用原来的基地址值再覆盖回去,这样就相当于没有修改了。只需要猜测原来的基地址值就可以了。从程序中可以看到,缓冲区被复制之后,没有正确的结束符,这样给了我们打印 blah 变量原来的值的可能。

wargame narnia writeup

缓冲区长度是,所以输入长度为的字符串正好可以覆盖缓冲区,同时又没有正确的结束符,就可以看到 blah 变量的值了。在这里是 0xffffd7c7 ,根据测试,我们输入的字符串长度每增加,这个基地址值就会减少,通过计算,就可以得到正确的基地址值了。具体操作过程如下图所示。

wargame narnia writeup

end of the game

终于结束了这次的 wargame ,想到这一期的 wargame 难度只有 2/10 ,我就知道后面还会有更多更好玩的东西。毕竟这里还有没涉及到 ASLRstack canary 等缓冲区溢出保护策略,不过这些在后面的游戏都会有的。尽请期待~

【来源: wargame narnia writeup 作者: litao3rd 】

正文到此结束
Loading...