0%

​ 首先需要找到OEP,我们按照之前说的ESP定律来找,看到PUSHAD后按F7,各个寄存器的初始值被压入到堆栈中了,这里我们可以对这些初始值设置内存或者硬件访问断点,当解密例程读取这些初始值的时候就会断下来,断下来处基本上就在OEP附近了。

​ 在寄存器ESP处右键Follow in Dump,然后将第一个字节或前四个字节设置硬件访问断点,右键breakpoint-Hardware,on access-Dword,然后按F9

image-20211220140915458

​ 发现断在了POPAD指令的下一行,当壳的解密例程读取该值的时候断了下来,紧接着下面就是跳往OEP处

image-20211220141025695

​ 继续按F7单步,就到了OEP处,OD代码解析有误,将代码解析成了数据,右键Analysis-Remove analysis from module,删除掉OD的分析结果,这样就能正常解析了。

image-20211220141338534

解析还不够彻底,再次Analysis-analysis code

image-20211220142249513

​ 然后dump,保险起见我们使用imprec重构输入表

image-20211220142411426

​ 代码中直接有一个GetModuleHandle函数,右键Follow,我们可以看到直接来到了GetModuleHandleA的入口点处,并没有间接跳转(并不是所有的程序调用API函数都是通过间接跳转来实现的)。

​ 这里是通过一个间接CALL来调用API函数的。

​ 显然,4011F4是IAT其中的一项,该内存单元中保存了GetModuleHandleA的入口地址。

image-20211220144406702

image-20211220144622020

​ 如上图,Follow出来的跳转表和之间看到的有所出入,我们直接看M按钮

image-20211220150021566

接着我们在数据窗口中定位到IAT,第二个图是最后一个Dll的IAT项,地址是771xxxxx,现在我们知道了IAT的结束地址是401218,再来看看起始地址

image-20211220150352948

image-20211220150432901

​ 上图中标注起来的颜色前有一部分是10xx或者11xx的地址,明显不属于任何一个DLL,而且这些数值比当前进程空间中分配内存单元中的最小地址(M窗口中现实了最小地址为00010000)还要小。所以这些数值不属于任何一个DLL,也不属于任何一个区段,有可能是壳存放的一些垃圾数据,我们继续往下拉。

​ 我们可以看到一共调用了三个Dll,其余还有一些dll估计是被壳调用的,

image-20211220153539586

​ 我们看一下ntdll的调用,看到这一项的地址为401200,和Kernel32.dll的IAT项混在了一起。我们看看这个Dump窗口中的地址可以看到,kernel32.dll的地址是最靠前的,ntdll是最靠后的,往前拉可以看到,我们的IAT起始地址确实是40119c,与上图一致。

image-20211220153736685

image-20211220154448259

好了,现在我们有了以下三条数据:

OEP = 4000 (RVA)

IAT的起始地址 = 119C (RVA)

IAT的大小 = 401218 - 40119C = 7C。

image-20211220154632198

中间的是垃圾数据,不知道为什么和之前一样第一个userdll也显示No,但是点开+号里面的数据又是对的。

image-20211220154903219

​ 可以看到这里ntdll.dll中分配内存空间的两个函数用kernel32.dll中两个类似的函数HeapAlloc,HeapFree替换掉了。这里日志信息中也提示说这两个函数跟ntdll.dll中的RtlAllocateHeap,RtlFreeHeap完成的功能类似,壳也可以对这些IAT项进行修改和混淆。

image-20211220155159007

​ 现在我们需要剔除掉垃圾数据,即valid显示为NO的项(无效数据),我们单击左边的+号将其展开。

img

单击Show Invalid,显示无效的数据,然后单击鼠标右键选择-Cut thunk(s)(剪切掉)。

img

image-20211220155402523

加壳脱壳对比

我们先看没有加壳的pe文件,PE文件入口是401000

image-20211214162015301

加壳后入口地址如下,入口地址变成了409c80

image-20211214170024538

查看401000位置,可见代码段是空的,即加壳程序将原程序代码段加密/压缩后保存到其他地方,并且将原程序代码段清空了。

image-20211214170853897

继续往下看,可以看到跳转到OEP的指令,设置一个断点运行起来。

image-20211214171034553

运行后如下,可看到原程序。加壳程序执行的整个流程如下

1.执行解压/解密例程

2.解压/解密原程序的各个区段的数据

3.跳往OEP处

4.执行原程序代码

image-20211214173928228

原代码段是空的,在合适的时间,解压/解密程序会将解压/解密后的代码段数据重新写到这里,所以我们可以对这里设置内存访问断点,当解压/解密例程向这里写入数据的时候就会断下来。(点击M->如下图下断点)

img

断下来以后看到这里401000开始处内存单元第一个字节将被赋值为AL的值,此时AL值为6A,我们再来看看原程序401000处的第一个字节,可以看到也是6A

image-20211214195748271

image-20211214200245457

​ 如果我们逐字节的跟踪,可以看到解密/解压例程将原程序代码恢复的整个过程。但是并不是所有的壳都是按照这个顺序来解密/解压原程序区段的,这里是最常规的情况。

​ 如果大家仔细跟踪这个流程的话,就会发现这是一个循环,逐字节读取加密过的字节,接着进行数学运算(例如说:加法,乘法,等等)来解密,运算完毕得到原始字节值,然后将其恢复到原处。

​ 接下来,我们清除掉前面设置的内存访问断点(右键breakpoint->remove memory breakpoint),单击菜单项中Animate into(自动步入ctrl+f7)选项,将能看到原程序代码段被逐字节还原的动画过程,原程序各个区段被还原后,就会断在我们前面设置的jmp OEP处的断点处。

脱壳

ESP定律

​ 又称堆栈平衡定律**(如果要返回父程序,则当我们在堆栈中进行堆栈的操作的时候,一定要保证在RET这条指令之前,ESP指向的是我们压入栈中的地址),是应用频率最高的脱壳方法之一。ESP(Extended Stack Pointer)为扩展栈指针寄存器,是指针寄存器的一种,用于存放函数栈顶指针。与之对应的是EBP(Extended Base Pointer),扩展基址指针寄存器,用于存放函数栈底指针**。ESP为栈指针,用于指向栈的栈顶(下一个压入栈的活动记录的顶部),而EBP为帧指针,指向当前活动记录的底部。

​ 加壳后的程序入口点最开始是pushad,就是把所有寄存器压入栈,在壳的最后有一个popad将所有寄存器出栈,对ESP的0019FF54下硬件访问断点。也就是说当程序要访问这些堆栈,从而恢复原来寄存器的值,准备跳向苦苦寻觅的OEP的时候,OD帮助我们中断下来。

总结:我们可以把壳假设为一个子程序,当壳把代码解压前和解压后,他必须要做的是遵循堆栈平衡的原理,让ESP执行到OEP的时候,使ESP=0019FF54。载入程序后只有esp寄存器内容发生变化,那么这个程序多半可以用ESP定律。

​ 首先f7执行pushad,然后在ESP处选择Follow in dump,就可以在数据窗口中定位到刚刚PUSHAD指令保存到堆栈中的寄存器环境了,我们选中前4个字节,我们通过单击鼠标右键选择Breakpoint-Hardware,on access-Dword给这4个字节设置硬件访问断点。

image-20211214204923883

​ 然后点击f9我们可以看到,有一个判断。如果不为0 那么就跳向 00409DFA,我们在这一行的下面一行按F4,直接跳过这个判断,然后继续按F8,就到了程序入口,或者直接在最后的jmp下断点,然后f7到OEP。

image-20211214171034553

dump

