格式化字符串漏洞原理詳解

菜雞剛學總結下,方便複習。

理解這個漏洞的原理,你需要有彙編層面的函數調用和函數的參數傳遞知識。如果你不清楚函數的參數是如何傳遞的,可以看《加密與解密》的逆向分析技術篇,也可以參考我博客裏的(https://blog.csdn.net/qq_43394612/article/details/84332149)

再說格式化字符串漏洞之前,先了解一下printf函數和利用該漏洞的重要 格式化字符串%n,利用他可以做到任意內存寫入。

函數原型

int printf (“格式化字符串”,參量… )
函數的返回值是正確輸出的字符的個數,如果輸出失敗,返回負值。
參量表中參數的個數是不定的(如何實現參數的個數不定,可以參考《程序員的自我修養》這本書),可以是一個,可以是兩個,三個…,也可以沒有參數
printf函數的格式化字符串常見的有 %d,%f,%c,%s,%x(輸出16進制數,前面沒有0x),%p(輸出16進制數,前面帶有0x)等等。
但是有個不常見的格式化字符串 %n ,它的功能是將%n之前打印出來的字符個數,賦值給一個變量。

除了%n,還有%hn,%hhn,%lln,分別爲寫入目標空間2字節,1字節,8字節。 注意是對應參數(這個參數是指針)的對應的地址開始起幾個字節。不要覺得%lln,取的是8個字節的指針,%n取的就是4個字節的指針,取的是多少字節的指針只跟 程序的位數有關,如果是32位的程序,%n取的就是4字節指針,64位取的就是8字節指針,這是因爲不同位數的程序,每個參數對應的字節數是不同的。

具體事例:
在這裏插入圖片描述
%n之前打印了5個a,所以n的值變成了5。

瞭解了這些後就可以說下格式化字符串漏洞了。

漏洞成因和基本原理

正確使用printf是這樣的:

#include <bits/stdc++.h>
using namespace std;
int main()
{
  int n=5;
  printf("%d",n);
  return 0;
}

但也有人會懶省事,寫成這樣:

#include <bits/stdc++.h>
using namespace std;
int main()
{
  char a[]="neuqcsa";
  printf(a);
  return 0;
}

實參與函數形參的結合順序是從左往右依次進行的,所以上面的代碼也能輸出:
在這裏插入圖片描述
上面的代碼不會有什麼問題,但是如果將字符串的輸入權交給用戶就會有問題了。看下面的代碼:

#include <bits/stdc++.h>
using namespace std;
int main()
{
  char a[100];
  scanf("%s",a);
  printf(a);
  return 0;
}

如果用戶輸入的字符串是"%x%x%x",則會輸出以下結果
在這裏插入圖片描述
輸出的結果是 內存中的數據。

看一下調用printf函數後的堆棧圖:(cdecl調用方式,參數從右往左依次入棧)
在這裏插入圖片描述
在OD中可以清晰的看到:
在這裏插入圖片描述

這是因爲printf函數並不知道參數個數,它的內部有個指針,用來索檢格式化字符串。對於特定類型%,就去取相應參數的值,直到索檢到格式化字符串結束。

所以儘管沒有參數,上面的代碼也會將format string 後面的內存當做參數以16進制輸出。這樣就會造成內存泄露。

任意內存的讀取及任意內存寫入:

任意的內存的讀取需要用到格式化字符串 %s,其對應的參量是一個指向字符串首地址的指針,作用是輸出這個字符串。

在說任意內存的讀取之前要知道 局部變量是存儲在棧中,這點很關鍵。所以一定可以找到我們所輸入的格式化字符串。
例:

#include<stdio.h>
#include<stdlib.h>
int main()
{
  char a[100];
  scanf("%s",a);
  printf(a);
  system("pause");
  return 0;
}

可以得到以下結果
在這裏插入圖片描述
看下堆棧圖:
這是調用scanf函數前的堆棧圖。
在這裏插入圖片描述
輸入字符串後的堆棧圖:
在這裏插入圖片描述
調用printf函數的過程:

mov eax,數組首地址
push eax  
call printf

該過程只是將數組的首地址入棧,此時堆棧圖如下。
在這裏插入圖片描述
所以在格式化字符串裏用很多的%x 就一定可以找到這個AAAA的位置。我們將這個位置記下來,實例中就是第七個%x的位置,即第7個參數。

這裏說下可以直接讀取第七個參數的方法。(在linux下有用,win下沒用)
%< number>$x 是直接讀取第number個位置的參數,同樣可以用在%n,%d等等。
但是需要注意64位程序,前6個參數是存在寄存器中的,從第7個參數開始纔會出現在棧中,所以棧中從格式化串開始的第一個,應該是%7 $n.

下面用ubuntu的環境演示下。
在這裏插入圖片描述
圖中是第六個參數是41414141。
在這裏插入圖片描述
同樣可以得到41414141。這樣就方便的多了。

讀取內存

有了上面內容的鋪墊就可以學任意讀取了:
看下面的代碼:
在這裏插入圖片描述
從命令行輸入字符串後,將該字符串複製到a內,再直接打印a;
輸入的字符串的前4個字節如果是一個有效的字符串的首地址,就可以用%s將其打印出來,做到任意內存讀取。如果不是有效的字符串,會出現段錯誤。
在這裏插入圖片描述
如何寫入地址,需要用到linux自帶的printf命令,將shellcode編碼轉義爲字符。(注意用反引號將printf命令括住,反引號在Tab鍵的上面,反引號內的內容會被當做命令執行。)
如果是用scanf輸入字符串,則無法使用printf命令,只能對照ascii碼錶,scanf和命令行輸入的shellcode編碼不能直接被轉義。(所以爲了方便演示,後面都使用了命令行輸入參數)
寫入地址實例:
在這裏插入圖片描述
0x41414141這個地址已經成功寫入內存,下面只需用%s讀取對應位置,就能讀取以0x41414141爲首地址的字符串。
如果用%n就能將0x41414141這個地址指向的值修改,就能造成任意內存的修改,可以將棧中返回地址修改爲想要執行的shellcode的首地址等等。

修改內存

下面寫個修改靜態變量的例子
例:
在這裏插入圖片描述
測試前,請先關閉內存地址隨機化(PIE),否者b在內存中的地址是不確定的。
先運行下,得到b的地址
在這裏插入圖片描述
接着確定偏移量
在這裏插入圖片描述
這裏是第九個參數。
接着用shellcode編碼將b的地址寫入,並查看能否寫入成功。
在這裏插入圖片描述
用%n修改其值。
在這裏插入圖片描述
因爲%n之前打印了75個字符,所以這裏將b的值從0修改爲75

你也可以通過%< number >$n 來直接修改第九個參數來修改b的值。注意在命令行輸入字符串參數時,要用 " \ "將 $ 轉義,例如:
在這裏插入圖片描述
在%n之前打印了4個字符,所以b的值直接被修改爲4了。
你可以通過控制打印的字符個數來修改b的值,達到幾乎任意修改。
例如%0xxxxxd,通過打印數字前面補0,進行簡化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章