整數溢出攻擊(一):原理介紹


         1、CPU算術計算原理介紹

       (1)CPU叫"central process unit",中文叫中央處理器,內部主要的功能模塊之一就是算術邏輯單元,專門負責做各種快速的算術計算;由於硬件原理限制(再說細一點就是數字電路原理限制:一般情況下高電平表示1,低電平表示0),CPU只能讀取、識別和處理二進制數據;如果要處理其他進制的數,需要人為先轉成二進制。拿人們常用的10進制算術計算舉例:3+5=8,這種算術題小學一年級的學生都會。但對於CPU來說卻沒法直接計算的,必須要轉成二進制才能計算。3+5就變成了 0011+0101,然后從右向左逐位相加,逢2進1,最后的結果就是1000,轉成10進制就是8;所有的算術都需要先轉成二進制,再通過CPU的算術邏輯單元計算

      (2)加減乘除這種最基本的四則運算都是互相關聯的。比如減法可以轉換成加負數;乘法可以轉換成多個數相加(或則左移位);除法可以轉成多個負數相加(或則右移位),那么加法運算就是CPU內部最基本的計算方法了,這個必須從硬件上實現,其他3中運算都可以轉成加法運算;加法器可以由異或門和與門實現,所以CPU內部的算術邏輯單元硬件上就是由大量的異或門和與門組成的!加法器的邏輯原理:

         

       上面舉了一個簡單的加法原理,這里再介紹一個減法原理,比如9-5,可以看成是9+(-5),cpu在硬件上是可以直接計算加法的,但是-5該怎么表示了?計算機中,負數是以正數的補碼形式表示的!比如5的原碼是00000101,那么反碼就是11111010,補碼=反碼+1,也就是11111011;現在的問題來了,為啥負數要用補碼表示了?為啥不直接用反碼表示了?為啥不用其他形式的碼表示了?

       CPU內部只能識別二進制,也只能進行二進制的加法運算,至於減、乘、除這些運算就需要人來進一步抽象了!回到上面提到的9-5=9+(-5),也就是00001001+ 11111011 = 1 0000 0100,此時產生了溢出,OF=1。 如果只取8bit,那么結果就是0000 0100,剛好是4,就是我們想要的結果!如果負數直接用反碼,那么-5=11111010,9+(-5) = 00001001 + 11111010 = 1 0000 0011,取8bit的話結果是3,很明顯是錯的;所以負數直接用反碼這種編碼方式運算后的結果,和10進制的運算結果無法吻合,一般人習慣了10進制,以10進制結果為准的話,二進制的結果就是錯的,也就是說直接用反碼表示負數的方式是錯的,必須用補碼表示負數,其在二進制運算的結果和10進制運算的結果才相同,符合算術計算的通識!這里再舉個例子:

        10進制下:5-5=0,這個沒人反對吧(幼兒園小朋友都會的算術)? 5=00000101,5的反碼是11111010;如果直接用反碼表示負數,那么5-5=5+(-5)=00000101+11111010=1111 1111,轉成10進制就是256,這很明顯不符合10進制的算術邏輯。如果用補碼表示-5,那么-5=反碼+1=11111010+1=11111011,此時5+(-5)= 00000101 + 11111011 = 1 0000 0000,產生了溢出,低8bit剛好是0,符合10進制的算術規則!

       本質上,用補碼表示負數,是借助了二進制運算溢出的原理,讓二進制加法的結果和10進制減法的結果保持一致

     (3)所以說CPU最核心的算術功能原理一點都不復雜,還很簡單,就是依靠速度快才完成了很多人腦完不成的任務,所以CPU算術計算功能也可以認為是:速度超快的二傻子(硬件上只會做二進制加法,其他啥也不會了,需要人為基於二進制加法重新定義其他運算)!

     (4)CPU硬件上是由無數晶體管構成的,這些晶體管通電表示1,斷點表示0; 內存的存儲芯片也是由無數的電容構成的,充電表示1,放電表示0;這種0、1都是人為抽象出來的,底層的硬件只管充放電、通電或斷點,至於通電/充電后表示什么、斷電/放電后又表示什么,都是人為自己設定的!舉個栗子:

      內存有一個8bit的存儲單元,每bit的狀態為:0110 0001,其中1表示電容有電,0表示沒電; 硬件方面內存能做的就是讀寫這些電容,讓其狀態在0和1之間互相轉換,至於這些0、1有啥業務意義,cpu和內存這些硬件是不知道的,需要人為賦予意義,比如: 

  •     如果認為這是10進制的無符號數,那么就是97;
  •     如果認為這是10進制的有符號數,由於最高位是0,那么還是正數97;
  •     如果認為是16進制的無符號數,那么就是0x61;
  •     如果認為是ascii碼,那么就是字符“a”;

      二進制數是0110 0001,代表什么業務意義完全需要人為定義的!這一點非常重要,后續所有的和數相關的溢出都是根據這個原理來操作的!

    2、正式做測試前,先明確各種數據類型的長度和人為抽象的數值范圍:

     

     數據類型之間的本質區別就是長度不同,人為解讀的方式不同;比如char是1byte,int是4byte,后者的長度是前者的4倍; char默認是有符號的,取值范圍-128~128;unsigned char是無符號的,取值范圍0~255,這些取值都是人為設定的規則決定的!所以不同類型的數據是可以強行轉換的。比如char 類型的字符a,在內存中存儲的方式是0110 0001。如果轉成10進制的int類型,就是97;不管類型怎么轉換,二進制的數值是不變的,唯一不同的是人為怎么去解讀和使用!其實C語言中有一種數據叫union,里面可以有很多種不同的基本類型,但是整個變量只取最長的那種類型,比如:

