Linux 2.6 下通過 ptrace 和 plt 實現用戶態 API Hook

 

這廝此文寫的相當實用,不知道爲啥不好好整理一下,得,我代勞了吧。作者:[email protected]原文。去看一眼就知道我幹嘛幹這個髒活兒了... 感覺這篇文章有上首頁的素質,可惜不是我自己寫的,那就算了吧。

本來我自己想用類似這篇文章說的方法,仔細琢磨了一下,似乎我的事兒還是用別的方法幹比較好。另外感覺和這篇文章需求相似的話,只要不是偷着摸着幹事,也還是LD_PRELOAD來的簡單直接。LD_PRELOAD的方法見:http://www.linuxjournal.com/article/7795,和這裏

 

以下是轉載的那篇的正文。


 

最近寫的,曬一下—_—正文裏面出了點比較嚴重的問題,是某L在某處神志不清地把這篇東西貼出去之後才囧然發現的,所以就乾脆不改了……看出來的當看笑話吧—_—

目錄 

1、背景

2、ELF文件格式和庫函數動態解析過程

3、ptrace和Linux下調試器工作原理

4、實現過程

5、程序代碼

6、參考資料

1、背景

API Hook,傳說中的API鉤子,是指神不知鬼不覺地替換掉標準系統API的方法和研究這種方法的藝術學科……好吧,學科的確談不上,不過這些技術貌似在Windows下面已經發展得登峯造極了,大家回頭看看坐在後排爲數衆多的病毒+木馬就不難明白……當然大家不要理解錯了,我沒有教大家幹壞事的意思……比方說我們還可以用API鉤子讓glut支持全屏抗鋸齒之類的……

寫這個之前在網上看過一陣,發現似乎Linux下的API鉤子大家研究得比較少……可能主要是因爲,第一,有LD_PRELOAD這種方便易用的好東東……第二,Linux的運行庫不像Windows,沒有提供專用的掛鉤工具,而且由於兩者之間進程模型的區別,要在Linux下面實現API鉤子是比較麻煩的(忽略 LD_PRELOAD —_—),特別是全局Hook。

這篇文章旨在研究怎樣比LD_PRELOAD更有技術含量地鉤上系統API,當然啦,還是局部掛鉤,擴展成全局的話還沒試過,不清楚效率 怎麼樣……另外我們這裏用的是ptrace,和調試器原理相似,可以在目標程序運行時將鉤子掛上,有牛人已經知道的就直接無視俺吧……

然後這篇文章是基於 Linux 2.6 和 x86-64 機器的,例程在32位機器上跑不起來,不過原理不變……這篇東西假設你瞭解AT&T格式的x86彙編、c語言和 x86-64 cpu的一些特性,當然基本的Linux操作知識是必不可少的……

2、ELF文件格式和庫函數動態解析過程

ELF,Executable and Linking Format,是Linux使用的可執行文件和鏈接庫格式,一般分爲幾部分(廢話),行話叫“sections”,就是我們運行objdump -h /bin/bash之後左邊第二列的內容,名字有個點,都叫“.text”、“.data”之類的,大部分顧名思義沒問題(詳情見參考資料[ELFstd])……像 .text section 就是ELF中專放代碼的部分。

這裏我們關心的是“.got”(Global Offset Table)和“.plt”(Procedure Linkage Table)兩個區域,因爲它們和ELF的動態鏈接有非常曖昧的關係—_—:當程序代碼在 .text 區裏 call 一個庫函數的時候,call 指令的操作數實際上不是對應庫函數映射到內存中的入口位置,而是 .plt 區域中的一個地址,這個地址對應類似下面的一條 jmp 指令。比方說我們在代碼區見到這樣一條指令 

45d23f: e8 04 b3 fb ff callq 418548  

這個用 objdump -d 可以看到(詳情見 man objdump),最後尖括號裏的東西是 objdump 自己加上去的,說明被 call 的這個地址在 plt 裏 chdir 對應的部分。下面就是 0x418548 附近的景象

0000000000418548 : 

418548: ff 25 b2 1c 2a 00 jmpq *0x2a1cb2(%rip) # 6ba200
41853e: 68 00 00 00 00 pushq $0x0
418543: e9 e0 ff ff ff jmpq 418528 <_init+0x18> 

第一個jmp指令裏的 %rip 是下面 push 指令的地址,所以是jump到了 0x41854e+0x2a1cb2=0x6ba200 這個內存單元的內容所指向的位置。不要被 objdump 加上去的註釋騙了—_—,那個地址不是代碼區內的,和compgen_doc函數也一點關係都沒有。

實際上那是 .got 區內的地址,而那個地址裏放的就是真正的chdir入口,是由 ld-linux 在將可執行文件扯進內存的時候根據文件內的符號填上的,這樣無論庫函數被映射到內存中的什麼偏僻地方我們的程序都能馬上找到,從而實現“位置無關”和“動態鏈接”這樣的東西。

總結一下,控制從用戶程序轉移到庫函數的過程就是call xxxx -> xxxx函數的plt表項 -> xxxx函數的got表項 -> 真正的xxxx庫函數入口其實蠻簡單的嘛……

接下來用gdb驗證一下上面的說法,因爲got表項只有在程序運行時才能查到(又廢話—_—)

[l_amee@localhost linux]$ gdb /bin/bash # 我們的目標是 bash :目
GNU gdb Fedora (6.8-17.fc9)(gdb的版權信息,省略)
This GDB was configured as "x86_64-redhat-linux-gnu"...(no debugging symbols found)
Missing separate debuginfos, use: debuginfo-install bash.x86_64
(gdb) startBreakpoint 1 at 0x41a2c0
Starting program: /bin/bash (no debugging symbols found)……0x000000000041a2c0 in main ()
(gdb) x/20xg 0x6ba1d8 # 這是用 objdump -h 看到的got起始地址
0x6ba1d8 <__mbrlen+2756720>: 0x0000000000000000 0x00000000006ba028
0x6ba1e8 <__mbrlen+2756736>: 0x000000000041853e 0x0000000000000000
0x6ba1f8 <__mbrlen+2756752>: 0x000000321eedb670 0x000000321eed6ec0
0x6ba208 <__mbrlen+2756768>: 0x000000321ee70a70 0x000000321eed6de0
0x6ba218 <__mbrlen+2756784>: 0x000000321ee35ee0 0x000000321ee809f0
0x6ba228 <__mbrlen+2756800>: 0x000000321ee37560 0x000000321ee82460
0x6ba238 <__mbrlen+2756816>: 0x000000321ee89410 0x000000321eea7260
0x6ba248 <__mbrlen+2756832>: 0x000000321ee887b0 0x000000321eef9d60
0x6ba258 <__mbrlen+2756848>: 0x000000321eed6510 0x000000321eedc180
0x6ba268 <__mbrlen+2756864>: 0x000000321ee33c00 0x000000321eed7370
(gdb) x/i 0x000000321eed6ec0 
0x321eed6ec0 :     mov $0x50,%eax # 我們找到 chdir() 了……
(gdb) 

可以看到上面列出了N多 0x321 打頭的地址,那是我的機器上庫函數的映射地址。頭4條記錄貌似是鏈接器幹完活之後留下的,包括一個回指指針指向第一條plt條目,我們忽略……

關於ELF就到這裏,因爲……那個……我懂的也不多—_—……有興趣的請移步到參考資料[ELFstd]

3、ptrace和Linux下調試器工作原理

呃,這個就涉及到調試器和strace、ltrace(詳見它們的manpage)等東西的工作原理了……以前我一直對ltrace和gdb這種東西的強大迷惑不已——究竟它們是什麼怪物,能在用戶態用跟蹤每個庫函數調用這種方式來蹂躪別的程序呢……直到我看到[PTRACE]這篇文章—_—,原因是它用了ptrace。

具體來說呢,ptrace是Linux的系統調用之一,由於調試器這種特殊工具需要追蹤別的程序甚至是查看和修改別人的內存空間(據我所知這是非常非常不禮貌的=_=),所以ptrace就有了存在滴理由……爲了幾個調試器提供一個系統調用,可見調試器多麼重要……而用調試器的都是程序員,可見程序員多麼重要……不好意思扯遠了……

這裏舉個gdb裏設置斷點的例子。當你給出地址讓gdb弄個斷點的時候,它名義上是被調試進程的父進程,能夠通過ptrace往那個進程的地址空間(包括一般情況下只讀的代碼區)寫入數據,這時它往斷點位置寫入一個 int3 斷點指令,然後讓子程序繼續運行,而自己進入wait()。

Linux的信號系統在子程序不幸遭遇到gdb填入的 int3 指令後會用 SIGCHLD 將gdb喚醒,然後gdb檢查斷點位置,用ptrace將自己改過的指令復原,將子進程的 %rip 退回到斷點前。如果這時我們輸入c讓被調試程序繼續運行,那個程序就會好像什麼事都沒發生一樣繼續運行了……

ltrace這邊原理據說也差不多,是通過設置斷點來偵測每個函數的調用情況。源碼我沒看過,有人看過而實現方法和這裏描述的有出入的話請務必指出。我的猜測是,一個程序調用庫函數的地方有那麼多,不可能每處都設了斷點,但是每次調用都必須經過plt和got(見上面ELF文件一節),而過了got就已經進入我們要鉤的函數了。

ltrace這類工具是不可能搞代碼注入的,改got 沒用,所以就只剩下在plt內設置斷點這個方法了(在我看來—_—)。具體請看下一節的實驗……

4、實現過程

這裏我們用gdb來模擬它自己背地裏乾的東西……而我們的目標是 bash 裏的chdir調用,也就是我們要hook的東西。首先請打開一個bash會話,這裏假定它的pid是10854。然後在另外一個bash會話裏:

[l_amee@localhost linux]$ gdb

GNU gdb Fedora (6.8-17.fc9)(版權信息……)
This GDB was configured as "x86_64-redhat-linux-gnu".
(gdb) attach 10854  # 附着到10854也就是另一個bash上面去
Attaching to process 10854
Reading symbols from /bin/bash...(no debugging symbols found)...done.
(N多其他讀取symbol的提示……)
(no debugging symbols found)
0x000000321eed6590 in __read_nocancel () from /lib64/libc.so.6
Missing separate debuginfos, use: debuginfo-install bash.
x86_64(gdb) x/xg 0x418548 # 這個是上面 objdump -d 找出來的地址(見第二節),plt裏chdir對應的條目
0x418548 :       0x0168002a1cb225ff # 這個記下,下面恢復用到(gdb) set *(0x418548) = 0xcc 
# 低半字改爲0x000000CC,即是int3斷點(這裏“字長”指64位)
(gdb) cContinuing.

這時被調試的 bash 會話恢復響應,進去打個 cd,cd是bash的內置命令,會調用chdir函數,然後由於碰到斷點它會看起來像死掉,這證明斷點設置成功了。如果是在程序裏我們可以馬上對函數的參數進行處理,達到hook的目的ohoho……

回到gdb:

Program received signal SIGTRAP, Trace/breakpoint trap. # gdb收到子進程停止的信號
0x0000000000418549 in chdir@plt ()(gdb) x/10xg $rsp-72   # 通過 %rsp 可以找到堆棧內容
0x7fff8f0635f0:     0x2f00000000000000      0x000000321f167a00
# 下面這一行兩個地址是chdir的參數,但是chdir的manpage裏面寫明參數只有一個,不明白怎麼回事……
0x7fff8f063600:  0x0000000002647dd0      0x000000000262a550 
0x7fff8f063610:  0x0000000000000000      0x000000000000000c
0x7fff8f063620:  0x0000000000000000      0x000000321ee7a866
0x7fff8f063630:  0x0000000000000000      0x000000000045d2dd
(gdb) x/s 0x0000000002647dd00x2647dd0:         "/home/l_amee" # 絕對路徑,看來bash在調用chdir前就已經把路徑解析了一邊哦……
(gdb) x/s 0x000000000262a5500x262a550:       "/home/l_amee" # 相對路徑,就是你輸入bash的cd參數
(gdb) set *(0x418548) = 0x0168002a1cb225ff # 恢復plt條目原來的內容
(gdb) set $pc = $pc-1  
# 倒回觸發斷點的地址重新執行,int3的長度是一字節
(gdb) cContinuing.

這時被調試的 bash 繼續照常運行……

上面整個過程就是第三節描述的截獲API調用的方法,只不過素“全手工打造”滴……步驟都有了寫個程序還不簡單……例程代碼見下節。另外貌似gdb對追蹤“追蹤別人的程序”不大在行—_—,所以例程如果用gdb調試的話可能工作不正常……這可以理解,畢竟沒多少人會用gdb來調試gdb……

5、程序代碼

這個程序用於鉤住bash裏的chdir調用,我的環境是Fedora9 64位版本+bash 3.2.33+gcc 4.3.0,新裝的系統,發現堆棧竟然是默認向高地址增長的(因爲函數參數在%rsp位置之前),如果你的機子不是這個情況可能程序要小小改一下……原理是利用上面的技巧將chdir的調用鉤住,修改其參數爲字符串“/hooked”,那麼每次chdir調用除了這個目錄外無處可去……

ps:本來想弄個給gtk界面加點“特效”的hook的,無奈有關函數太多,只好對bash下手了……

用法:ptrace_hook pid addr

其中pid是要鉤住的bash會話的pid,addr是chdir的plt條目地址,可以用objdump找到(見ELF文件那一節),鉤住後對應bash shell裏的cd命令無法使用,而是顯示類似下面的東西:

bash: cd: hooked: No such file or directory

證明鉤子生效。

 

#define STKALN 8 /* We use this to extract info from words read from the victim */

union pltval { 
    unsigned long val; 
    unsigned char chars[sizeof(unsigned long)];
};

void usage(char** argv){ printf("Usage: %s plt_posn", argv[0]);}

void peekerror(){ printf("Status: %sn", strerror(errno));}

/* function to modify the two parameters used by chdir */
void mod_test(pid_t traced, void* addr1, void* addr2) { 
    union pltval buf; 
    buf.val = ptrace(PTRACE_PEEKDATA, traced, addr1, NULL); 
    printf("--- mod_test: "); 
    peekerror();  
    memcpy(buf.chars, "hooked", 6); 
    buf.chars[6] = 0;  
    ptrace(PTRACE_POKEDATA, traced, addr1, buf.val); 
    printf("--- mod_test: "); 
    peekerror();  
    buf.val = ptrace(PTRACE_PEEKDATA, traced, addr2, NULL); 
    printf("--- mod_test: "); 
    peekerror(); 
    memcpy(buf.chars, "/hooked", 7); 
    buf.chars[7] = 0;  
    ptrace(PTRACE_POKEDATA, traced, addr2, buf.val); 
    printf("--- mod_test: "); 
    peekerror();
}

int main(int argc, char** argv) { 
    pid_t traced; 
    struct user_regs_struct regs; 
    int status, trigd=0; 
    unsigned long ppos; 
    union pltval buf; 
    unsigned long backup; 
    siginfo_t si; 
    long flag = 0, args[2]; 
    if(argc < 2) { 
        usage(argv); 
        exit(1); 
    } 
    traced = atoi(argv[1]); 
    ppos = atoi(argv[2]); 
    ptrace(PTRACE_ATTACH, traced, NULL, NULL); 
    printf("Attach: "); 
    peekerror(); 
    wait(&status); 
    buf.val = ptrace(PTRACE_PEEKDATA, traced, ppos, NULL); 
    backup = buf.val; 
    buf.chars[0] = 0xcc; 
    ptrace(PTRACE_POKEDATA, traced, ppos, buf.val); 
    ptrace(PTRACE_CONT, traced, NULL, NULL);  
    while(1){ 
        printf("I'm going to wait.n"); 
        wait(&status); 
        printf("Done waitingn"); 
        if(WIFEXITED(status)) 
            break; 
        ptrace(PTRACE_GETSIGINFO, traced, NULL, &si); 
        ptrace(PTRACE_GETREGS, traced, NULL, ®s); 
        if((si.si_signo != SIGTRAP) || (regs.rip != (long)ppos +1)) { 
            ptrace(PTRACE_GETREGS, traced, NULL, ®s); 
            ptrace(PTRACE_CONT, traced, NULL, NULL); 
            continue; 
        } 
        printf("Hook trigered: %ld timesn", ++flag); 
        printf("RSP: %lxn", regs.rsp); 
        int i; 
        for(i = 0; i < 2; i++) { 
            args[i] = ptrace(PTRACE_PEEKDATA, traced, regs.rsp-STKALN*(i+6), NULL); 
            printf("Argument #%d: %lxn", i, args[i]); 
        } 
        mod_test(traced, (void*)args[0], (void*)args[1]); 
        buf.val = backup; 
        ptrace(PTRACE_POKEDATA, traced, ppos, buf.val); 
        regs.rip = regs.rip - 1; 
        ptrace(PTRACE_SETREGS, traced, NULL, ®s); 
        ptrace(PTRACE_SINGLESTEP, traced, NULL, NULL);      // We have to wait after each call of ptrace(), 
        wait(NULL);   
        ptrace(PTRACE_GETREGS, traced, NULL, ®s);  
        buf.chars[0] = 0xcc; 
        ptrace(PTRACE_POKEDATA, traced, ppos, buf.val); 
        ptrace(PTRACE_CONT, traced, NULL, NULL); 
    } 
    return 0;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章