​ 然后使用工具PETools将程序dump出来,双击程序会显示不是一个有效的win32程序,这时候我们就需要修复IAT表了。

​ 下图中403238是IAT的其中一个元素,里面保存的是GetModuleHandleA这个API函数的入口地址,我们dump出来的程序的IAT部分跟这里是一样的,属于同一个动态库的API函数地址都是连续存放的,不同的动态库函数地址列表是用零隔开的。

image-20211219110628313

​ 上图中有三个77开头的地址,我们点击窗口M按钮,看看这个三个地址属于什么dll

image-20211219111016142

​ 要修复IAT,我们需要知道的是开始地址和结束地址,IAT起始地址是403184,有些强壳可能会将IAT前后都填充上垃圾数据,让我们定位IAT的起始位置和结束位置更加困难。但是我们知道IAT中的数值都是属于某个动态库代码段范围内的,如果我们发现某数值不属于任何一个动态库的代码段的话,就说明该数值是垃圾数据。

image-20211219111712077

我们看到最后一个元素地址是75xxxxxx,属于COMDLG32.DLL,所以40328c是IAT结束地址,

begin:403184

end:40328c

image-20211219112120480

1)IAT的起始地址,这里是403184,减去映像基址400000就得到了3184(RVA:相对虚拟地址)。

2)IAT的大小

IAT的大小 = 40328C - 403184 = 108(十六进制)

3)OEP = 401000(虚拟地址)- 映像基址400000 = 1000(OEP的RVA)。

然后点击Get Imports,不知道为啥userdll会显示无效,先点击fix dump,然后选择dumped.exe

image-20211219113450399

依然显示无效32文件,使用PEtools rebuildPE,就可以运行起来了。

最后一次异常法

​ 新开一个例子,在debug options-exception,各个选项全部选择然后运行起来

image-20211216184247952

img

​ 这里我们可以看到产生了好几处异常,但是都不是位于第一个区段,说明这些异常不是在原程序运行期间发生的,是在壳的解密例程执行期间产生的异常,最后一次是46e88f处的这个异常。

​ 好,现在我们重新启动OD,将EXCEPTIONS菜单项中忽略的异常选项的对勾都去掉,仅保留Ignore memory access violations in KERNEL32这个选项的对勾。

img

​ 这里不是,我们按SHIFT + F9忽略异常继续运行,我们知道最后一次异常是46E88F处的INT 3指令引发的。

img

​ 这里是壳的解密例程执行过程中产生的最后一次异常,接着就是执行原程序的代码了。

​ 接着我们可以对代码段设置内存访问断点,为什么不在一开始设置内存访问断点呢?原因是很多壳会检测程序在开始时是否自身被设置内存访问断点,如果执行到了最后一次异常处的话,很可能已经绕过了壳的检测时机,我们来试一试。

img

​ 我们按SHIFT + F9忽略该异常运行起来。

img

​ 我们可以看到断在了OEP处,下面我们来看看该壳在开始的时候是否有检测内存访问断点。

​ 我们重新加载该程序,将忽略的异常选项都勾选上,接着打开区段列表窗口,给第一个区段设置内存访问断点,过了很久断在了OEP处。

img

​ 虽然这里我们直接给第一个区段设置内存访问断点直接定位到了OEP。

利用壳最常用的API函数来定位OEP

 将忽略的异常选项都勾选上,我们来定位一下壳最常用的API函数,比如GetProcAddress,LoadLibrary。ExitThread有些壳会用。我们首先来看看GetProcAddress。

img

​ 我们可以看到该壳使用了GetProcAddress,接着使用bp GetProcAddress命令给该API函数设置一个断点。

img

​ 如果在命令栏中使用bp命令设置断点失败的话,可以尝试手工设置断点,运行起来。

img

​ 这里我们并不需要其断下来,我们只需要知道壳在哪些地方调用GetProcAddress,所以我们在断下来的这一行上面单击鼠标右键选择-Breakpoint-Conditional log,来设置条件记录。

img

​ 这里我们将Pause program这一项勾选上Never,记录的表达式设置为[ESP],也就是记录返回地址,这样我们就能知道哪些地方调用GetProcAddress。接着在日志窗口中单击鼠标右键选择-Clear Log(清空日志)。

img

​ 运行起来,我们可以看到程序的主窗口弹了出来,打开日志窗口,看看最后一次GetProcAddress(排除掉第一个区段中调用的位置)是在哪里被调用的。

img

​ 我们可以看到基本上GetProcAddress都是解密例程中调用的,除了428C2B这一处以外(这里是第一个区段中调用的,也就是原程序本身调用的)。所以我们要定位的应该是47009A这一处。接下来我们重新来编辑一下条件断点中断的条件,将中断条件设置为[ESP] == 47009A。

​ 并且将Pause program这一项勾选上On condition,重新启动OllyDbg。

img

​ 编辑条件断点,设置Condition为[ESP] == 47009A,接着将Pause program这一项勾选上On condition,运行起来。

img

img

​ 断了下来。我们可以在对代码段设置内存访问断点之前尝试一下这种方法,这样就可以绕过很多壳对内存断点的检测,但是有一些壳也会对API函数断点进行检测,所以说我们需要各种方式都尝试一下,找到最合适的。

​ 对当前这个壳定位GetProcAddress的调用处是可行的,我们现在已经在OEP附近了。如果定位GetProcAddress的调用处失败的话,我们可以换其他的API函数,这里我们再来看看日志窗口,可以看到一处线程结束记录。

img

​ 因此接下来给ExitThread设置断点,并且将菜单项Debugging options-Events中的Break on thread end(在线程结束位置中断下来)勾选上。

img

​ 运行起来。

img

​ 断在了线程结束的位置。

img

​ 接着我们给代码段设置内存访问断点就能够马上定位到OEP。

利用应用程序调用的第一个API函数来定位OEP

​ 这种方法就是直接给应用程序调用的第一个API函数设置断点,比如说,很多程序(VC++)一开始会调用GetVersion,GetModuleHandleA,对于bitarts_evaluations.c来说我们可以断GetVersion,对于CRACKME UPX来说我们可以断GetModuleHandleA。

img

运行起来。

img

这里我们断在了GetVersion的入口点处,从堆栈中我们可以看到返回地址位于第一个区段。我们直接在返回地址上面单击鼠标右键选择-Follow in Disassembler。

img

​ 这里我们又定位到了OEP。以上就给大家演示的如何利用应用程序调用的第一个API函数来定位OEP了。如果我们遇到有的壳检测GetVersion入口处的INT 3断点的话,我们可以尝试在该API函数的返回指令RET处下断。

​ 其实还有很多适用于特定壳定位OEP的方法,基本上也是根据上面的这些基本方法变通来的。

​ 如果壳检测INT 3断点或者硬件断点的话,使用ESP定律给堆栈中的寄存器初始值设置硬件断点也是不起作用的,只能换其他方法。

Yara是一种模式匹配(模式匹配是数据结构中字符串的一种基本运算,给定一个子串,要求在某个字符串中找出与该子串相同的所有子串)技术,它可以使用沙箱内存转储来识别恶意软件家族。然而,由于微小的代码变化,模式匹配技术会悄然失败,从而导致未知的恶意软件样本。本文提出了一种利用增量聚类(MVIIC)过程的两层恶意软件变异识别方法,并提出了未知恶意软件样本的聚类方法,以识别恶意软件变异和新的恶意软件家族。该新的增量聚类算法被用于从未知的恶意软件样本中识别新的恶意软件变体。本研究表明,聚类可以提供比Yara规则更高层次的性能,并且聚类可以抵抗恶意软件变体引入的小变化。本文提出了一种混合的方法,利用Yara扫描来消除已知的恶意软件,然后进行聚类,协同作用,以允许识别新的恶意软件变体。