union data{
    int n;
    char ch;
    short sh;
};

  這里有3個變量,最長的是int,有4字節,那么data的長度也是4字節,選取不同變量本質上是讀取不同長度的數據;比如data d; d.n就讀4字節;d.sh就讀2字節;d.ch只讀1字節!這就是union的本質!

   3、上面鋪墊了這么多,現在開始實際測試;

      (1)先來個簡單點的:

#include <iostream>

int main()
{
     unsigned short int var1 = 1, var2 = 65537;
     if(var1== var2) 
     { 
         printf("溢出"); 
     }
     return 0; 
}

       這段代碼定義了兩個數,然后判斷這兩個數是否相等。如果相等就答應溢出,否則直接退出。代碼非常簡單,一般人看了第一反應就是不相等,然后程序直接退出,實際效果是這樣的么? 我們來看看結果:

     

    在控制台居然打印了“溢出”,說明var1==var2是成立的,是不是大跌眼鏡啊?

    繼續分析:既然cpu只能識別和計算二進制的數,那么我們先把1和65537轉成二進制看看了!看到了吧,這里的數據類型是unsigned short,只有2字節,超過2字節的部門丟掉;如果只取2字節的長度,1和65537的二進制表示確實是一樣的,所以if條件是成立的!

    (2)繼續:先定義一個4字節的整型,再強制付給2字節的short和1字節的char:

  •   由於后面兩個長度不夠,只能從低位取對應長度的數據;
  •   %d表示以有符號整型打印,所以s和c高位不夠4字節的用ff填充補齊;
  •   s+c的結果其實是0x1FFFFDC74,但是最高位的1並未打印出來;因為%x只打印unsinged int類型,最多打印4字節!
  •   sizeof(s+c)*8為啥是32,不是24了? s是2字節,c是char,不是應該只有1字節么?  這就涉及到內存對齊的概念了!32位下,內存是4字節對齊的,這么做可以讓cpu快速讀寫數據。char只有1字節,但還是會占用2字節,和short拼湊成4字節;cpu只需要讀內存1次就能取出s和c兩個變量!

      

       (3)繼續:這里先定義一個有符號的4字節整數i,既然是有符號,那么最高位就是符號位,能用來表示數值的只剩后面的31位了。所以4字節有符號的最大數是2^31-1=2,147,483,647(這里減1是去掉0); 用二進制表示就是0111 1111 1111 1111 1111 1111 1111 1111; 這個數加1后變成了1000 0000 0000 0000 0000 0000 0000 0000,這是二進制的表示方式,轉成10進制后是多少了?

    

     這里用計算器看看:和C程序打印出來的一樣,只不過沒負號。這就奇怪了,最高位是符號位,1表示負數容易理解。剩下31位都是0,為啥轉成10進制就變成了-2147483648?

     

     現在用的這個數太大了,為了便於理解,這里以8bit的長度舉例: 00000000到01111111,表示0到+127。10000001到11111111,表示-1到-127。大家可以注意到,10000000我們沒有用到。因為如果我們把它看成-0,那么會和00000000發生重復。於是人為將10000000定義為-128(即在最終進位后符號位不產生進位)。所以結果是 -2147483648 完全是人為定義的,和cpu沒關系!

    (4)上面是有符號的例子,4字節最大數值+1變成負數,最小負數-1變成最大正數!因為涉及到符號位,有些細節不好理解,下面拿無符號數做個測試,會容易很多!4字節無符號數最大值是2^32=4,294,967,295;32bit全是1;二進制加1后產生進位,CF=1,第33bit是1,1~32bit全是0了,但打印時%u只取4字節,並且按照無符號數打印,所以結果就是0了!

     反過來,減1后,第33bit被借位后變成0,1~32bit都變成1,打印時%u取低4字節,結果就是4,294,967,295;

     

    4、上面舉了那么多實例,看起來可能有點亂,總結概括起來就這么幾點:

  •      有符號數最高位溢出,0和1之間互相變,導致正數和負數之間來回互相變化;
  •      無符號數因為數據類型長度的限制產生了截斷,在最大和0之間互相來回變化;

     

        應用場景距離:

      (1)繞開長度檢測;下面這個例子:我們在做字符串的copy、cat前一般都會先檢查字符串長度,超長的會采取措施;但這里的例子成功繞開了長度檢測,如下: len是int類型,人為賦值2147483648,普通人一看這個數,第一反應比80大,if條件肯定成立,結果了?大跌眼鏡,if條件不成立,長度檢查被成功繞過!

      

        去年windows下爆出了一個漏洞:https://www.cnblogs.com/theseventhson/p/14004712.html 就是對外部接收到的字符串沒有正確地處理導致的!

       上面用符號數表示數據長度,輸入負數后導致檢測失效;同理,用unsigned int類型存儲字符串長度同樣能想辦法攻擊,如下:

     size_t len;
    // int len;
    char* buf;
    char* fd;
    len = 4294967296;
    buf = (char *)malloc(len + 5);
    memcpy(fd, buf, len);

        開發人員的本意是讓數據發送方提供字符串的長度,根據對方提供的長度再+5字節分配緩沖區的長度,然后向緩沖區寫數據;這里的len是unsigned int類型,如果輸入4,294,967,296,再+5變成了4,294,967,301;但這里是unsigned int類型,只取4字節,那么導致buf長度僅僅是4字節(產生了回繞),下面的memcopy極有可能導致緩沖區溢出攻擊;

 

總結:

1、cpu只能識別和處理二進制數;硬件方面,算術運算只實現了二進制的加法,其他3種運算都是人為想辦法轉換編碼和繞行實現的;

2、cpu一點都不聰明,只不過速度非常快罷了,才能完成人腦短時間內完不成的計算!

3、整數溢出常見的利用場景:

4、本質上:整數溢出就是“物極必反”,通過賦一些臨界值,讓有符號數的正負數之間、無符號數在最大和0之間互相轉換; 

5、本文總結如下:

       

 

 

 

參考:

1、https://www.cnblogs.com/flashsun/p/9621986.html  從0開始自制CPU

2、https://bbs.pediy.com/thread-254269.htm 經典整數溢出漏洞演示

3、https://www.bookstack.cn/read/CTF-All-In-One/doc-3.1.2_integer_overflow.md   整數溢出

4、https://cloud.tencent.com/developer/article/1176463  printf詳解

5、https://zhuanlan.zhihu.com/p/71025065  C++整型上下限INT_MAX INT_MIN及其運算

6、https://www.cxybl.com/2020/jisuanjijichu_1106/3508.html     波音787 每248天重新啟動一次Dreamliner,以避免整數溢出


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM