工作需要,需要注入其他程序監控一些東西,檢測到的數據通過WM_COPY 消息發送給顯示窗體。(大體是這樣的還沒定稿)
##1 選擇一個框架 ##
tombkeeper/Shellcode_Template_in_C
框架選擇上,我選擇了第一個,婦科聖手tomkeeper提供的框架,對比發現這個比較簡單。
##2 搭建框架 ##
我使用的是vs10,(vs15我試了下各種錯誤 ,可能是自己笨吧,用管了vs10了)
- 新建個解決方案(名字看自己喜好)
- 解決方案上新添個空的 win32console 工程
- 再在工程上新建個 xxxx.c的文件,把tomkeeper工程中的shellcode.c的內容復制過來
- 編譯試試吧
- 其他的2個工具文件代碼,分別是生成字符串用的工具(str2intarr.c) 和 生成函數hash宏定義的 gethash.c (方法同上)
- 我自己定義了個字符串生成的增強版(如下,我這個是cpp文件,和上面的有所不同),根據 第2個參數 <a/w> 將字符串轉為多字節字符串和寬字節字符串。
#include <Windows.h> int _tmain(int argc, char* argv[]) { if (argc < 2) { printf("Use %s <a/w> <string> \n",argv[0]); }else { printf("DWORD str[] = {\n\t"); if (argv[1][0] == 'a') { int strLen = strlen(argv[2]); strLen = (strLen /4 +1)*4; int n = strLen /4 ; DWORD* pArry = (DWORD*)malloc(strLen); ZeroMemory(pArry,strLen); memcpy(pArry,argv[2],strlen(argv[2])); for (int i=0;i<n;i++ ) { printf("0x%08x",*(pArry+i)); if (i != n-1) { printf(", "); } if (i && i%9 ==8) { printf("\n\t"); } } }else { int strLen = strlen(argv[2]); int nAlloc = strLen*2+4; DWORD* pWchar = (DWORD*)malloc(nAlloc); ZeroMemory(pWchar,nAlloc); MultiByteToWideChar(GetACP(),NULL,argv[2],strLen,(WCHAR*)pWchar,strLen); int n = (strLen/2+1); for (int i=0;i< n;i++) { printf("0x%08x",*(pWchar+i)); if (i != n-1) { printf(", "); } if (i && i%9 ==8) { printf("\n\t"); } } } printf("\n};"); //getchar(); } return 0; }
##3 編譯選項的設置 ##
編譯選項的設置直接關系到shellcode大小的准確計算,代碼的排列順序,以及代碼的精簡程度,還有 GS。。。 是否被添加。
- 設置為release模式,我就不多說了,大家都會
- 去掉Gs 選項,如果不去掉shellcode()中會有gs檢測函數的插入,這是我們不想要的。
- opt, 調試時我選擇 disable, 發布時我使用 minsize (體積小些)。disable代碼順序是一樣的就是多了不少int3 (cc)。
- linker 下的debuging :generate Debug info 我設置為YES。便於調試,對shellcode無影響
##4 測試shellcode的正確性 ##
tomkeeper的工程能將shellcode以c數組的形式輸出出來,我增加了個函數可以將 shellcode dump到2進制文件中。
- 利用c數組, char shellcode[] = {0xb8,....}; __asm{ call shellcode} ,這樣的話要注意將 工程的NX關閉(數據執行保護DEP) ,不會的自行補腦
- 第2種將 dump出的文件讀入到申請的可讀可寫可執行的內存中,然后 __asm { call lpMem} .(我用的這種辦法,不用每次進行復制黏貼,編譯,執行)
FILE* file = 0; int iRead = 0; HANDLE hFile =CreateFile("c:\\temp\\c.sc",GENERIC_WRITE|GENERIC_READ,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL); fprintf(stdout,"test shellcode ........... \n"); if (hFile != INVALID_HANDLE_VALUE) { DWORD dwFileSizeHigh = 0; DWORD dwFileSizeLow = GetFileSize(hFile,&dwFileSizeHigh); fprintf(stdout,"file size is 0x%x \n",dwFileSizeLow); BYTE* lpMem = (BYTE*)VirtualAlloc(NULL,dwFileSizeLow+4,MEM_COMMIT|MEM_RESERVE,PAGE_EXECUTE_READWRITE); ZeroMemory(lpMem,dwFileSizeLow+4); ReadFile(hFile,lpMem,dwFileSizeLow,&dwFileSizeHigh,NULL); printf("should read %d ,readed %d byte \n",dwFileSizeLow,dwFileSizeHigh); CloseHandle(hFile); //getchar(); _asm{ call lpMem } }else { fprintf(stdout,"open file error [0x%x] \n",GetLastError()); }
我先把shellcode dump到 c:\\temp\\c.sc 中,在用這個代碼去測試它。
##5 小技巧 (排錯、編程 經驗) ##
- 不要用 *1.02 這種浮點算法,會死在這種命令上,那個2B2040絕對會出錯,所以盡量別用了。
000300B3 DF6D D8 FILD QWORD PTR SS:[EBP-28]
000300B6 DC0D 40202B00 FMUL QWORD PTR DS:[2B2040]
000300BC D97D D6 FSTCW WORD PTR SS:[EBP-2A]
- 如何在shellcode后面追加上 配置信息便於測試呢
shellcode會帶着些配置信息很長見的。 一般這些都是后期用工具合並上去的。
對於我們調試每次都去追加修改太麻煩了。所以要用到 #define NAKED __declspec(naked) 和 __asm{ __emit 0xbb }
__emit 可以使數據以代碼的形式寫入到代碼段,也就是shellcode中。記得和 DWORD str[] = {0xabcdef,0xbaddef..}這種技術分開。這樣就實現了任意數據的存入
NAKED 進制優化:當我們使用minsize選項的時候,這些代碼可能就被優化掉,我試過了,所以才用到了NAKED,注意函數必須有 參數,聲明不用實現用。
最后:在永遠不會調用到的地方調用下,要不編譯器一定會給你省略掉
void StoreConfigData(DWORD dwVoid); NAKED void StoreConfigData(DWORD dwVoid) { __asm{ __emit 0xff __emit 0xfe __emit 0xff .}
我這里專門寫了個小工具將bin文件轉為上面的形式
// bin2emit.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <Windows.h> int _tmain(int argc, _TCHAR* argv[]) { if (argc < 2) { printf("Use %s <path of bin> \n",argv[0]); }else { printf("path of bin: %s \n",argv[1]); HANDLE hFile = CreateFile(argv[1],GENERIC_WRITE|GENERIC_READ,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL); if (hFile == INVALID_HANDLE_VALUE) { printf("open '%s' error \n",argv[1]); return 0; } DWORD dwFileSizeLow,dwFileSizeHigh; dwFileSizeLow = GetFileSize(hFile,&dwFileSizeHigh); LPVOID lpBuffer = HeapAlloc(GetProcessHeap(),0x08,dwFileSizeLow); if (!ReadFile(hFile,lpBuffer,dwFileSizeLow,&dwFileSizeHigh,0)) { CloseHandle(hFile); printf("read file '%s' error \n",argv[1]); HeapFree(GetProcessHeap(),HEAP_NO_SERIALIZE,lpBuffer); return 0; } CloseHandle(hFile); printf("\n\n\n __asm{\n\t"); for ( int i=0;i < dwFileSizeLow;i++) { printf("__emit 0x%02x \n\t", *(((LPBYTE)lpBuffer)+i) ); } printf("\n}"); } return 0; }
- 函數重定位
我們的shellcode都是經過 call聯系出來的,在調用的時候編譯器已經算好了偏移,所以不存在重定位的問題,但是,向createthread這種將傳入函數地址作為參數的函數就會出先這種問題;
解決的辦法就是要動態計算地址。
//記得-8 DWORD GetEip() { __asm{ call get_eip get_eip: pop eax } } void WINAPI AdjustFunction(struct ADJUSTFUNCTIONS* pFuns) { DWORD dwGetEip; dwGetEip = GetEip(); dwGetEip -= 8; //計算得到 GetEip()函數在內存中的地址 pFuns->lpThreadProc = (DWORD)dwGetEip+( (DWORD)ThreadProc- (DWORD)GetEip ); // 以GetEip()函數做參考動態計算出其他 函數的地址 }
// 調用的時候這樣寫
hThread = modules.kernel32.CreateThread(NULL,0,adjustFuns.lpThreadProc,¶m,0,NULL);
- 函數動態獲取
開發中發現通過函數hash獲取時會出現和GetProcAddress返回地址不同的問題。出現以上問題有3種可能:
1.hash碰撞了,沒辦法那只能找替換函數了,例如CreateFile 函數,可以看看ntdll重的zw 、nt、rtl、有沒有名稱相似的函數,我們知道很多函數都是層層封裝的,不怕找不到的。再說tk教主的hash計算時帶着i也就是長度進行了計算,大大減小碰撞幾率。
2.函數名搞錯了,注意 xxxA xxxB xxxx 有些函數有A和W版本,你可要看清楚自己要的是哪個,但有些函數沒有A和W區分。還有很多函數是通過lib鏈接的,根本導不出函數如 —beginthreadex,所以還是用 createthread代替吧 。
3. 還有些函數導出地址不是最終真正的函數地址,而是像ntdll.xxxxx的這種形式的內存地址,這樣就還要再次去計算才能得到,而GetProcAddress這個函數應該進行了2次轉換。例如:HeapAlloc是kernel32導出的,通過hash找到的是ntdll!RtlAllocateHeap字符串的地址(大約是這個樣子,反正是個ntdll開通的),所以還是直接去ntdll下找 RtlAllocateHeap 它吧,所以 找函數盡量去ntdll中招吧。
http://www.onlinedown.net/soft/59825.htm
http://www.pc6.com/softview/SoftView_16910.html dllviewer 可以看看dll導出函數表
- 初始化局部變量
struct MODULES modules = {0x00};
這種初始化還是不要有的好,我跟蹤發現這樣會讓執行流程變亂。
還是用自己寫的memset吧。
ps:shellcode真省地方,寫了這么多代碼還沒2K呢。
# 自我實現一些函數 #
有些函數沒必要動態獲取,太麻煩了。很多函數很簡單,用代碼就能實現
- GetProcessHeap (測試環境 win7 x64 \win8 x64 \win10 x64)
HANDLE WINAPI GetProcessHeap() { __asm{ mov eax,fs:[0x30] mov eax,[eax+0x18] } }
- memcpy
void WINAPI zMemcpy(LPVOID lpDest,LPVOID lpSrc,DWORD dwSize) { if (!lpDest || !lpSrc ||!dwSize) { _asm int 3 } __asm{ mov ecx,dwSize mov edi,lpDest mov esi,lpSrc rep movsb } }
為了兼容x64shellcode開發,這里將內嵌的匯編都去掉了。
最近發現在大數據除法和取余時,vs會嵌入alldiv、allrem等函數,這將直接造成,shellcode跑飛