PWN學習之格式化字串漏洞
PWN學習之格式化字串漏洞
格式化輸出函式
可變引數:https://blog.csdn.net/smstong/article/details/50751121 (C語言可變參函式的實現)
首先我們瞭解格式化字串漏洞前,需要對格式化輸出的函式進行一個瞭解,在C中格式化輸出函式一共有如下:
fprintf() "按照格式字串將輸出寫入流中。三個引數分別是流、格式字串和變參列表。" printf() "等同於fprintf(),但是它的輸出流為stdout。" sprintf() "等同於fprintf(),但是它的輸出不是寫入流而是寫入陣列。在寫入的字串末尾必須新增一個空字元。" snprintf() "等同於sprintf(),但是它指定了可寫入字元的最大值size。超過第size-1的部分會被捨棄,並且會在寫入陣列的字串末尾新增一個空字元。" dprintf() "等同於fprintf(),但是它的輸出不是寫入流而是一個檔案描述符fd。" "分別與上面的函式對應,但是它們將變參列表換成了va_list型別的引數。" vfprint()、vprintf()、vsprintf()、vsnprintf()、vdprintf()
格式化字串漏洞
格式化字串漏洞從2000年左右開始流行起來,幾乎在各種軟體中都能見到它的身影,隨著技術的發展,軟體安全性的提升,如今它在桌面端已經比較少見了,但在物聯網裝置 IoT
上依然層出不窮。
#include <stdio.h>
void main()
{
printf("%s %d %s %x %x %x %3$s","Hello World!",233,"\n");
}
我們輸入的引數只有三個,但是格式化字串中還有3個%x和一個%3$s,其中3個%x由於沒有引數他會洩漏出棧的地址
。
接下來繼續來看一個例子,其中fgets來接受使用者輸入的字串,但是如果是hack他就會輸入控制字串來洩漏出棧地址,這點感覺和Web中的xss有點像,程式設計師沒有過濾敏感字元導致被攻擊。由此可以總結出,格式字串漏洞發生的條件就是格式字串
引數和實際提供
的引數不匹配
。
#include <stdio.h>
void main()
{
char buf[50];
if(fgets(buf,sizeof buf,stdin) == NULL)
return;
printf(buf);
}
漏洞利用
對於格式化字串漏洞的利用主要有:
- 使程式崩潰
- 棧資料洩露
- 任意地址記憶體洩露
- 棧資料覆蓋
- 任意地址記憶體覆蓋
使程式崩潰
造成程式崩潰原因:printf需要在棧中取一個數字視為地址,然後打印出地址所指向的記憶體,知道出現空白字元;獲取的某個數字可能並不是一個地址;獲得的陣列確實是一個地址,但改地址受保護。
在Linux中,存取無效的指標
會使程序收到SIGSEGV
訊號。
printf("%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s")
棧資料洩露
#include <stdio.h>
void main()
{
char format[128];
int arg1 = 0x00123456,arg2 = 0x11111111,arg3 = 0x22222222;
char arg4[4] = "ABCD";
scanf("%s",format);
printf(format,arg1,arg2,arg3,arg4);
printf("\n");
}
輸入 %p-%p-%p-%p-%p
可以根據洩漏出來的棧的資料,然後挨個的計算出引數的位置,因為棧中的資料一般都是挨著的,可以看到在0xffffd484的下個數據就是字串ABCD
字串的地址。
現在我們已經知道了如何按順序洩露棧資料,那麼如果想直接洩露指定的某個資料,則可以使用與下面類似的格式字串,這裡的n
表示位於格式字串後的第n個數據
。%n$p
%<arg#>$<format>
%n$x
分別獲取arg3、arg1、arg2、arg2、arg4 以及棧上經跟引數的兩個值
"%3$x-%1$p-%2$p-%2$p-%4$p-%5$p-%6$p"
任意地址記憶體洩漏
攻擊者使用類似%s
的格式規範就可以洩露出引數(指標)所指向記憶體的資料,程式會將它作為一個ASCII字串處理,直到遇到一個空字元。所以,如果攻擊者能夠操縱這個引數的值,那麼就可以洩露任意地址的內容。
AAAA-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p
AAAA代表地址
棧資料覆蓋
#include <stdio.h>
void main()
{
int i;
char str[] = "hello";
printf("%s %n\n",str,&i);
printf("%d\n",i);
}
這個例子i被賦值成了6,因為遇到轉換指示符之前一共寫入了6個字元("hello"加上一個空格)。在沒有長度修飾符時,預設寫入一個int型別的值。有關詳細內容可以參考:https://blog.csdn.net/FollowGodSteps/article/details/74115138
以下例子參考文章:c語言中對欄位寬度的理解?
/*************************************************************************
> File Name: printf.c
> Author: Mr.Yang
> Purpose:演示printf的用法
> Created Time: 2017年05月21日 星期日 10時07分44秒
************************************************************************/
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
float i = 10000.123;
printf("%5f\n",i);
printf("%10f\n",i);
printf("%15f\n",i);
printf("%20f\n",i);
printf("%25f\n",i);
return 0;
}
輸出內容:
10000.123047
10000.123047
10000.123047
10000.123047
10000.123047
回到一開始的程式,我們嘗試將arg2的值更改為任意值(例如0x00000020,十進位制32),於是構造格式字串\x28\xcd\xff\xff%08x%08x%012d%13$n
,其中\x28\xcd\xff\xff
是arg2的地址,佔4位元組,“%08x%08x”表示兩個8字元寬的十六進位制數,佔16位元組,“%012d”佔12位元組,三個部分加起來共佔4+16+12=32位元組,也就是把arg2賦值為0x00000020。格式字串最後一部分“%13$n”是最重要的一部分,表示格式字串的第13個引數,即寫入0xffffcd28的地方(0xffffcd58),printf()通過該地址找到被覆蓋資料。
對比printf()執行前後的棧,可以看到其首先解析“%13$n”,從0xffffcd58找到地址0xffffcd28,然後將其資料覆蓋為“0x00000020”。
任意地址記憶體覆蓋
也許已經有人發現了問題,使用上面的方法,值最小隻能是4,因為光地址就佔去了4個位元組,那麼怎樣覆蓋比4小的值呢?利用整數溢位是一個方法,但是在實踐中這樣做很難成功。再想一下,前面的輸入中,地址都位於格式字串之前,這樣做真的有必要嗎,能否將地址放在中間呢?我們來試一下,使用格式字串“AA%15\(nA"+"\x38\xd5\xff\xff”,開頭的“AA”佔2個位元組,即將地址賦值為2,中間“%15\)n”佔5個位元組(這裡不是%13$n,因為地址被放在了後面),是第15個引數,後面跟上一個“A”佔用1個位元組。於是前半部分總共佔用2+5+1=8個位元組,剛好是兩個引數的寬度,這裡的8位元組對齊十分重要。最後,輸入我們要覆蓋的地址“\x38\xd5\xff\xff”,如下所示。