Yara是一种模式匹配技术,它可以使用沙箱内存转储来识别恶意软件家族。Yara规则包含正则表达式和字符串。

方法分为两层,在第一层中,先前开发的Yara规则用于拒绝已知恶意软件家族的样本。在第二层采用了一种新的增量聚类算法。与传统的对启动聚类中心的选择很敏感的聚类算法不同,增量聚类能够找到或近似最佳的聚类分布,并检测到小规模的聚类。

从每个集群中随机选择的恶意软件样本中包含的新的恶意软件家族(6)和恶意软件变体(6)被识别出来,并使用集群恶意软件样本的内存转储(7)创建新的Yara规则(8)。

img

提供了一种新的增量聚类算法的一个用例;

提供从实时获取的恶意软件样本的动态分析日志的特征的特征工程;

开发了一个两层的动态分析系统,该系统使用Yara规则来拒绝已知的恶意软件,并使用一种新的聚类算法和特征工程来对未知样本进行聚类,以识别新的恶意软件家族、变体和Yara规则中的缺陷。

由于现有恶意软件家族的软件开发和新的恶意软件家族的创建,恶意软件源中的样本会受到持续的概念漂移的影响。传统的聚类算法被设计为使用静态地面真实值。为了处理概念漂移,传统的聚类算法要求对整个数据集进行定期的重新聚类,以合并更新后的地面真相。传统聚类算法的执行时间随数据集大小的增加成比例而增加。T

这篇文章详细分析了APT29使用的Miniduke,Minicuke是纯汇编编写的一个后门 ,该恶意软件采用了控制流整平迷惑并且实现了多种数据泄露的方法,比如用POST和PUT HTTP方法发送数据给C2服务器,或者在无网络连接的情况下用一个命名管道。后门实现37种不同的功能,如下图。

img

技术分析

首先用IDA打开文件,看到第一个函数setunhandledexceptionfilter,恶意软件将异常过滤器函数设置为特定函数。

img

前面的函数有点看不出来是个啥,我们注意看最后一个函数,可以看到用了GetStartupInfo获取STARTUPINFO结构的内容。Sub_4030A4如下图,可见是创建了一个线程。我们点进StartAddress看看

img

img

StartAddress函数前部分非常多的Switch case函数,即使是后半段的这些函数也有非常多的switch case结构,非常多的无用的计算,并且加载了三个dll。

img

在IDA的图中可以看到startAddress如下,这就是前面说的控制流整平诱惑。StartAddress中的loc_4036F2中就实现了30个case,我们将该跳转用nop替换掉,因为该命令占6字节,nop一个字节,所以要用6个nop。

img

img

img

img

