APT28 比较经典的一个样本,拿来练练手。本文参考了 [1]、[2] 两篇文章,主要是动手实践一遍,另外再加上一些自己的理解。(样本来源、样本基本信息)
1 初步静态分析
查壳,无壳。查看字符串,没有特别值得注意的字符串,有一些函数名。查看导入函数,导入了三个 DLL。
1.1 advapi32.dll
中间的三个函数是用来 enable 特权的。在这里顺便复习一下特权(privilege)的概念。特权是指一个账户执行某个与系统相关的操作的能力,比如关闭计算机对应的特权就是 SeShutdownPrivilege。特权是可以被允许(enable)和禁止(disable)的。账户登录后获得的令牌中含有一项特权列表,用来表示该账户拥有的特权,以及特权的允许、禁止情况。通过 whoami /priv
查看,管理员账户在 UAC 之前(被过滤的受限令牌)的特权为:
UAC 权限提升后的特权为:
有些特权是默认禁用的(disabled),这是因为一般在正常情况下不需要用到这种特权,出于安全的目的就将特权禁用了。要使用这种特权对应的能力,就要先将 disabled 的特权 enable。但是 enable 的时候,只能把 disabled 变成 enabled,不能无中生有。所以,如果使用 UAC 之前的管理员账户,想要 enable 管理审核和安全日志的特权(SeSecurityPrivilege)是不可以的,因为受限令牌根本没有该特权。想要使用管理审核和安全日志的特权,必须以管理员身份启动程序,手动 enable 该特权,再使用相应的功能。通过 enable 特权来将普通权限提升至管理员权限也是不行的,这是两个完全不同的概念!
接下来看看剩下的两个导入函数 GetSidSubAuthorityCount 和 GetSidSubAuthority,这两个 API 不太熟,看名字应该和 SID 有关。首先复习一下代表用户的 SID 的结构。一个 SID 的组成为 S-R-X-Y1-Y2…Yn-1-Yn:
- SID 开头的字母 “S” 表示该字符串为 SID
- R 表示 SID 结构的版本号(Revision),Windows 系统中都是 1
- X 表示标识符颁发机构(Identifier Authority)。Windows 特定的账户/组,如 Administrators,该值为 5,Everyone 等通用的账户/组,该值为 1
- 剩下的 Y 都是 Subauthority。其中 Y1 到 Yn-1 表示各子级颁发机构(Domain Identifier),标识各级不同的域。Yn 表示域内特定的账户和组,称为相对标识符(Relative Identifier),简称 RID
SID 结构体的组成如下图所示:(图片来源)
除了具体数值,结构体中还有一个 Subauthority Count 成员用来表示所有的 Subauthority 占几个 DWORD。
查下文档,GetSidSubAuthorityCount 返回 SID 结构体中指向 Subauthority Count 的指针。GetSidSubAuthority 返回指向特定 Subauthority 的指针,可以理解为是对 Subauthority 这个数组按索引取值,索引为该函数的第二个参数。至此这两个函数的功能就差不多弄清楚了,应该是用来读取 SID 中的某个值,估计就是 RID 了。
1.2 kernel32.dll
很多导入函数,其中关键的有:
- 进程遍历:CreateToolhelp32Snapshot、Process32FirstW、Process32NextW
- 文件遍历:FindFirstFileExA、FindNextFileA
- 获取环境变量:GetEnvironmentVariableW
- 反调试:IsDebugerPresent
- 文件操作:WriteFile、CreateFileW、DeleteFileW
1.3 shell32.dll
一个导入函数 ShellExecuteW,用于执行。
2 初步动态分析
用火绒剑和 fakenet 对样本的动态行为做一个初步的分析。
2.1 写文件
向 C:Users\test\AppData\Local\
目录下写了两个文件:cdnver.dll 和 cdnver.bat。其中 cdnver.bat 的内容是:start rundll32.exe "C:\Users\test\AppData\Local\cdnver.dll",#1
。
2.2 写注册表
向 HKEY_USERS\SID\Environment\
(相当于 HKEY_CURRENT_USER\Environment\
)下写
UserInitMprLogonScript,内容为 C:Users\test\AppData\Local\cdnver.bat
,应该是用于自启动。那么,有没有想过一个问题,为什么这个注册表项可以用于自启动呢?对于问题多问为什么,知其然更知其所以然,可以帮助我们加深对系统的理解,避免成为脚本小子。
经过一番调查,得知 UserInitMprLogonScript 是由 userinit.exe 读取并执行的,那就逆向 userinit.exe 看看,顺便推荐一个 pdb 文件下载器。搜字符串 UserInitMprLogonScript,找交叉引用,在 WinMain 里:
这里 v11 和 v12 就是 UserInitMprLogonScript 的值,即C:Users\test\AppData\Local\cdnver.bat
。然后稍微下面一点的地方:
不管是 if 分支还是 else 分支都执行了,所以确实是 userinit.exe 运行了 UserInitMprLogonScript 的值。
第二个问题,userinit.exe 运行 UserInitMprLogonScript 的值是在杀软运行之前吗?对于这个问题我是存疑的。再经过一番调查,得知 userinit.exe 的父进程是 winlogon.exe。当用户认证通过后,winlogon.exe 启动 userinit.exe 来运行 explorer.exe 和用户登录脚本。另一方面,对于杀软,以火绒为例:
它是基于服务运行的,最终起作用的是 HipsTray.exe。通过命令 Get-Process | select name, id, starttime | select-string <PID>
查看 explorer.exe 和 HipsTray.exe 的启动时间:
可以看到杀软确实是后运行的。
2.3 进程执行
运行释放出来的 cdnver.dll 的序号为 1 的导出函数:
2.4 网络
有两个突出的域名解析,其中 google.com 可能是用来测试网络连接的:
用 VT 查 cdnverify.com 则显示与 Fancy Bear(即 APT 28)相关:
另外 fakenet 可以观察到发送了大段 base64 编码后的数据,估计是向 C2 发送搜集的本机信息。
3 深入分析
一边用 IDA 看,一边用 x64dbg 调。因为导入了 IsDebugerPresent 这个函数,交叉引用找调用
IsDebugerPresent 的位置,没有,所以就放心调。
3.1 WinMain → sub_401DEF
入口点连续调用了三次 sub_401DEF:
点进去看的话,有一些循环异或,判断为解密函数:
调试步过可以得到三个被解密出来的字符串:
将 sub_401DEF 重命名为 _401DEF_decode。
3.2 WinMain → sub_4012D3
后面紧接着调用了 sub_4012D3,如果返回失败程序直接跳到了退出的地方,因此这个函数很重要。进去首先分配堆内存,失败就退出:
size 是四字节,所以应该是用来放指针的。然后 new 了 0x28 的内存,再调用两次 sub_401063 函数:
最后调用 sub_1000,然后结束:
分别看看这两个函数。
3.2.1 sub_4012D3 → sub_401063
点进去先分配堆空间,然后循环异或,估计也是解密函数:
应该是将密文解密到分配的堆空间里。调试步过可以得到两个解密出来的字符串 cdnver.dll 和
LOCALAPPDATA:
其中 0x43B300 就是一开始 new 出来的 0x28 大小的内存,估计是个什么结构体。将 sub_401063 重命名为 _401063_decode。
3.2.2 sub_4012D3 → sub_401000
循环异或,而且是两重循环,应该是比较大的数据了:
可以看出来是原地读写,运行到第一次读数据的位置,然后跳转到内存查看:
可以看出密文在可执行文件的数据段,IDA 里也可以看:
异或完成后:
像个 PE 文件了,但是怪怪的。前面有三个字节 0x0A、0xBA、0x00,而且后面也有一些莫名其妙的
0x00,后续再观察。将 sub_401000 重命名为 _401000_decode_PE。
到这里整个 sub_4012D3 就分析完了,它解密两个字符串到分配的堆空间中,并把字符串地址写到 new 出来的结构体里。然后原地解密了数据段的一些内容,疑似 PE。至于最开始在堆上分配的四字节,下写入断点运行可以发现 0x4013E1 处的指令将 new 出来的结构体的地址写入了该地址。将 sub_4012D3 重命名为 _4012D3_decode_str_PE。
但是分析到现在整个数据链条还是断的。回过头来看看分配四字节堆空间的地方:
返回值写入了 [edi+4],而 edi 又等于 ecx,即 this 指针。再回头看调用 0x4012D3 的地方:
调用前 new 了 0xC 大小的空间,将其内容初始化为 0。那么这个类应该就是贯穿整个样本最重要的类了。这个类的第一个成员变量(第五个字节开始,前四字节是虚函数表指针),就是 sub_4012D3 里在堆中分配的四字节。那么现在整个数据链条就连通了。
3.3 WinMain → sub_4013F7
执行完 sub_4012D3 后,控制流来到 sub_4013F7。开头又是两个解密函数。动调可以得到
RtlGetCompressionWorkSpaceSize 和 RtlDecompressBuffer 两个字符串。然后通过一次写一个字符的方式在栈上写 ntdll,然后 LoadLibrary:
之后再执行两次 GetProcAddress 来获取 RtlGetCompressionWorkSpaceSize 和
RtlDecompressBuffer 两个函数的地址:
所以此样本采取了一定的字符串隐藏和导入函数隐藏技术,来减少静态特征。然后调用
RtlGetCompressionWorkSpaceSize,再分配一些堆空间,再调用 RtlDecompressBuffer:
解压后就得到正常的 PE 了。然后通过 0x40152F 处的指令将 PE 的地址写入之前的那个 0x28 大小的结构体:
因为进行了多次调试,所以堆分配的地址的基址不一样,但是后两个字节是一样的。将 0x4013F7 重命名为 _4013F_decompress。
3.3 WinMain → sub_40155B
紧接着来到 sub_40155B 函数,该函数最开始调用了 sub_4010CD 函数。
3.3.1 sub_40155B → sub_4010CD
开头依旧是解密字符串,解密了 SystemRoot 和 \System32 两个字符串。然后在 0x401226 处取出之前的结构体中的第五个成员变量,即 LOCALAPPDATA:
分配一些空间用于存放路径,然后调用 GetEnvironmentVariableW:
调用两次拼接函数,从结构体中取第六个成员变量,即 cdnver.dll,拼接得到完整路径C:\Users\test\AppData\Local\cdnver.dll
。然后将结构体的第五个成员变量改为这个完整的路径:
将 sub_4010CD 重命名为 _4010CD_construct_PE_path。
回到 sub_40155B,Load kernel32.dll,GetProcAddress 获取 CreateFileW 和 WriteFileW 的地址,然后调用。创建文件 C:\Users\test\AppData\Local\cdnver.dll
,将结构体第一个成员变量指向的内容,即解压后得到的 PE 写入该文件。将 sub_40155B 重命名为 _40155B_write_dll。
3.4 WinMain → sub_40264C
首先连续解密了七个字符串:
然后采用老方法,Load Advapi32.dll,GetProcAddress 获取 RegOpenKeyExW,然后调用:
0x80000001 代表 HKEY_CURRENT_USER,所以此处打开的注册表项是HKEY_CURRENT_USER\Environment\
。然后再次获取环境变量 LOCALAPPDATA 的值:
对于解密出来的字符串 cdnver.dll,循环找子串 .
,最终得到后缀名,并将后缀名与 exe 进行比较:
因为后缀名是 dll,来到处理 dll 的分支,首先调用了 sub_402030 函数。
3.4.1 sub_40264C → sub_402030
解密字符串并将宽字节转换为 ASCII,对以下字符串进行了这种处理:rundll32.exe、#1、cdnver.dll、LOCALAPPDATA、start、cdnver.bat:
分配三个空间,获取环境变量 LOCALAPPDATA 的值,并拼接字符串将其写到分配的空间中:
然后 Load kernel32.dll,GetProcAddress 获取 CreateFileA 的地址,调用函数,创建文件C:\Users\test\AppData\Local\cdnver.bat
。接下来拼接字符串得到 cdnver.bat 的内容,写入分配的第三个空间中:
最后调用 WriteFile 将该字符串写入 cdnver.bat。将 sub_402030 重命名为 _402040_write_bat。
回到 sub_40264C,拼接字符串得到 cdnver.bat 的完整路径:
然后,Load Advapi32.dll,GetProcAddress 获取 RegSetKeyExW,调用函数,向HKEY_CURRENT_USER\Environment\
写 UserInitMprLogonScript 项,其值为 cdnver.bat 的路径:
将 sub_40264C 重命名为 _40264C_persistence。
3.5 WinMain → sub_401707
首先判断结构体第五的成员变量的值(C:\Users\test\AppData\Local\cdnver.dll
)是否以 .dll 结尾,对 exe 文件和 dll 文件采取不同的处理方式,看 dll 文件的处理分支即可。
解密三个字符串:
然后调用 sub_401D09,根据返回值的不同有两个分支。返回值为 3 执行 if 分支,否则执行 else 分支:
先看 else 分支,拼接字符串作为参数然后通过 ShellExecuteW 执行:
3.5.1 sub_401707 → sub_401D09
调用 GetCurrentProcess 获取当前进程句柄。调用 OpenProcessToken 获取令牌句柄。调用 GetTokenInformation 两次,参数为 TokenIntegrityLevel,用于获取令牌完整性级别。当参数为 TokenIntegrityLevel 时,返回 TOKEN_MANDATORY_LABEL 结构体。第一次调用获取 size,第二次调用获取实际值。
然后调用 GetSidSubAuthorityCount 获取 SubAuthorityCount 的值。再调用 GetSidSubAuthority 获取 RID(第二个参数设为 SubAuthorityCount - 1):
最后依据 RID 的不同返回不同的值。根据微软的文档,进程 RID 与完整性级别的关系如下:
RID 与返回值的关系:
- RID == 0x1000:返回 0
- RID < 0x2000:返回 0
- 0x2000 ≤ RID < 0x3000:返回 1
- 0x3000 ≤ RID < 0x4000:返回 2
- RID ≥ 0x4000:返回 3
似乎样本并没有考虑 PROTECTED_PROCESS 的情况。当运行的完整性级别为 SYSTEM 时,样本会执行 if 分支。
将 sub_1D09 重命名为 _401D09_get_integrity_level。
if 分支内,拼接字符串然后执行 sub_401B02:
3.5.2 sub_401707 → sub_401B02
开头首先调用了 sub_401957 函数。
3.5.2.1 sub_401B02 → sub_401957
Load Advapi32.dll,GetProcAddress 获取 OpenProcessToken 函数的地址,GetCurrentProcess 获取当前进程句柄。调用 OpenProcessToken 获取当前进程令牌句柄。GetProcAddress 获取 LookupPrivilegeValueW 函数的地址,调用两次,获取 SeSecurityPrivilege 和 SeTcbPrivilege 两个特权的 LUID,然后调用两次 AdjustTokenPrivileges 来 enable 这两个特权。其中 SeSecurityPrivilege 用于管理审核和安全日志,SeTcbPrivilege 用于以操作系统方式操作,允许很多敏感的操作。
将 sub_401957 重命名为 _401957_enable_privilege。
回到 sub_401B02,紧接着调用了 sub_401C3D。
3.5.2.1 sub_401B02 → sub_401C3D
通过 CreateToolhelp32Snapshot、Process32FirstW、Process32NextW 找到 explorer.exe,获取其 PID。然后通过 OpenProcess 和 OpenProccessToken 获取其令牌句柄,然后返回。将 sub_401C3D 重命名为 _401C3D_get_explorer_token。
回到 sub_401B02,紧接着调用了 sub_4018DC:
里面的逻辑很乱,决定直接步过看效果,将第三个参数指向的字符串写到了第一个参数指向的位置:
然后,Load Advapi32.dll,GetProcAddress 获取 CreateProcessAsUserW 函数的地址。调用,第一个参数 hToken 为之前获取的 explorer.exe 的令牌句柄,第三个参数 lpCommandLine 即 RunDll32.exe "C:\Users\test\AppData\Local\cdnver.dll",#1
总得来说,sub_401707 这个函数是用来执行的,将其重命名为 _401707_exec。但是对于在 SYSTEM 权限下的运行,还有以下两个疑问:
在 SYSTEM 权限下,LOCALAPPDATA 会被解析为
C:\WINDOWS\system32\config\systemprofile\AppData\Local
。但实际运行样本会发现样本不能向这个路径写文件(里面只有一个 Microsoft 文件夹),导致运行 CreateProcessAsUserW 报错:一开始对当前进程的令牌 enable 了 SeSecurityPrivilege 和 SeTcbPrivilege 两个特权,但是后续 CreateProcessAsUserW 使用的是 explorer.exe 的令牌,那么 enable 这两个特权的意义何在?另外,根据微软的文档,调用 CreateProcessAsUserW 需要 SeIncreaseQuotaPrivilege 特权,但 SYSTEM 用户这个特权是默认 disabled 的,为什么不 enable 这个特权呢?
3.6 WinMain → sub_4018AD
最后还有一个删除文件的函数,实际调试时没有调用:
将其重命名为 _4018AD_delete。
到这里整个样本就基本分析完了,剩下的 cdnver.dll 之后再分析,之前观察到的网络行为不是样本的,而是 cdnver.dll 的。
4 一些细节
4.1 类与结构体
4.1.1 贯穿样本的类与结构体
12 字节大小,第一个指针为虚函数表,第一个成员变量为指向另一个结构体的指针(下称该结构体为 A),第二个成员变量的值被赋值为 1,应该是起到一些控制效果的布尔值:
结构体 A 有 6 个重要的成员变量:
- 解压后的 PE 的指针
- 解压前的 PE 的指针
- 解压后的 PE 的 size
- 解压前的 PE 的 size
- 首先指向 LOCALAPPDATA 字符串,之后指向 cdnver.dll 的完整路径的字符串
- 指向 cdnver.dll 字符串
4.1.2 用于解密的结构体
实际上 _401DEF_decode 函数也用了一个结构体:
16 字节,两个数值,两个指针,应该是密文和 xor key,以及对应的 size:
- 密文
- 密文 size
- xor key
- xor key size
4.2 解密逻辑
4.2.1 _401DEF_decode
可以写解密脚本:
1 | cipher = [0x41, 0xA1, 0xD1, 0xFC, 0xC7, 0x20, 0x6D, 0xDA, 0x7C, 0x8B, 0xCF, 0xDD, 0x6F, 0x66, 0x24, 0x6C, 0xE4, 0xDD, 0x1E, 0xDD, 0x76, 0x5F, 0x1E, 0xFE, 0x8E, 0xA3, 0x98, 0x7F, 0x11, 0xA6, 0x0A, 0xFD, 0xA3, 0xB1, 0x67, 0x6E, 0x77, 0x3F, 0xD8, 0xE1, 0x12] |
这里还有个小坑,实际上 cipher 的 size 是 0x28 + 1
4.2.2 _401000_decode_PE
1 | cipher = [0x3A, 0x09, 0xA6, 0xDC, 0x8E, 0x7F, 0xCA, 0x5E, 0x18, 0x2B, 0x4E, 0xEB, 0xB8, 0xA7, 0x02, 0x8A] |
4.2.3 _401063_decode
1 | cipher = [0x00000053, 0x000001D7, 0x000001C8, 0x000001E7, 0x000001B1, 0x0000019D, 0x000001E4, 0x00000039, 0x00000074, 0x00000047, 0xFFFFFFFF] |