菜雞剛學總結下,方便複習。
理解這個漏洞的原理,你需要有彙編層面的函數調用和函數的參數傳遞知識。如果你不清楚函數的參數是如何傳遞的,可以看《加密與解密》的逆向分析技術篇,也可以參考我博客裏的(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,進行簡化。