SetErrorCode函数参数为2,作用是让系统不显示Windows错误报告对话框 (0x2 = SEM_NOGPFAULTERRORBOX)。加载的三个dll后的sub_407B60中有一个函数sub_417DCC,将在执行期间运行,并使用哈希机制定位。基本上,对于DLL中的每个函数名,恶意软件计算一个4字节的值,并与硬编码的值进行比较。这些函数都是目标:GetProcAddress, GetLongPathNameA, GetLastError, CreateProcessWithLogonW, CryptAcquireContextW, CryptGenRandom, InternetOpenA, InternetConnectA, InternetSetOptionA, HttpOpenRequestA, (httpsendRequestA, HttpQueryInfoA, InternetReadFile, InternetCloseHandle, HttpAddRequestHeadersA。

img

img

在该函数中有一个sub_423FC8函数,红框圈起来的函数就是hashing函数

img

img

现在回到加载dll的目标函数中,下图中的API用于连接CSP,获得指定CSP(真正实行加密的独立模块)的密钥容器的句柄(0x1 = PROV_RSA_FULL, 0xF0000040 = CRYPT_VERIFYCONTEXT | CRYPT_SILENT****)

img

img

下图函数使用一个子权限分配和初始化安全标识符(SID)

img

通过将新的访问控制或审核控制信息合并到现有的ACL结构中来创建新的访问控制列表(ACL)

img

通过恶意进程初始化新的安全描述符

(0x1 = SECURITY_DESCRIPTOR_REVISION),并使用setseCurityDescriptordACL API设置DACL(自定义访问控制列表)中的信息

img

进程创建了一个Software\Microsoft\ApplicationManager注册键值

img

一个新的值AppId被创建,使用GetTickCount函数调用的输出计算此值,它检索自系统启动以来经过的毫秒数

img

img

img

继续查看下一个函数sub_41CD30,

从上面的一个输出被转换并写入缓冲区,以及“AppID”值。将使用传统算法加密此缓冲区,该算法还包括XOR运算符(sub_420200):

img

img

img

控制流整平

if-else、while、for具有典型的跳转等结构,即使通过多层嵌套、拓展条件等方法,依然可以通过“切片技术”来判断。有了这些依据,就给程序分析带来很多便利。
正是这个原因,为了增加程序逆向的难度,我们得使得这些特征结构变得模糊,并且能让类似“切片技术”这样基于具体语义分析的方法失效,迫使逆向分析人员进行完整的抽象语义分析,斩断所谓的“捷径”。
控制流整平的策略是这样的,它把所有的典型控制流以及其衍生结构“统而为一”,各种控制流的区别只是语义方面的,增加了理解控制流转换关系的难度。

控制流整平迷惑,是通过打破程序原有的控制流之间的嵌套和顺序关系,使得变换后的程序控制流扁平化的混淆方法,其基本思想是令程序中所有的基本块拥有共同的前驱和后继代码块。如下图

img

进行控制流整平后,使得面向过程的代码片段,原来比较清晰的控制流向混杂在一起,同时这也比较好的并行图形态,也有利于进一步的迷惑处理。

EasyRE_REvenge

这道题和前面那道easyre结构一样如下,函数中有花指令

image-20211115101010954

image-20211115110634542

我们edit-patch-changebyte(不是assemble)。0B前面那个00也要改成90

image-20211115112905196

可以看到还要修改一个地方,全部nop

image-20211115113230271

image-20211115113438466

发现还是不行,往下一直看会发现全是一样的代码(花指令),写个py脚本全部nop掉(IDA7.0自带IDAPython,但是需要安装python27),File-Scripts Files就ok

image-20211115202819745

反汇编后如图,我们自己写个代码

image-20211115203400188

文章将主流的基于学习的算法按照其出入格式分为了三类,分别是基于图像的、基于二进制的和基于反汇编的。文章基于现在很多放大都没有一个一致的比较,特别是从实际应用场景来看以及在概念漂移(指的是数据流中的潜在数据分布随时间发生不可预测的变化,使原有的分类器分类不准确或决策系统无法正确决策)的场景中也没有进行比较。文章的工作主要是在4个不同的数据集和一致的实验设置上对基于学习的PE恶意软件分类方法进行了全面的实证研究。根据实验结果发现(1)没有哪类方法明显优于其他方法;(2)所有类型的方法在概念漂移上都表现出性能下降(平均f1分32.23%);(3)的预测时间和高内存消耗阻碍了现有方法被用于行业使用。

相关工作

基于图像的分类方法,在2011年首次被提出,最初采用的是机器学习的方法,但是由于提取复杂纹理特征的开销很高,这些方法通常效率低下。随着深度学习模型在图像分类任务中的不断发展,它逐渐地被运用到恶意软件家族分类场景,表现出了比机器学习方法更好的性能。当然基于图像的方法也有缺点,例如将恶意软件转换为图像会引入新的超参数(例如,图像宽度),并施加不同行中像素之间不存在的空间相关性,这可能是错误的。

基于二进制的方法,以恶意软件的二进制代码为输入,通常采用现有的序列模型,特别是来自自然语言处理(NLP)领域进行分类。最初一般是n-gram算法提取特征然就用算法进行分类检测,但是随着n的增大,计算开销会非常大;随后提出了端到端的浅层CNN模型,但是接收整个二进制文件作为输入需要非常大的内存。为了解决这个问题,提出了只关注PE头的方法。

基于二进制文件的方法不需要领域知识,而是考虑恶意软件二进制文件中的上下文信息。然而,与其他类别的方法相比,将恶意软件样本表示为字节序列可能会带来一些挑战。首先,通过将每个字节作为一个字节序列中的一个单元,恶意软件字节序列的大小可能达到数百万个时间步长,这是相当消耗资源的。其次,相邻的字节在空间上是相关的,但是由于跳转和函数调用,这样的情况可能并不总是保持,因此二进制文件中的信息可能存在不连续性。

基于反汇编的方法,首先将二进制文件分解成汇编代码,并根据函数调用图(FCG)和控制流程图(CFG)等特征执行恶意软件分类,这些特征是从汇编代码中提取的。提出计算图之间的相似性,然后用其作为恶意代码聚类的距离度量。计算图之间的相似性属于计算密集型,会带来很大的性能开销。

也有一些方法从程序代码中提取操作码序列,与其他方法相比,基于反汇编的技术可以更好地捕获代码结构特征,但它们通常需要领域知识,如汇编语言及其相应的分析方法。

实证研究

首先论文提出了三个研究问题,分别是:

  • 1.不同的PE恶意软件族分类方法在不同的数据集上如何执行?

指出目前缺乏系统的研究来评估在多个数据集的同一实验设置上一致的不同方法的性能。

  • 2.恶意软件概念漂移如何影响各种模型的分类性能?

指出概念漂移是PE恶意软件建组分类任务中一个关键的现实问题,评估概念漂移应用场景中不同方法的性能非常重要。

  • 3.是什么因素阻碍了当前基于学习的PE恶意软件分类方法在行业中的部署和相应的改进方向?

由于之前的研究问题所发现的差距,论文最终目标是为如何使当前基于学习的PE恶意软件分类方法适用于真实的行业场景提供建议。

基于图像的测试模型选了VGG-16、ResNet-50、Inception-V3、IMCFN.

VGG-16通过减少卷积层的参数来提高训练时间,它包含了13个卷积层、5个最大池化层、三个全连接层。所有隐藏层都是用ReLU,输出层使用softmax,优点是结构简单,参数多,拟合能力高。

ResNet的提出是为了解决传统CNN网络在深层信息传输中普遍存在的信息丢失、梯度消失和梯度爆等问题。通过在一个称为跳过连接的结构中将输入直接带到输出中来保护信息的完整性。由于该设计,ResNet-50可以训练一个包含50个隐藏层的更深的网络。ResNet-50使用全局平均池,然后使用最终预测层的softmax。相比VGG-16更有效,因为它的参数要少得多。

InceptionNet目的是找出如何用密集成分来近似最优的局部稀疏结。与ResNet-50一样,Inception-V3最终也使用了一个全局平均池,然后使用softmax作为最终的预测层。与VGG-16相比,它更有效,其参数数量甚至比ResNet-50更少。

IMCFN是使用微调卷积神经网络的基于图像的恶意软件分类,它是针对恶意软件族分类的任务而定制的。它是VGG-16的变种,将前两个FCs中的神经元从4096减少到2048,并增加了一个dropout层来减少过拟合的影响。

基于二进制的测试模型选了Word2Vec+MLP、MalConv.

Word2Vec+MLP的关键思想是,来自同一族的样本中的字节关系相似,且与不同族的样本明显不同。因此,原始字节的向量矩阵是恶意软件分类的有效特征。首先对原始二进制文件进行预处理,删除5个或更多连续的0x00或0xCC(无意义的字节)。然后将每个文件作为一个语料库,看作由从0x00到0xFF的256个单词组成的。使用Word2Vec中的连续字袋模型(CBOW)获得文件中256字节的嵌入向量,每个文件用一个字节向量升序矩阵表示。MLP将这些矩阵作为输入,并输出相应的族类别。MLP由3个FCs组成,其中由预测类的数量决定,对于前两个FCs,添加一个dropout层。MLP是最直观、最简单的深度神经网络。与VGG-16类似,虽然结构相对简单,但参数较多,可以很好地拟合训练数据。

MalConv是第一个允许将整个恶意软件作为输入的端到端恶意软件检测模型。首先使用嵌入层将原始字节映射到一个固定的8维向量。通过这种方式,它可以通过同时考虑本地和全局上下文来捕获原始二进制文件中的高级位置不变性。然后,使用一个浅层CNN,大滤波器宽度为500字节,结合500步幅。这使得模型能够使用PyTorch以数据并行的方式更好地平衡计算工作负载,从而可以缓解第一个卷积层的GPU内存消耗问题。

作为一个浅层的CNN架构,MalConv克服了一个主要的实际限制,即读取整个恶意软件字节是消耗内存的,它还捕获了原始二进制文件中的全局位置不变性。它允许将嵌入层与卷积层联合训练,以便更好地提取特征。

基于反汇编测试模型选了MAGIC、Word2Vec+KNN、MCSC.

MAGIC是一种端到端的恶意软件检测方法,通过使用深度图卷积神经网络(DGCNN)的属性控制流图(ACFG)。首先将ACFG(将CFG的顶点抽象为离散的数值向量)转换,从恶意软件反汇编文件中提取为一个数值向量。然后,DGCNN将这些不同大小的无序ACFG转换为固定大小和顺序的张量,用于恶意软件家族分类。

Word2Vec+KNN将反汇编文件建模为恶意软件语言,将其操作码序列提取为恶意软件文档,并使用Word2Vec模型生成此类文档的计算表示。这篇论文选择WMD作为KNN分类中文档之间语义紧密度的度量,计算将文档A的所有嵌入单词传输到文档B的所有嵌入单词的成本。

MCSC首先从拆反汇编件中提取操作码序列,并根据SimHash将其编码为相同长度的序列。然后,它将每个SimHash值作为一个二进制像素,并将SimHash位转换为灰度图像。它训练了一个由LeNet-5修改后的CNN结构来对这些灰度图像进行分类。在训练CNN分类器时,采用多哈列和双线性插值来提高模型的精度,并采用主要的块选择来减少图像生成时间。

实验采用了四个数据集,分别是BIG-15,Malimg,MalwareBazaar和MalwareDrift.(前两个开源)

发现

论文通过实验发现

  • 1.在所有方法中,基于二进制的模型CBOW+MLP在不同的数据集上表现最好,而没有单个类别的性能显著优于其他类别。

  • 2.迁移学习有可能进一步提高基于图像的PE恶意软件分类方法的有效性,并打开模型内部特征提取层进行微调,以获得更好的迁移学习性能。

  • 3.所有现有的方法在面对现实工业场景的概念漂移时都存在性能较差,因此在评估PE恶意软件族分类方法时应认真考虑。

并且实验结果表明,不同方法的稳定性也差异很大。CBOW+MLP方法虽然在普通机器学习场景中表现最好,但在概念漂移场景中下降幅度最大,这主要是由于其结构简单。相反,MCSC和Word2Vec+KNN的下降率最小。原因是它们都从拆卸文件中提取视码序列,并专注于在恶意软件进化过程中往往被保留的视码序列的本地上下文连接,因此这些方法在概念漂移上表现出稳定的性能。一个有趣的发现是,尽管Word2Vec+KNN也有简单的结构,但在概念漂移下,它的表现比CBOW+MLP更好,这归功于KNN算法,该算法计算未来的样本与所有其他样本之间的相似性距离。因此,它通常需要更长的预测时间。

  • 4.CBOW+MLP对概念漂移的性能最差,而MCSC对概念漂移的性能最稳健。结果表明,操作码序列保留了PE恶意软件的家族特征,是概念漂移下分类的有效和鲁棒特征。

论文作者向实际应用场景种的合作伙伴提出问题:公司目前采用了哪种分类方法以及原因;在实际适用的场景中,影响合适方法选择的因素是什么?概念漂移在实际应用场景中很常见吗?处理概念漂移的当前状态如何?

目前,在行业应用场景中采用了两种主流方法,即沙箱和基于模式的方法。基于模式的方法是基于模式/特征数据库的静态检测方法。它在时间和资源消耗方面是有效的,但对噪声、混淆和概念漂移是脆弱的。

PE恶意软件分类方法的行业使用主要受到三个因素的限制,即预测精度和召回率、预测时间和资源消耗,主要资源关注的是运行时内存和CPU使用情况。

作为一个具体的例子,在他们的一个包含基于学习的恶意软件分类模型的产品中,他们要求运行时内存低于1gb,这是我们所有研究的方法都无法满足的。另一个要求是能够在0.1秒内预测一个恶意软件,准确率超过93%,这过滤掉了大多数基于二进制和基于反汇编的方法。

概念漂移通常是由于恶意软件的进化。例如,在现有的恶意软件想要逃避检测的情况下,可能会有新的非内核功能,如通信和消息传递技术正在改变。这种情况经常发生,并对恶意软件家族分类提出了挑战。目前缺乏特定的机制来处理这种情况,目前的实践通常使用沙箱方法来处理这种场景。另一个观察结果是,除了概念漂移之外,它们还需要应对快速发展的新恶意软件家族和特性的挑战。除了现有学术数据集定义的细粒度家族分类外,行业合作伙伴更感兴趣的是基于恶意软件家族的恶意行为的检测,即特洛伊、Rootkit或勒索软件。然而,目前还缺乏关于这一方向的研究,很可能是因为没有这样的可用的数据集。

  • 5.(1)如何处理快速发展的恶意软件家族,而不是只使用一个或几个数据集进行评估;(2)一个预测精度更高、重量更轻的模型(3)从恶意行为的角度进行恶意软件家族分类。

这篇文章主要分析一个dll,该恶意软件与APT28有关。

先拖进IDA,f5反汇编后如下图,第一次见这么简单的dll

img

我们查看导出有DllEntryPoint和RunMod,因为我们的IDA静态无法分析dll,所以用rundll32.exe通过调用RunMod函数启动dll

img

img

因为这样的动态调试还不会,所以先搁置一下,先用静态查看一下RunMod函数做了什么(Update already)

IDA静态调试

IDA查看该函数发现,主要功能就是创建两个线程函数

img

先看线程函数下方的sub_10001CF0()函数,这个函数主要的函数是GetMessage,观察函数上下文应该是从线程队列中检索消息,如果线程消息等于22(WM _ ENDSESSION 消息通知应用程序会话是否正在结束),会中断循环。

img

Thread1 StartAddress

先看第一个thread,一开始的sub_100010C0函数有两条命令行指令systeminfo和tasklist,进去详细查看发现先createpipe创建了个管道(匿名),启动cmd执行命令。systeminfo输出用ReadFile函数读出,tasklist同样

img

img

紧接着查看下面的函数sub_100013B0,初步猜测影噶是检索各个文件目录作为目标对象,结合OD看一下是哪些目录

img

img

目录获取完成后,我们继续看下面的函数sub_100016C0就是一些关于堆空间的的分配,猜测是为下面的函数申请空间

img

从Sub_10002900进去看到的函数分析这部分应该是网络连接操作

img

InternetOpenW API 初始化 WinINet 函数的使用(用户代理被硬编码为“Opera”),使用 InternetSetOptionW 例程(0x6 = INTERNET_OPTION_CONTROL_RECEIVE_TIMEOUT and 0x5 = INTERNET_OPTION_CONTROL_SEND_TIMEOUT)将发送和接收超时设置为 600 秒,InternetConnection用443端口与服务器updaterweb.com建立连接

img

Base64加密完数据后就要开始发送了,主要关注下面这个函数,(httpOpenRequestW 例程用于创建 (http POST 请求句柄,恶意软件在 (http 请求句柄中添加了一个 (http 请求标头(“application/x-www-form-urlencoded”),使用 (httpSendRequestExW API 将请求发送到 (http 服务器,如下图所示,看到网上的分析443端口连接失败的情况下,会尝试80端口,但是在动静态分析的时候并没有看到。

img

img

最后线程会将之前创建的事件置为信号状态

Thread2 sub_6BD71960

线程一开始就将事件之谓无信号状态img

有一个类似的工作流程,从调用 InternetOpenW 函数开始,直到连接到端口 443(或端口 80,如果第一个不成功)上的 C2 服务器。这次的 POST 请求不同,因为它包含用于下载 DLL 文件的“cmd=y”命令:

img

恶意软件使用 InternetQueryDataAvailable 例程查询服务器以确定可用数据量,函数sub_10001FD0中有InternetReadFile函数读取dll文件,下前 4 个字节表示数据大小,还有 32 个字节表示内容的 SHA256 哈希值,图中还有一个CryptStringToBinaryA 函数,对base64编码后的文件进行解码(下图是网上分析)

img

img

CryptAcquireContextA 用于获取 Microsoft RSA 和 AES 加密提供程序 (0x18 = PROV_RSA_AES) 的句柄,CryptCreateHash 例程用于创建 CSP(加密服务提供程序)哈希对象 (0x800c = CALG_SHA_256) 的句柄,在 base64 编码的 DLL 文件被解码后,恶意软件会使用 SHA256 算法哈希应该包含 DLL 文件的缓冲区(CryptHashData),通过调用CryptGetHashParam API提取哈希值,(0x2 = HP_HASHVAL),恶意进程验证上面计算出的哈希值是否与 DLL 文件附带的 32 字节缓冲区重合img

恶意进程会在%TEMP%目录下创建一个名为fvjoik.dll的文件,新创建的文件填充从 C2 服务器下载的潜在 DLL,使用 LoadLibraryW 例程将 DLL 文件加载到当前进程的地址空间中,恶意软件将使用序号 1 执行导出的函数

img

函数完成后,sub_10001000()函数中可以看到采用WinExec函数命令行执行cmd删除dll文件

img

最后还与C2服务器进行了通信,应该是传输了DLL执行的结果

img

OD动态调试

具体步骤参考博客:(https://www.cnblogs.com/feiyucq/archive/2010/06/07/1753465.html

拖进去一开始,如果F8或者F7的话进的是DLL的主函数,如果不想跟这些主函数的话可以直接F9,然后OD会再停下来,在最下方的消息栏会提示DLL初始化完成。

如下图的操作点击,然后选择导出函数,在导出函数的地址下个断点就可以直接调试了,点击调用按钮程序就断在这里了

img

img

结合静态分析,查找下图的字符串定位函数位置

img

调试f7进SHGetFolderPathW函数,可以看到该函数想获取的是桌面文件夹路径

img

由静态分析的图可以看出后一个函数PathAddBackslashW是在路径后加反斜杠,通过findfirstfile和findNextfile枚举桌面所有文件,并在文件夹名称前后添加了18个“#”字符然后再与提取的文件和目录列表和前面的字符串连接起来

img

以下目录也是后门的目标“C:\Program Files”, “C:\Program Files (x86)”, “C:\Users< User >\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Administrative Tools”, “C:\Users< User >\AppData\Roaming”, “C:\Users< User >\AppData\Roaming\Microsoft\Windows\Templates”, “C:\WINDOWS” and “C:\Users< User >\AppData\Local\Temp”

img

img

img

img

img

使用 GetComputerNameA API 检索本地计算机的 NetBIOS 名称;GetUserNameA 用于提取与当前线程关联的用户名;恶意软件通过调用 GetVolumeInformationW 的函数提取当前目录根目录的卷序列号;systeminfo 命令的输出加 tasklist 命令的输出加目标目录列表及其内容使用 CryptBinaryToStringA API (0x1 = CRYPT_STRING_BASE64) 进行 base-64 编码(IDA)

img

img

(上面这张图应该是第二个线程的,因为命令中包含cmd=y,该命令用于下载dll文件,因为两个线程在网络连接那一块是类似的,所以通过查找字符串定位函数位置的时候找错了)

img

主线程

主线程完成最后的收尾工作,将之前创建的事件设置为信号状态,使用GetExitCodeThread API获取两个线程的终止状态:

img

overlong

用PEid显示有壳,但是ExInfo不是,直接拖到IDA看看

image-20211025101621373

我们转到sub_401160函数看一下,图中的\x1c是十进制的28

image-20211025104708760

image-20211025104824155

unk_402008指针指向的内容与TEXT(a1)一共进行了28次的一些字符操作。但是该指针指向的内容不止28个字符,点击进去查看有175个

exe一开始运行是这样的,说从未破坏编码,那就直接将其改为读取175个

image-20211025105732834

image-20211025110224330

这样直接修改会有错误,提示Invalid operand,可能是吧修改的数据会覆盖掉后面的一些数据,我们拖进OD试试,发现改了以后后面的代码也变了

image-20211025111317713

image-20211025111342988

直接修改二进制码

image-20211025113152358

运行后如图,得到flag

image-20211025113214588

总结

​ 这道题目的收获主要是关于修改代码,一般都是直接修改,还没有考虑过会改变后面的数据。以后修改代码最好用OD,可以看到后面代码是否被修改

脱壳存根

​ 脱壳存根是一个类似于一个exe加载器,如果静态分析某个加壳的exe,实际上分析的是脱壳存根。主要执行如下:

  • 将原始程序脱壳到内存
  • 解析原始可执行文件的所有导入函数
  • 将可执行程序转移到原始的程序入口点(EOP)

解析函数导入表

  • 导入Loadlibrary和GetProcAddress

  • 保持原始导入表的完整,让windows加载器加载所有dll以及导入函数

  • 为原始导入表种的每个dll保留一个导入函数

  • 不导入任何函数,在不用函数的前提下,自己从库中查找所有需要的函数或者首先找到LoadLibrary和GetProcAddress,然后定位其他的库

    第二种和第三种静态分析都会很容易看到导入库。

尾部跳转

​ 完成脱壳后必须使用尾部跳转指令跳转到OEP执行,jmp是最简单的指令,所以有些使用ret或者call来隐藏。有时还会使用操作系统转移控制的函数来掩盖(譬如NtContinue、ZwContinue)。

脱壳过程

image-20211129202544768

image-20211129202522905

脱壳分类

自动脱壳

​ 自动静态脱壳解压缩或者解密一个可执行文件,但是仅仅只针对某个壳。PE Explorer提供了一些默认安装的静态脱壳插件,支持NSPack、UPack、UPX。

​ 自动动态脱壳程序运行可执行文件,并让脱壳存根脱出原始的可执行文件。自动的脱壳程序必须确定脱壳存根的结束位置,原始文件的开始位置。但是目前并没有非常好用的公开的自动动态脱壳程序。

手动脱壳

  • 找到加壳算法,然后编写程序逆向运行它。

  • 运行脱壳程序,让脱壳存根帮你工作。(让它从内存中转储出进程,然后再手动修正PE头部)

    用UPX的壳尝试一下OllyDbg脱壳
    重构导入表

    ​ 当OllyDump重构导入表失败时,可以使用导入重构器(ImpRec)。

    操作系统如何填充IAT:

    ​ 1:定位导入表

    ​ 2:解析第一个IID(IMAGE_IMPORT_DESCRIPTOR)项,根据IID中的第4个字段定位DLL的名称

    ​ 3:根据IID项的第5个字段DLL对应的IAT项的起始地址

    ​ 4:根据IAT中的指针定位到相应API函数名称字符串

    ​ 5:通过GetProcAddress获取API函数的地址并填充到IAT中

    ​ 6:当定位到的IAT项为零的时候表示该DLL的API函数地址获取完毕了,接着继续解析第二个IID,重复上面的步骤。

    1)定位导入表

    img

    2)定位到导入表的起始地址

    img

    3)根据第一个IID项中的第四个字段得到DLL名称字符串的指针,这里指向的是USER32.DLL

    img

    根据第五个字段的内容定位到IAT项的起始地址,这里是403184,我们定位到该地址处。

    img

    这里我们可以看到已经被填充了正确的API函数的入口地址,跟我们dump出来的结果一样,我们再来看看相应的可执行文件偏移处的内容是什么。

    img

    这里我们可以看到第一个API函数的名称位于4032CC地址处,我们定位到该地址处。

    img

    第一个API函数是KillTimer,我们在OD中看到的KillTimer的入口地址是操作系统调用GetProcAddress获取到的。

    img

    这里我们可以看到KillTimer的入口地址为77D18C42。该地址将被填充到IAT相应单元中去覆盖原来的值。

    img

    这里是IAT中的第一元素。

    我们再来看下一个元素,向后偏移4就是,来看一看该API函数名称字符串的指针是多少。

    img

    定位到可执行文件的相应偏移处:

    img

    32D8即4032D8,来看看该API函数的名称是什么,这里由于该指针不为零,说明该API函数还是位于USER32.DLL中的。

    img

    这里我们可以看到第二个API函数是GetSystemMetrics,通过该函数名称可以通过GetProcAddress获取到其函数地址然后填充到IAT中。接下来按照以上步骤依次获取USER32.DLL中的其他的函数地址,直到遇到的IAT项为零为止。我们来看一看可执行文件中结束项位于哪里。

    img

    我们可以看到当IAT中元素为零的时候表明USER32.DLL就搜索完毕了,

    手动修复导入表

    ​ 前面的工具是搜索内存中的导入函数来重构导入表,但是也有失败的时候。先了解一下导入表如何工作的。导入表在内存中实际有两个表,一个是函数名称或者序号列表,其中包含记载器或脱壳存根需要的函数名称或者序号;第二个表是所有导入函数的地址列表。实际上运行的时候只需要第二个,所以加壳程序可以通过移除名字列表阻止分析。这时候就需要手动重构。

    ​ 在反汇编中遇到导入函数时,逐个进行修复是最简单的策略,比如下图为例,IDA中DWORD处的值明显位于加载程序的地址范围之外,使用OD打开文件,将光标移动到该处查看内容,OD标注为WriteFile,所以我们可以将其标注。

    ​ 另外一种方法是需要运行脱壳程序,如果发现了导入函数表就看可手动重构原始的导入表,也可以写个脚本来帮助输入这些信息,缺点是复杂又费时。

    image-20211201103634458

    查找OEP

    ​ 自动工具查找OEP,最常使用的是OllyDump。通常脱壳存根与可执行文件不在一个节中,使用step-over或者step-into的方法,当程序跳转到另外一个节中运行时,OllyDbg可以探测到这种转移,并在那里进行中断。call函数就是用来调用其他节的代码,这种方法旨在防止OllyDbg错误标注这些调用OEP的曹祖破,step-over方法将跳过所有call指令。但是如果该call没有返回,那么OllyDbg就会定位到OEP。因此一些恶意代码经常包含一些没有返回的call,以此来干扰分析。

    ​ 所以实际情况要结合step-over和step-into方法。

    手动查找OEP

    1.最简单的就是查找尾部跳转指令(一串无效字节指令前的最后一条有效指令),填充这些字节的目的是为了保证节的字节对齐。

    image-20211130161428842

    ​ 图中非常明显的标志是:位于尾部但是链接到了一个很远的位置正常情况为几百个字节以内),并且一般情况下跳转指令之后会有一个返回,但是图中全是一些无意义代码

    ​ 在IDA中,如果是一个尾部跳转会标注为红色

    ​ 在脱壳存根开始运行时,尾部跳转的地址不包含有效指令,但是一旦被运行就肯定包含有效指令。

    2.在栈上设置读断点。反汇编中的大部分函数,包括脱壳存根都是以push开头,首先在栈中记录第一个入栈的内存地址,然后在这个栈位置设置一个读断点,只有在脱壳完成的情况下,才能获取原始push的堆栈地址。所以在pop指令获取那个地址的时候,就会命中断点。(通常尾部跳转紧跟pop

    ​ **3.**在代码中每个循环后面设置断点(通过扫描代码来识别循环,并且在每个循环之后设置断点)

    ​ **4.**在GetprocAddress函数设置断点。

    ​ **5.**在被原始程序调用且继续向后工作的函数上设置断点。因为壳通常相同,因此可以在它调用的一个函数上设置断点,来发现OEP。比如壳通常很早就调用了GetVersion以及GetCommandlineA函数,所以可以在这些函数调用时中断程序。于GUI程序,GetModuleHandleA通常是第一个被调用的函数。

    ​ **6.**使用OllyDbg的Run Trace。Run Trace提供一些额外的断点选项,这使得能在较大范围的内存地址上 设置断点。很多加壳程序都会留下原始文件的.text节,OEP总是位于原始文件的该节中,一般是这个节中第一个被调用的指令,Run Trace可以让你设置这样的一个断点,无论什么时候执行.text节中的指令,此段点都能被触发。

    常见的壳

    UPX

    ​ 这个壳开源免费并且易于使用,但是它不会对分析人员造成阻碍,所以很多恶意代码看似使用了UPX,但是实际上使用其他的壳或者修改过的UPX,这个时候可以根据前面描述的策略查找OEP,也可以通过OllyDump的Section Hop功能来查找OEP,或者直接仔细检查脱壳存根,直到找到尾部跳转为止,然后使用OllyDump转储文件,并重构导入表,成功脱壳。

    PECompact

    ​ 是一个商业的壳,它有一个插件框架,允许第三方工具集成进去,因此恶意程序进场使用该第三方工具,使得脱壳变得更困难。该加壳方法的脱壳与谦和相似,程序会产生几个异常,所以需要设置OD将异常传回程序(options-Debugging options-Exceptions)。可以通过查找尾部跳转失灵来查找OEP。

    ASPack

    ​ 使用了自我修改代码,让设置断点和分析它变得困难。在程序上设置断点,可以让程序立即终止,但是可以在栈地址行设置硬件断点完成手动脱壳,但是因为该壳很流行,所以网上有很多自动脱壳程序。

    ​ 手动脱壳时,首先打开脱壳存根的代码,在代码开始部分,会看到一个PUSHA指令。确定用来存在寄存器的栈地址,然后在这些栈地址设置硬件断点,调用POPAD指令时,就会触发,然后会在离尾部跳转不远的地方找到OEP。

    Petite

    ​ 使用了单步异常,但是可以像前面说的将异常传回程序,与ASPack相同,使用栈上的硬件断点来查找OEP是最佳策略。另外,它保持从原始导入表的每个库中至少导出一个函数,这在没有脱壳的情况下,很容易确定恶意程序使用了那些dll。

    WinUpack

    ​ 一个有着GUI终端的壳,设计目的在于优化压缩而不是安全。该壳有一个命令行版本叫做UPack,有专门针对该两种壳的自动脱壳器。

    ​ 识别尾部跳转有一个技巧,大多数脱壳存根都小于0x4000个字节,因此跳转的大小大于或者等于0x4000一般是跳转到OEP(针对书中例子),脱壳存根通常有很多条件跳转并且在函数中间返回,但是OEP周围的代码应该不会有这些不寻常的元素。

    ​ 另一种针对Upack的策略是在函数GetModuleHandleA(GUI)或者函数GetCommandlineA(命令行)上设置断点。在windows中,这些函数在调用Oep不久后就会被调用,一旦触发断点就向后搜索代码查找OEP。

    ​ WinUpack有时会使用一个OD不能正确解析的头部,使其崩溃,这时候需要首先使用WinDbg.

    Themida

    ​ 一个非常复杂的壳,包含阻止使用vmware、调试器,以及Procmon分析的功能。此外还有一个内核模块,运行在内核中的代码限制很少,而且分析程序通常运行在用户空间中,所以分析会受到很多限制。自动化工具成功与否与加壳版本有关。

    ​ 如果自动化工具无法脱壳,那么一种较好的方法是用procDump工具从内存中转储不在进行调试的进程。该工具的最大优点是在不停止进程或者调试进程的情况下,转储进程中的内存。这个过程并不能完全恢复可执行文件,但是能让我们在代码上运行strings工具并做一些分析。

    加壳DLL

    ​ 加壳Dll列出的开始地址是脱壳存根中的一个地址,位于DllMain中,而不是主函数中。DllMain函数在OC终端他运行之前被调用,中断发生的时候,脱壳存根已经运行,这将很难找到OEP。解决这个问题,我们可以打开PE文件,定位到IMAGE_FILE_HEADER节的特征标志域,将该节0x2000处的比特位从1修改为0,文件从dll变为exe.

这篇文章主要目的是帮助安全从业人员理解:Windows下访问令牌是怎么工作的;攻击者是如何利用合法的Windows功能来横向移动并损害整个ActiveDirectory域信;他们检测和响应其环境中的访问令牌操作的能力。

Windows Security Internals

Logon Sessions and Access Tokens

简单的说,用户登陆到windows系统之后,不管该用户是本地登陆的,还是远程登陆,系统都会为这个用户分配一个新的**会话ID(SID)**。也就是说会话与用户的登录是相关连的,没有用户登录就不存在会话。因此,会话的含义是指用户登录之后的一种运行的环境。会话管理器(\Windows\System32\Smss.exe)是系统中第一个创建的用户态模式进程,负责完成执行体和内核的初始化工作的内核模式系统线程在最后阶段创建了实际的Smss进程。

会话(session)是由进程和其他的系统对象(比如窗口站、桌面和窗口)构成的,它们代表了一个用户的工作站登录会话。会话具体是由如下几个部分组成的:

\1. 每个会话包含一个单独的win32k.sys

\2. 专门的换页池区域

\3. 私有windows子系统和登陆进程的拷贝

\4. 系统空间中被映射的空间,被称为会话空间的区域

现在会话同进程做一个比较,发现他们之间有一些相似之处:

\1. 都提供一个执行的环境

\2. 都有一个私有空间

进程是为了内部的执行的线程提供一个空间和环境,而会话则是为内部所有的进程提供一个执行的空间和环境。当用户登陆到系统中之后,用户下所有的进程都属于这个会话空间,在每个进程的PEB当中就有SessionID。Windows创建的第一个会话被称为会话0,是系统用户SYSTEM的会话通常称为会话0,服务就是运行在此会话中。而令牌就像是一个标识符,标识该账号的一切应用和操作。

访问令牌是用来描述进程或线程安全上下文的对象,令牌所包含的信息是与该 user 账户相关的进程或线程的身份和权限信息。当 user 登录时,系统通过将 user 输入的密码与储存在安全数据库中的密码进行对比。若密码正确,系统此时会为 user 生成一个访问令牌。之后,该 user 执行的每个进程都会拥有一个该访问令牌的副本。每个线程默认直接继承进程的token,线程还可以通过模拟,改变自己的token,让自己拥有别的用户的token。

令牌产生过程:使用凭据(用户密码)进行认证;

登录Session创建;

Windows返回用户sid和用户组sid;

LSA(Local Security Authority)创建一个Token;

依据该token创建进程、线程(如果CreaetProcess时自己指定了 Token, LSA会用该Token, 否则就继承父进程Token进行运行)

首先了解下令牌的四个模拟级别,分别是:Anonymous,Identification,Impersonation,Delegation

Anonymous:服务器无法模拟或识别客户端。

Identification:服务器可以获取客户端的身份和特权,但不能模拟客户端。

Impersonation:服务器可以在本地系统上模拟客户端的安全上下文。

Delegation:服务器可以在远程系统上模拟客户端的安全上下文。

所以当令牌具有Impersonation和Delegation级别的时候才可以进行模拟。

img
言而总之,只要我们有SeAssignPrimaryToken或者SeImpersonate权限,就可以通过模拟Primary令牌来提升权限,而Primary令牌可以通过DuplicateTokenEx调用一个Impersonation令牌来转换。

所以一个模拟令牌的过程大概是:OpenProcess(获取目标进程上下文)->OpenProcessToken(获得进程访问令牌的句柄)–>DuplicateTokenEx(创建一个主/模拟令牌)–>CreateProcessWithTokenW(创建进程)

How Attackers Abuse Access Token

攻击者可以使用内置的Windows API函数从现有进程复制访问令牌,这就是所谓的令牌窃取。然后可以将这些令牌应用于现有进程(即Token Impersonation/Theft)或用于生成新进程(即Create Process with Token)。攻击者必须已经处于特权用户上下文(即管理员)中才能窃取令牌。然而,攻击者通常使用令牌窃取将其安全上下文从管理员级别提升到系统级别。然后,如果帐户在远程系统上具有适当的权限,则攻击者可以使用令牌向远程系统进行身份验证,作为该令牌的帐户。

在ATT&CK框架中,对于令牌操作有五种技术介绍:

img

Token Impersonation/Theft

攻击者可以使用DuplicateToken(Ex)创建一个新的访问令牌来复制现有令牌。然后,该令牌可与ImpersonalLoggedOnUser一起使用,以允许调用线程模拟登录用户的安全上下文,或与SetThreadToken一起将模拟令牌分配给线程。

检测:

如果攻击者使用标准命令行shell,分析人员可以通过审核命令行活动来检测令牌操纵。具体来说,分析人员应该寻找runas命令的使用。默认情况下,Windows中未启用详细的命令行日志记录。

分析人员还可以监视Windows API(如DuplicateToken(Ex)、ImpersonalLoggeDonUser和SetThreadToken)的使用情况,并将活动与其他可疑行为关联起来,以减少由于用户和管理员正常的良性使用而导致的误报。

Create Process with Token

攻击者可能会使用重复的令牌创建一个新进程,以升级权限并绕过访问控制。攻击者可以使用DuplicateToken(Ex)复制所需的访问令牌,并将其与CreateProcessWithTokenW一起使用,以创建在模拟用户的安全上下文下运行的新进程。这对于在不同用户的安全上下文下创建新进程非常有用。

检测:

如果攻击者使用标准命令行shell,同上。

如果攻击者使用直接调用Windows令牌API的有效负载,分析人员只能通过仔细分析用户网络活动、检查正在运行的进程以及与其他端点和网络行为的关联来检测令牌操纵。

分析人员还可以监视Windows API,同上。

Make and Impersonate Token

攻击者可以制作和模拟令牌以提升权限并绕过访问控制。如果攻击者拥有用户名和密码,但用户未登录系统,则攻击者可以使用LogonUser功能为用户创建登录会话。该函数将返回新会话的访问令牌的副本,攻击者可以使用SetThreadToken将令牌分配给线程。

检测:

同上

Parent PID Spoofing

攻击者可以欺骗新进程的父进程标识符(PPID),以逃避进程监视防御或提升权限。除非明确指定,否则新进程通常直接从其父进程或调用进程派生。显式分配新进程的PPID的一种方法是通过CreateProcess API调用,该调用支持定义要使用的PPID的参数。此功能由Windows功能(如用户帐户控制(UAC))使用,以便在系统生成请求的提升进程后正确设置PPID(通常通过svchost.exe或approve.exe)而不是当前用户上下文。

攻击者可能会利用这些机制来逃避防御,例如阻止直接从Office文档生成的进程,以及针对异常/潜在恶意父子进程关系的分析,例如将PowerShell/Rundll32的PPID欺骗为explorer.exe,而不是将Office文档作为Spearphishing Attachment的一部分。此欺骗可以通过Visual Basic在恶意Office文档或任何可以执行本机API的代码中执行。

显式分配PPID还可以启用提升的权限,赋予父进程适当的访问权限。例如,特权用户上下文(即管理员)中的攻击者可能会生成一个新进程,并将父进程分配为作为系统运行的进程(如lsass.exe),导致新进程通过继承的访问令牌提升。

检测:

查找存储PPID信息的各个字段之间的不一致性,例如通过Windows事件跟踪(ETW)收集的数据中的EventHeader ProcessId、Windows事件日志中的创建者进程ID/名称以及ProcessId和ParentProcessID(也由ETW和其他实用程序(如任务管理器和Process Explorer)生成)。ETW提供的EventHeader ProcessId标识实际的父进程。

监视和分析对CreateProcess/CreateProcessA的API调用,特别是来自用户/潜在恶意进程的API调用,并使用显式分配PPID的参数(例如:进程创建标志0x8XXX,表示正在使用扩展启动信息创建进程)。恶意使用CreateProcess/CreateProcessA也可能通过调用UpdateProctThreadAttribute进行,这可能是更新进程创建属性所必需的。这可能会从正常UAC提升行为中产生误报,因此如果可能,请与系统基线/对正常系统活动的理解进行比较。

SID-History Injection

对手可以使用SID历史记录注入来升级权限和绕过访问控制。Windows安全标识符(SID)是标识用户或组帐户的唯一值。Windows security在安全描述符和访问令牌中都使用SID。帐户可以在SID-History Active Directory属性中保存其他SID,从而允许在域之间进行可互操作的帐户迁移(例如,SID-History中的所有值都包含在访问令牌中)。

使用域管理员(或同等)权限,可以将获取的或已知的SID值插入到SID历史记录中,以启用对任意用户/组(如企业管理员)的模拟。此操作可能会导致通过横向移动技术(如远程服务、SMB/Windows管理员共享或Windows远程管理)提升对本地资源的访问和/或访问无法访问的域。

检测:

使用PowerShell Get-ADUser 命令行工具检查用户SID历史记录属性中的数据,尤其是具有来自同一域的SID历史记录值的用户。还可以监视域控制器上的帐户管理事件,以查看SID历史记录的成功更改和失败更改。

监视对DsAddSidHistory函数的Windows API调用。