背景
好久没分析样本了,最近闲暇之余有时间又刷了一下Bazzar.abuse.ch,下载并分析了一些最近上传的样本,其中有一个被标记为了Gozi的样本比较有意思,在这里分享给大家。
样本分析
基本信息
样本来源:https://bazaar.abuse.ch/sample/dddf5829a3bdcb2b6562eb194a138f8de5da26eb5dda0bbfacbbf1124ad51ec6/
家族基本信息:
银行木马Gozi
由于目前此类样本的诱饵较多,这种银行木马的诱饵基本都千篇一律,这里就不再赘述诱饵相关信息,直接看看样本。
代码分析
样本WinMain
好久没分析样本了,先通过WinMain复习一下汇编阅读吧~
样本是由vc系列编译器编译的exe,有着标准的WinMain函数,WinMain函数首先是进行了栈初始化,然后:
- 程序首先是将Strings1变量的内容赋值给了eax寄存器
- 将eax+1地址处的值取出来赋值给了edx,后面将会使用该值进行跳转的条件判断
- xor ebx,ebx 将ebx寄存器的值清零。
- 0041012B到00410130是一个小循环,该循环中程序首先将eax指针所指的值赋值给cl,然后指针自增,判断cl是否等于bl,上面ebx清零的操作已经将bl置为零,所以这里实际是在判断eax指针指向的内容是否为0。所以此操作是在判断eax所指向字符串的有效长度,此操作执行完成之后,eax会指向String1字符串的尾部。
- 计算完成之后,使用eax减去edx即可获得String1字符串的长度,接着比较长度是否等于722359,如果长度等于,程序则会继续执行后面的代码,否则跳转到loc_4101AB继续执行。
这里第一次运行的时候,String1的长度明显是空,理论上来讲,如果没有在CRT中操作String1的值,那么这里代码肯定会无法通过长度比较,从而跳转到后面执行。
接着往下看,String1的长度比较比较之后,到loc_4101AB处程序会判断dwSize的值是否等于976h,如果相等,则会依次调用ReleaseMutex、InitAtomTable和QueryPerformanceFrequency函数。其中ReleaseMutex和InitAtomTable参数均为0;而QueryPerformanceFrequency函数常在多线程的情景中使用。
若dwSize不等于976h,程序则会判断dwSize是否等于0CD2h,若相等则创建一个管道。
接着程序分别判断了两个变量,变量1(ESI)是否大于21E49Fh,变量2(ebp+3A8h+var_418)是否不等于7239C9EEH.
这里如果esi不大于21E49Fh或者变量2不等于7239C9EEh,程序则会跳转到loc_41020D的地方,给esi自增,自增之后判断esi是否大于等于38F964FFh,如果不等于则跳转回循环一开始的地方执行。否则就跳转到loc_410236继续执行。
通过简单的分析,可以知道这里的循环只有两个出口,就是0041020b处的jnz loc_410218和410216处的jmp loc_401236。
可以看到的是,两个出口都依赖于esi的值,由于第一个出口比较的值比第二个小,所以程序会从一个出口出来,出来之后,esi的值应该为21E49F+1 = 21E4A0
退出循环之后,程序会再次比较dwSize的值是否等于0D47,若相等则调用FindFirstFileEx查找文件,否则跳转到loc_410256依次调用sub_40FC99函数和sub_401AC7函数,然后WinMain结束。
这里可以看出来,前面应该都是环境验证的操作。当程序第一次运行的时候,只会执行WinMain最后面的这两个函数。
WinMain->sub_40FC99
由于样本代码太长,汇编代码实在太多,无法将分析思路全部记录下来,接下来将会只记录重点操作。
进入到40FC99函数之后,程序首先是进行了三个判断,分别是判断
ebp+var_8A8变量是否等于4CAD2h
String的长度是否等于0B4F43
String1的长度是否等于31295h
若三个条件同时满足,程序则会调用GetFileAttributesEx,否则跳转到loc_40FD4C继续执行,这里想都不用想,第一次执行的时候肯定是不满足。
往下走之后,程序会有非常多的判断,代码很没有条理性,第一次运行的时候每个判断都没通过,不知道是否是为了干扰分析人员。
其中比较关键的操作是通过LocalAlloc函数分配了一段堆空间:
且堆空间分配成功之后,会赋值到0x506EAFC
之后在sub_40FADF函数中对这片内存进行解密:
解密之后会在函数底部,通过jmp lpAddress的操作,这里可以可以看到地址是写死的,就是0506EAFC
所以可以直接在这个地址里面对应的堆空间首地址设置断点F9跑过来:
通过05266493函数解密API地址:
将LoadLibrary和GetProcAddress两个API解密出来之后,程序会开始动态解密其他所需的API:
比较值得关注的API有:
Sleep
VirtualAlloc
CreateToolhelp32Snapshot
Module32First
解密之后,程序首先会通过CreateToolhelp32Snapshot创建一个进程快照,然后通过Module32First检索对应的模块信息:
接着程序VirtualAlloc分配了一个地址空间:
接着在05266902这个函数中解密出shellcode存放在这片内存中,并通过一个jmp跳转到shellcode执行:
shellcode
过来的shellcode依旧是按照之前的方法解密出LoadLibrary和GetProcAddress两个API,然后去加载后续的API。
在这段shellcode中新解密的API有:
VirtualAlloc
VirtualProtect
VirtualFree
GetVersionEx
TerminateProcess
ExitProcess
SetErrorMode
API解密完成之后,程序会首先通过SetErrorMode函数来判断当前的运行环境是否在沙箱中,如果在则调用ExitProcess退出进程。
接着调用GetVersionEx获取当前操作系统的版本信息
接着再次VirtualAlloc分配一段内存空间
通过循环解密,根据下面的VirtualProtect和解密出来的第一个字节 4D,这里很明显是在解密一个PE,等下加载执行。
解密完成之后,程序通过VirtualProtect将00400000开始的地址更改为可读可写可执行属性
设置之后,将00400000开始的地址清空:
然后把先前解密出来的PE移过来到00400000开始的地方:
通过多次调用,将所有数据移过去之后,调用VirtualFree释放原本的PE空间:
重新load Kernel32.dll
然后通过GetProcAddress获取各个API地址,由于此次获取的API地址过多,这里就不一一列举了。
这里是个大循环,会依次获取每个dll的API,直接在后面设置断点即可:
所有API加载完毕,程序会调用终止函数atexit,然后jmp eax的方式跳转到新的PE中继续执行。
新PE分析
调试这种shellcode的时候,可以考虑建立一个快照,然后对一些常用的API设置断点以防程序跑飞,比如
VirtualAlloc
CreateThread
CreatePorcess
OpenPorcess
……
其他的API根据样本行为再添加
转入的新PE首先通过SwitchToThread准备线程调度。
接着还是分配了一段内存空间用于存放一些字符串
然后将这部分内存拷贝到00404000处,清空此片内存:
创建当前进程的长路径:
APC线程注入:
新线程分析
创建文件的内存映射:
再次解密一个PE,跳转进去执行:
内存PE代码混淆比较严重,但是也是关键的模块了,程序会获取当前主机的一些基本信息进行网络请求。
比如获取UserName
获取ComputerName
断点设置在00393d5d
设置网络请求参数:
soft=3&version=250161&user=87704afd42dddba9a6cdc8874a4c424e&server=12&id=8005&crc=1&uptime=74782
获取到请求地址:siberiarrmaskkapsulrttezya.ru
编码请求参数准备请求:
拼接完整的请求路径:
修改IE注册表,并且注入IE进程,通过IE进行循环请求。
程序内部还内置了其他备用请求地址:massidfberiatersksilkavayssstezya.ru
和上面一样,这里也是拼接请求地址尝试下载一个m.avi
massidfberiatersksilkavayssstezya.ru/images/JZ_2BlnEMJ5ubcGk0/MH0mpeGhwq6w/4AncNADtjcC/DZ_2F7_2BeMS6l/gX7ftVVQD7F1lnHc_2Fkf/HCoaIYuNpqXYqG_2/BrvG_2FsMd2uvqF/DYV_2BqCHpXosKYE8M/CwW3zlxw3/qoYC1j2ACF9SkfQtLUGX/MdBbbemHLb/m.avi
第三个域名:dolsggiberiaoserkmikluhasya.chimkent.su
通过ole调用IE请求域名:00392923