C語言格式化字符串漏洞實驗

護花罓使者 7年前發布 | 31K 次閱讀 C語言 C/C++開發

格式化字符串漏洞實驗

在線實驗環境: 格式化字符串漏洞實驗

一、 實驗描述

格式化字符串漏洞是由像printf(user_input)這樣的代碼引起的,其中user_input是用戶輸入的數據,具有Set-UID root權限的這類程序在運行的時候,printf語句將會變得非常危險,因為它可能會導致下面的結果:

  • 使得程序崩潰
  • 任意一塊內存讀取數據
  • 修改任意一塊內存里的數據

最后一種結果是非常危險的,因為它允許用戶修改set-UID root程序內部變量的值,從而改變這些程序的行為。

本實驗將會提供一個具有格式化漏洞的程序,我們將制定一個計劃來探索這些漏洞。

二、實驗預備知識講解

2.1 什么是格式化字符串?

printf ("The magic number is: %d", 1911);

試觀察運行以上語句,會發現字符串"The magic number is: %d"中的格式符%d被參數(1911)替換,因此輸出變成了“The magic number is: 1911”。 格式化字符串大致就是這么一回事啦。

除了表示十進制數的%d,還有不少其他形式的格式符,一起來認識一下吧~

格式符 含義 含義(英)
%d 十進制數(int) decimal
%u 無符號十進制數 (unsigned int) unsigned decimal
%x 十六進制數 (unsigned int) hexadecimal
%s 字符串 ((const) (unsigned) char *) string 引用(指針)
%n %n符號以前輸入的字符數量 (* int) number of bytes written so far 引用(指針)

( * %n 的使用將在1.5節中做出說明)

2.2 棧與格式化字符串

格式化函數的行為由格式化字符串控制,printf函數從棧上取得參數。

printf ("a has value %d, b has value %d, c is at address: %08x\n",a, b, &c);

2.3 如果參數數量不匹配會發生什么?

如果只有一個不匹配會發生什么?

