調試httpd通過fork+execute調用的cgibin程序

在調試dlink的httpd時,漏洞可能發生在httpd通過fork+execute調用的cgibin中,其中httpd解析網絡請求中的字段,并且以環境變量的形式傳遞給cgibin進行處理。那么要調試cgibin就有兩種方式:
① 設置環境變量,然后直接調試cgibin。
② 了解httpd是如何調用cgibin的,然后通過設置gdb調試子進程、catch exec,調試cgibin。
方法1的優點是簡單直接,缺點是需要了解httpd是如何處理、傳遞數據到環境變量,以免使用了實際上通不過httpd校驗的環境變量;方法2的優點是直觀,但是需要熟練掌握gdb調試子進程相關的知識。
一、httpd調用CGI上下文說明
和許多典型的httpdserver一樣,傳入到函數process_cgi中的參數a1是httpd所定義的數據結構,其中包含了一個網絡請求的數據集合,例如請求方式REQUEST_METHOD、URI、SESSION等等。a1傳遞到process_cgi函數中進行處理,獲取到需要調用的cgi,以及將需要處理的數據轉化成環境變量集合到cgi中。在dir 850l的固件中,幾乎所有的cgi都是通過鏈接的形式到程序cgibin,cgibin根據請求的不同來采用不同的接口(函數)進行處理。
int __fastcall process_cgi(_DWORD *a1){...... v77 = spawn(*filename, filename, argv, v79, v75, v9, v8, a1 + 992);......}
spawn函數則是一個典型的封裝了fork+execve的函數,通過fork函數創建子進程,設置子進程的進程組、資源限制、重定向輸入輸出等。
__pid_t __fastcall spawn(const char *filename, char *const *argv, char *const *envp, int a4, int a5, int a6, int a7, char *path){ __pid_t result; // $v0 int v13; // $a0 int v14[4]; // [sp+18h] [-18h] BYREF __pid_t v15; // [sp+28h] [-8h] result = fork(); // 創建子進程,此時的子進程依舊是httpd的程序鏡像 if ( result == -1 ) { v15 = -1; lerror("spawn: failed to create child process"); goto LABEL_6; } if ( !result ) { setpgid(0, 0); // 改變子進程的進程組 sub_409C9C(13, 0); if ( coredir ) { v14[3] = 0; v14[2] = 0; v14[1] = 0; v14[0] = 0; setrlimit64(4, v14); // 設置資源限制 } dup2(a4, 0); // 復制文件描述符:標準輸入和標準輸出 dup2(a4, 1); if ( a5 != -1 ) dup2(a5, 2); if ( chdir(path) == -1 ) { v13 = 5; } else { execve(filename, argv, envp); // 執行cgibin,將cgibin加載并替換掉子進程的httpd v13 = 6; } exit(v13); } ++dword_42350C; if ( debug ) { v15 = result; log_d("child process %d created", result);LABEL_6: result = v15; } return result;}
那么到此,回想之前的標題:如何調試httpd通過execute調用的cgibin,該問題就可以抽象為:如何調試子進程中通過execute調用的可執行文件。
二、調試原理
默認情況下,gdb在調試多進程的時候,只會追蹤父進程,例如執行完fork函數,fork的返回值是子進程的pid,gdb中實際上在調試的是父進程。如果要調試子進程,則需要在gdb中使用如下的命令:
set follow-fork-mode child
上面命令解決了讓gdb調試到子進程,但是有時候還需要同時調試父進程和子進程,如果僅僅是gdb在子進程中,父進程依舊會正常運行。那么,就可以使用如下的命令,使得在調試子進程的時候,父進程也暫停處于掛起的狀態:
set detach-on-fork off # 默認是on
上面兩條命令結合起來就實現了同時調試父進程和子進程,那么這個時候也還有一個問題:在子進程中,execute調用cgibin是作為一個函數來實現的,單步步過該函數達不到調試目的,步進該函數更加容易陷入到函數的細節實現中。
對于調試cgibin,可以通過catch exec命令,來捕獲執行新進程的事件。當進程使用execute重新執行一個程序時,gdb會中斷程序的運行,到ld加載器start函數中。除此之外,還可以使用例如catch exec /bin/ls來指定需要捕獲的具體進程加載程序事件。
綜上所述,調試通過fork+execute調用的程序,可以使用如下步驟:
① 在fork的時候,執行gdb命令:set follow-fork-mode child,使得gdb開始調試子進程。
② 同時,可以執行gdb命令:set detach-on-fork off,讓在調試子進程的同時,父進程掛起。這樣執行完子進程也可以返回到父進程中。
③ 執行gdb命令:catch exec,捕獲子進程通過execute加載執行新程序的事件。
三、調試過程
現在回歸到具體的調試過程中。
當httpd執行到調用cgi的spawn函數中,此時準備執行fork函數,可以看到只有httpd一個進程,也就是父進程PID=1444:
pwndbg> info inferiors Num Description Executable* 1 process 1444 /home/utest/app/FirmAE/firmwares/_DIR850L_FW115KRb07.bin.extracted/squashfs-root/sbin/httpd
第一步需要保證在執行到spawn的時候,fork之后是進入了子進程,
0x409d2c 56> move $s3, $a2? 0x409d30 60> jalr $t9 <fork>
此時,執行gdb命令:
set follow-fork-mode childset detach-on-fork off

再查看進程信息,也可以確認此時gdb處于子進程中。此時也可以通過inferiors Num切換到父進程。
pwndbg> info inferiors Num Description Executable 1 process 1444 /home/utest/app/FirmAE/firmwares/_DIR850L_FW115KRb07.bin.extracted/squashfs-root/sbin/httpd* 2 process 31740 /home/utest/app/FirmAE/firmwares/_DIR850L_FW115KRb07.bin.extracted/squashfs-root/sbin/httpd
繼續在子進程中執行到execute函數,可以看到此時正準備加載執行/htdocs/web/session.cgi,該cgi實際上是一個鏈接到cgibin。
0x409e68 addiu $a0, $zero, 5 0x409e6c lw $t9, -0x7d7c($gp) 0x409e70 move $a0, $s2? 0x409e74 jalr $t9 path: 0x7ed330 ?— '/htdocs/web/session.cgi' argv: 0x4280f0 —? 0x7ed330 ?— '/htdocs/web/session.cgi' envp: 0x0
那么此時就應該執行gdb命令:catch exec,用來捕獲子進程加載cgi的事件:

這個時候可以查看棧,來看httpd傳遞給cgi哪些環境變量(也就是需要處理的數據):
pwndbg> stack 3000:0000│ fp sp 0x7fe2b4a0 ?— 0x101:0004│ 0x7fe2b4a4 —? 0x7fe2bd23 ?— '/htdocs/web/session.cgi'02:0008│ 0x7fe2b4a8 ?— 0x003:000c│ 0x7fe2b4ac —? 0x7fe2bd3b ?— 'HTTP_HOST=192.168.0.1'04:0010│ 0x7fe2b4b0 —? 0x7fe2bd51 ?— 'HTTP_USER_AGENT=Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/110.0'05:0014│ 0x7fe2b4b4 —? 0x7fe2bdb0 ?— 'HTTP_ACCEPT=*/*'06:0018│ 0x7fe2b4b8 —? 0x7fe2bdc0 ?— 'HTTP_ACCEPT_LANGUAGE=en-US,en;q=0.5'07:001c│ 0x7fe2b4bc —? 0x7fe2bde4 ?— 'HTTP_ACCEPT_ENCODING=gzip, deflate'08:0020│ 0x7fe2b4c0 —? 0x7fe2be07 ?— 'HTTP_ORIGIN=http://192.168.0.1'09:0024│ 0x7fe2b4c4 —? 0x7fe2be26 ?— 'HTTP_REFERER=http://192.168.0.1/index.php'0a:0028│ 0x7fe2b4c8 —? 0x7fe2be50 ?— 'HTTP_COOKIE=uid=bjtsPYnEJz'0b:002c│ 0x7fe2b4cc —? 0x7fe2be6b ?— 'GATEWAY_INTERFACE=CGI/1.1'0c:0030│ 0x7fe2b4d0 —? 0x7fe2be85 ?— 'CONTENT_LENGTH=31'0d:0034│ 0x7fe2b4d4 —? 0x7fe2be97 ?— 'CONTENT_TYPE=application/x-www-form-urlencoded'0e:0038│ 0x7fe2b4d8 —? 0x7fe2bec6 ?— 'SCRIPT_FILENAME=/htdocs/web/session.cgi'0f:003c│ 0x7fe2b4dc —? 0x7fe2beee ?— 'REQUEST_URI=/session.cgi'10:0040│ 0x7fe2b4e0 —? 0x7fe2bf07 ?— 'REMOTE_ADDR=192.168.0.2'11:0044│ 0x7fe2b4e4 —? 0x7fe2bf1f ?— 'REMOTE_PORT=45050'12:0048│ 0x7fe2b4e8 —? 0x7fe2bf31 ?— 'REQUEST_METHOD=POST'13:004c│ 0x7fe2b4ec —? 0x7fe2bf45 ?— 'SCRIPT_NAME=/session.cgi'14:0050│ 0x7fe2b4f0 —? 0x7fe2bf5e ?— 'SERVER_NAME=192.168.0.1'15:0054│ 0x7fe2b4f4 —? 0x7fe2bf76 ?— 'SERVER_ADDR=192.168.0.1'16:0058│ 0x7fe2b4f8 —? 0x7fe2bf8e ?— 'SERVER_PORT=80'17:005c│ 0x7fe2b4fc —? 0x7fe2bf9d ?— 'SERVER_SOFTWARE=Mathopd/1.6b9'18:0060│ 0x7fe2b500 —? 0x7fe2bfbb ?— 'SERVER_ID=LAN-1'19:0064│ 0x7fe2b504 —? 0x7fe2bfcb ?— 'SERVER_PROTOCOL=HTTP/1.1'1a:0068│ 0x7fe2b508 ?— 0x0
以前做畢設的時候,要實現對cgi的模糊測試,就是將AFL生產的數據通過設計的數據結構轉換成環境變量到cgi 中去執行。
四、小結
如何調試httpd使用fork+execute執行的cgibin,本質上可以抽象為:如何調試通過fork+execute調用的程序,辦法是:
① 執行到fork函數,執行gdb命令:set follow-fork-mode child,使得gdb調試到子進程中。
② 執行gdb命令:set detach-on-fork off,使得gdb在調試子進程的時候掛起父進程,這樣也方便并行調試。
③ 執行到execute函數處,執行gdb命令:catch exec,捕獲子進程通過execute調用執行cgi的事件。