本文為看雪論壇優秀文章
看雪論壇作者ID:psycongroo
DWORD WINAPI ThreadProc( _In_ LPVOID lpParameter);所以我們只要定義只有一個參數的函數,把它轉換成LPTHREAD_START_ROUTINE(這是一個指向上面模板的指針)就可以了。可問題來了,參數只有一個,萬一我的函數用了若干個參數呢?而且這個參數必須位於目標進程的虛擬內存當中,否則是無法使用的。對於第二個問題,我們可以使用VirtualAllocEx函數向目標申請內存虛擬空間。對於第一個問題,我們可以構建一個結構體,存放所有的參數,然後在調用的時候通過內存偏移來訪問參數:typedef struct _INJECT_DATA{ char lpText[8]; char lpCaption[8]; }INJECT_DATA;那有沒有更加方便快捷的方法呢?結合上面兩點就產生了本篇文章的主旨內容Shellcode。有人說Shellcode僅限於Linux和unix,而經百度似乎也有很多種說法。個人理解是一段被注入後可獨立運行,依賴性少或完全沒依賴的代碼。為了撇開爭論,暫且保持書上所說的那樣,稱之為Shellcode。說的直白一點,其實就是把參數,要運行的函數地址全部放到結構體裡,最後以一段用彙編編寫的Shellcode運行起來。以調用MessageBoxA彈出窗口的Shellcode為示例。首先是存放Shellcode和參數,函數地址的結構體:typedef struct _INJECT_DATA{ BYTE shellCode[0x1D]; char lpText[8]; char lpCaption[8]; LPVOID lpThreadStart; }INJECT_DATA;__declspec (naked)void shellCodeFun(void) { __asm { call L001; L001: pop ebx; //sub ebx, 5; and bx, 0; push 0; lea esi, dword ptr ds : [ebx]INJECT_DATA.lpCaption ; //0x25注意偏移量 push esi; lea esi, dword ptr ds : [ebx]INJECT_DATA.lpText ; //0x1D push esi; push 0; call dword ptr ds : [ebx]INJECT_DATA.lpThreadStart ; //0x2D ret; }}最後把整個結構體通過CreateRemoteThread注入即可。因為結構體的起始地址就是Shellcode的起始地址,因此注入後Shellcode就會運行起來。
步驟一定義和初始化:定義結構體和編寫Shellcode,當然這些都是根據自己的需求來具體定義的,在這裡同樣以彈框來做示例(Shellcode代碼在上面,這裡不贅述):INJECT_DATA data; ZeroMemory(&data, sizeof(INJECT_DATA)); PBYTE pShellCode = (PBYTE)shellCodeFun;#ifdef DEBUG if (pShellCode[0] == 0xE9) { pShellCode = pShellCode + *(ULONG*)(pShellCode + 1) + 5; }#endif memcpy(data.shellCode, pShellCode, sizeof(data.shellCode)); char text[] = "message"; char title[] = "title"; memcpy(data.lpText, text, 8); memcpy(data.lpCaption, title, 8); HMODULE hmod = GetModuleHandleA("user32.dll"); FARPROC dialog = GetProcAddress(hmod, "MessageBoxA"); data.lpThreadStart = dialog;1. 結構體需要初始化,用諸如ZeroMemory的函數來進行初始化,不然會報錯。call L001; //自己是為了自定位,為了拿到Shellcode開始的地址,也即是結構體的地址。 L001:pop ebx; //拿出call時保存的返回地址and bx, 0; //因為使用VirtualAllocEx函數申請的虛擬內存都是64kb對齊的,只要低地址。push 0;lea esi, dword ptr ds : [ebx]INJECT_DATA.lpCaption ;....3. 注入進程是debug版本,需要對Shellcode地址做額外處理:#ifdef DEBUG if (pShellCode[0] == 0xE9) { pShellCode = pShellCode + *(ULONG*)(pShellCode + 1) + 5; }#endif
步驟二申請內存並寫入:通過VirtualAllocEx申請虛擬內存,通過WriteProcessMemory寫入虛擬內存:int injectSize = sizeof(INJECT_DATA);PBYTE mBuffer = (PBYTE)VirtualAllocEx(mProcess, NULL, injectSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE); WriteProcessMemory(mProcess, mBuffer, &data, injectSize, NULL)步驟三建立遠程線程執行:mRemoteThread = CreateRemoteThread(mProcess, NULL, 0, (LPTHREAD_START_ROUTINE)mBuffer, NULL, 0, NULL);
第一次寫Shellcode,雖然看上去簡單,但卻遇到了很多問題。例如什麼是Shellcode中的自定位,Shellcode中如何使用參數,如何使用調試器調試等等。說實話這次將MessageBox作為示例,但個人感覺似乎不太妥當。因為只有當目標程序加載了User32.dll,這段Shellcode才能生效。也就是說這裡的Shellcode對User32.dll這個庫產生了依賴。或許可以在ntdll.dll中找到與之對應的函數,調用這個才更符合Shellcode無依賴的定義吧。Shellcode的應用很廣泛,粗略看了《加密與解密》,書中介紹到很多注入都是編寫Shellcode並利用形同如CreateRemoteThread函數能執行一段函數的特性來實現的。所以以後凡是看到一個函數能調用其另一個函數,就有這種注入的可能。
第一條:使用調試器調試這條才是最重要的,因為所有的疑問都能在調試器裡找到答案。為了調試遠程線程,首先我們在visual studio中對CreateRemoteThread打上斷點:然後用ollydbg或者x32dbg附加目標程序,隨後設置線程開始斷點:再然後運行我們的注入程序,程序會在創建遠程線程的地方斷點:同時記錄我們調用VirtualAllocEx時返回的內存地址,這是我們Shellcode所在的目標進程的地址。在調試器中跳到這個地址,然後我們就能看到自己寫的Shellcode:當然我們來這裡不是為了旅遊的,打下斷點,並且讓visual studio中的斷點繼續運行,調試器會在線程開始的地方斷下(因為我們設置過):其實只要找到Shellcode地址下斷就行了,並不需要在olldbg/32xdbg中設置線程開始的斷點。只不過這樣做是為了更直觀的看到線程的工作流程。
第二條Shellcode自定位為什麼要用到Shellcode自定位,因為我們要確定Shellcode在目標程序中的虛擬內存地址。為什麼要用到這個地址,因為我們Shellcode中用到的參數,如文中彈窗標題,內容等,都需要通過這個地址來尋找。為什麼能通過Shellcode地址定位到我們參數的地址?因為我們的Shellcode和參數都是放到同一個結構體中的,而結構體裡的成員地址都是連續的。結構體的首地址便是結構體中定義的第一個成員的地址(文中的是Shellcode)。使用Call會將Call下一條的地址入棧,隨後使用pop拿出來這條地址,通過低16位and運算置0(書上說的是因為使用VirtualAllocEx申請的內存大小是64kb對齊,說實話我也沒搞懂),或者sub減去5(Call 地址指令的大小),就能拿到基址:這樣,我們就可以通過基址+Shellcode大小+偏移量拿到參數,也可以通過dword ptr ds : [ebx]INJECT_DATA.lpCaption這樣的方法拿到參數。
第三條debug版本中Shellcode地址處理當我們的注入程序是debug版本,在把Shellcode寫入結構體前,需要對Shellcode的地址進行處理:#ifdef DEBUG if (pShellCode[0] == 0xE9) { pShellCode = pShellCode + *(ULONG*)(pShellCode + 1) + 5; }#endif 這是因為處於debug版本的注入程序下,會在跳到真正的函數地址前多出一個jmp指令的中間層:如果此時沒有對地址進行處理,本來應該複製Shellcode的代碼到結構體成員當中,現在卻複製了 jmp xxxx代碼,從而造成錯誤。而處理的方法就是將當前指向jmp代碼的地址加上jmp後面的地址,最後在加上整個jmp指令的長度。真正的Shellcode地址=006E180C(當前指向jmp的地址)+9EFF(jmp指令後面的地址數據)+5(jmp指令長度)=6EB710。造成這種現象是因為debug版本下編譯器並沒有進行優化,使得函數的調用都有一個jmp表,如上圖。而十六進位為E9的jmp為近距離跳轉,也就是說jmp後面跟的地址是相對於這條jmp指令的相對地址,也叫偏移。因此,兩者相加,最後加上指令本身的長度,就能獲得原地址了。
附件中的源程序包含之前文章DLL注入代碼,可無視,輸入進程PID後,按1即可進行Shellcode注入。https://share.weiyun.com/AhueFz5l看雪ID:psycongroo
https://bbs.pediy.com/user-home-899080.htm
*本文由看雪論壇 psycongroo 原創,轉載請註明來自看雪社區。
掃碼購票立享2.5折優惠!
十大乾貨議題收入囊中!