printf ("a has value %d, b has value %d, c is at address: %08x\n",a, b);
  • 在上面的例子中格式字符串需要3個參數,但程序只提供了2個。
  • 該程序能夠通過編譯么?
    • printf()是一個參數長度可變函數。因此,僅僅看參數數量是看不出問題的。
    • 為了查出不匹配,編譯器需要了解printf()的運行機制,然而編譯器通常不做這類分析。
    • 有些時候,格式字符串并不是一個常量字符串,它在程序運行期間生成(比如用戶輸入),因此,編譯器無法發現不匹配。
    </li>
  • 那么printf()函數自身能檢測到不匹配么?
    • printf()從棧上取得參數,如果格式字符串需要3個參數,它會從棧上取3個,除非棧被標記了邊界,printf()并不知道自己是否會用完提供的所有參數。
    • 既然沒有那樣的邊界標記。printf()會持續從棧上抓取數據,在一個參數數量不匹配的例子中,它會抓取到一些不屬于該函數調用到的數據。
    • </ul> </li>
    • 如果有人特意準備數據讓printf抓取會發生什么呢?
    • </ul>

      2.4 訪問任意位置內存

      • 我們需要得到一段數據的內存地址,但我們無法修改代碼,供我們使用的只有格式字符串。
      • 如果我們調用 printf(%s) 時沒有指明內存地址, 那么目標地址就可以通過printf函數,在棧上的任意位置獲取。printf函數維護一個初始棧指針,所以能夠得到所有參數在棧中的位置
      • 觀察: 格式字符串位于棧上. 如果我們可以把目標地址編碼進格式字符串,那樣目標地址也會存在于棧上,在接下來的例子里,格式字符串將保存在棧上的緩沖區中。
      int main(int argc, char *argv[])
      {
          char user_input[100];
          ... ... /* other variable definitions and statements */
          scanf("%s", user_input); /* getting a string from user */
          printf(user_input); /* Vulnerable place */
          return 0;
      }
      • 如果我們讓printf函數得到格式字符串中的目標內存地址 (該地址也存在于棧上), 我們就可以訪問該地址.

        printf ("\x10\x01\x48\x08 %x %x %x %x %s");
      • \x10\x01\x48\x08 是目標地址的四個字節, 在C語言中, \x10 告訴編譯器將一個16進制數0x10放于當前位置(占1字節)。如果去掉前綴\x10就相當于兩個ascii字符1和0了,這就不是我們所期望的結果了。

      • %x 導致棧指針向格式字符串的方向移動(參考1.2節)
      • 下圖解釋了攻擊方式,如果用戶輸入中包含了以下格式字符串
      • 如圖所示,我們使用四個%x來移動printf函數的棧指針到我們存儲格式字符串的位置,一旦到了目標位置,我們使用%s來打印,它會打印位于地址0x10014808的內容,因為是將其作為字符串來處理,所以會一直打印到結束符為止。
      • user_input數組到傳給printf函數參數的地址之間的棧空間不是為了printf函數準備的。但是,因為程序本身存在格式字符串漏洞,所以printf會把這段內存當作傳入的參數來匹配%x。
      • 最大的挑戰就是想方設法找出printf函數棧指針(函數取參地址)到user_input數組的這一段距離是多少,這段距離決定了你需要在%s之前輸入多少個%x。

      2.5 在內存中寫一個數字

      %n: 該符號前輸入的字符數量會被存儲到對應的參數中去

      int i;
      printf ("12345%n", &i);
      • 數字5(%n前的字符數量)將會被寫入i 中
      • 運用同樣的方法在訪問任意地址內存的時候,我們可以將一個數字寫入指定的內存中。只要將上一小節(1.4)的%s替換成%n就能夠覆蓋0x10014808的內容。
      • 利用這個方法,攻擊者可以做以下事情:
        • 重寫程序標識控制訪問權限
        • 重寫棧或者函數等等的返回地址
        </li>
      • 然而,寫入的值是由%n之前的字符數量決定的。真的有辦法能夠寫入任意數值么?
        • 用最古老的計數方式, 為了寫1000,就填充1000個字符吧。
        • 為了防止過長的格式字符串,我們可以使用一個寬度指定的格式指示器。(比如(%0數字x)就會左填充預期數量的0符號)
        • </ul> </li> </ul>

          三、 實驗內容

          用戶需要輸入一段數據,數據保存在user_input數組中,程序會使用printf函數打印數據內容,并且該程序以root權限運行。更加可喜的是,這個程序存在一個格式化漏洞。讓我們來看看利用這些漏洞可以搞些什么破壞。

          程序說明:

          程序內存中存在兩個秘密值,我們想要知道這兩個值,但發現無法通過讀二進制代碼的方式來獲取它們(實驗中為了簡單起見,硬編碼這些秘密值為0x44和0x55)。盡管我們不知道它們的值,但要得到它們的內存地址倒不是特別困難,因為對大多數系統而言,每次運行程序,這些內存地址基本上是不變的。實驗假設我們已經知道了這些內存地址,為了達到這個目的,程序特意為我們打出了這些地址。

          有了這些前提以后我們需要達到以下目標:

          • 找出secret[1]的值
          • 修改secret[1]的值
          • 修改secret[1]為期望值

          注意:因為實驗環境是64位系統,所以需要使用%016llx才能讀取整個字。但為了簡便起見,對程序進行了修改了,使用%08x也能完成實驗。

          有了之前預備知識的鋪墊,先自己嘗試一下,祝玩的愉快:)

          程序如下:

          / vul_prog.c /

          include <stdlib.h>

          include <stdio.h>

          define SECRET1 0x44

          define SECRET2 0x55

          int main(int argc, char argv[]) { char user_input[100]; int secret; long int_input; int a, b, c, d; / other variables, not used here./

          / The secret value is stored on the heap / secret = (int ) malloc(2sizeof(int));

          / getting the secret / secret[0] = SECRET1; secret[1] = SECRET2;

          printf("The variable secret's address is 0x%8x (on stack)\n", &secret); printf("The variable secret's value is 0x%8x (on heap)\n", secret); printf("secret[0]'s address is 0x%8x (on heap)\n", &secret[0]); printf("secret[1]'s address is 0x%8x (on heap)\n", &secret[1]);

          printf("Please enter a decimal integer\n"); scanf("%d", ∫_input); / getting an input from user / printf("Please enter a string\n"); scanf("%s", user_input); / getting a string from user /

          / Vulnerable place / printf(user_input);
          printf("\n");

          / Verify whether your attack is successful / printf("The original secrets: 0x%x -- 0x%x\n", SECRET1, SECRET2); printf("The new secrets: 0x%x -- 0x%x\n", secret[0], secret[1]); return 0; }</code></pre>

          (ps: 編譯時可以添加以下參數關掉棧保護。)

          gcc -z execstack -fno-stack-protector -o vul_prog vul_prog.c

          一點小提示:你會發現secret[0]和secret[1]存在于malloc出的堆上,我們也知道secret的值存在于棧上,如果你想覆蓋secret[0]的值,ok,它的地址就在棧上,你完全可以利用格式化字符串的漏洞來達到目的。然而盡管secret[1]就在它的兄弟0的旁邊,你還是沒辦法從棧上獲得它的地址,這對你來說構成了一個挑戰,因為沒有它的地址你怎么利用格式字符串讀寫呢。但是真的就沒招了么?

          3.1 找出secret[1]的值

          1.首先定位int_input的位置,這樣就確認了%s在格式字符串中的位置。

          2.輸入secret[1]的地址,記得做進制轉換,同時在格式字符串中加入%s。

          大功告成!U的ascii碼就是55。

          3.2 修改secret[1]的值

          1.只要求修改,不要求改什么?簡單!不明白%n用法的可以往前回顧一下。

          大功告成x2!

          3.3 修改secret[1]為期望值

          1.要改成自己期望的值,咋辦?填1000豈不累死?!可以用填充嘛!

          哦對了,0x3e8 = 1000。 大功告成x3!

           

           

          來自:https://github.com/shiyanlou/seedlab/blob/master/formatstring.md

           

 本文由用戶 護花罓使者 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!