Ransomware Locky Analysis

来源:互联网 发布:已定义两个字符数组ab 编辑:程序博客网 时间:2024/04/29 18:19

Locky的变种非常的多,这个样本来自下面的Url,是最新的一种变种。 

这是程序在刚开始执行时与释放了Image并替换了之后的对比,很明显发生了进程替换,因此进行分析之前有必要把它内部释放出来的image提取出来,分析这个image才能搞清楚它是如何做加密的。

 

 Locky存在一个未知的壳,IDA并不能检测出这个壳,因为它的导入表等信息并没有被破坏。它在运行起来后
,执行相当大量的垃圾代码干扰调试,依据是在调试的过程中它进行相当多的寄存器操作但6个通用寄存器的值始终是0。

这这些垃圾代码中隐藏着它获取Kernel32.dll的Addr的逻辑,它会获取Kernel32的BaseAddress并通过偏移计算出API的地址。

它会调用VirtualAlloc分配一块可写可执行的内存块,大小是61BB,可执行意味着会释放代码到这块内存区中,应该着重分析。(0x404587

TextSegment中存在一部分未被IDA识别的func,位置在(0x402940)。这部分代码的作用是将一些数据(在TextSegment中)释放到刚才通过VirtualAlloc分配的内存中去。

GenerateShellCodes(PVOID lpShellCodes, BYTE* pDataInTextSegment, DWORD dwLen, DWORD dwHEX);

fillShellCodes_40298D

其中bl寄存器用来作为临时存储一个字节的寄存器,ebx寄存器用作计数器,它从函数参数中获得Data的Length,然后递减,如果等于0就跳出循环,esi是指向VirtualAlloc返回的内存地址。处理完毕后,会在指定的位置写入一堆数据,这些数据还需要进行异或处理才能变成真正可以被执行的代码。

以下是没有被处理的数据,只是单纯的填充了缓冲区。

001D0000

它的解这部分Code的算法如下:

xorShellCodes_4029A2

其中dwHEX是这个处理函数的第四个参数。解完了以后的数据如下:

001D0000_2

下面已经完成了部分ShellCodes的释放,接下来将跳转到ShellCode去执行代码。

loc_404CB8

这段ShellCode的目的释放一个被压缩的PE文件,并加载这个PE文件,整个过程没有释放任何文件出来,无法被监控软件发现,释放PE文件使用了非常精巧的方式加载到当前进程中,并完成了进程替换。

被释放的ShellCode本身还有很多代码没有释放出来,在ShellCode的Offset=247处有一个func用于释放ShellCode中的代码(释放不太严谨,应该是通过一些异或操作把原来不是代码的数据转换成代码)。

loc_1D0266

这个操作会将ShellCode头部的一些代码释放出来。在后面经过调试发现,这个func会被反复的多次调用,用于释放存在ShellCode中的数据(转换成可以被执行的CPU代码)。因此这里应该是释放ShellCode自身代码的逻辑。

sub_1D1020用来获得Kernel32的BaseAddr:

loc_1D08EC

loc_1D0B58GetModuleHandle

 

 

再被释放的ShellCode+1020处有一个Func(SC_GetModuleAddr)它用来根据传入的HardCode得到对应的Module的基地址,例如Kernel32.dll Advapi32.dll等。

LPVOID SC_GetModuleAddr(DWORD dwHEX);

对应的在ShellCode+1122处有一个Func(SC_GetFuncAddr)它用来根据传入的HardCode得到对应的func的基地址,例如Kernel32的GetProcAddress等。

PFUNC SC_GetFuncAddr(DWORD dwHEX, LPVOID lpModuleBaseAddr);

如下图:loc_1D03E9

ShellCode会调用这段代码GetProcAddress,并传入Module的基地址以及要获取的函数名。这里函数的名字被嵌入到了ShellCode中,被解释称代码了,需要重新解析成字符串。loc_1D047C注册服务,隐藏自己的行为。

后面的逻辑大致符合如下描述:

  1. 首先通过压入一些特定的HEX值获取GetProcAddr的地址。
  2. 然后传入要用到的API所在的dll的基地址以及在ShellCode中隐藏的函数名。
  3. 使用获得的API,不缓存,下次再用还要走这个逻辑。

例如:用同样的方法获得了EnumServicesStatusExA的函数地址。001D0518

这里调用GlobalAlloc(ShellCode+001D0686)在堆上分配内存分配的内存用0初始化,大小是0x4214。(可能是一个结构)001D05E6因为被释放掉了。001D07A2

这里又使用API分配了一块内存,大小是1799C。页属性是可读写不可执行。001D2207这个函数的参数很值得注意,居然是当前进程的基地址。001D22C40012FCA0

这里从当前的进程中释放了一堆压缩数据到刚才分配的内存中,然后又继续调用VirtualAlloc分配了一块大小1AE00的内存,页属性依然是可读写不可以执行,这块内存用于解压缩刚才释放的压缩数据。001D30E4

程序接下来调用了RtlDecompressBuffer这个Undocument的API。用于释放压缩的数据到指定的Buffer中。释放出来的文件是一个PE Image。(ShellCode+318E)00200000

把这个PE dump出来之后可以从导入表中发现一堆Crypt相关的API,它就是真正加密用户数据的PE。00412000

到这里后面的分析都与Ransomware没有关系了,只要能拿到内部的加密数据的文件进行分析就可以了。下面是这段ShellCode作为加载器还有一些什么具体行为的分析。001D2928

这里释放了压缩数据所占用的内存。001D23F1001D24DB001D25BB

这里打开了通过获取当前壳进程的ImagePath,然后调用CreateFile打开文件并计算文件的Size。loc_1D264B

计算出来的文件大小是31000,然后调用VirtualAlloc分配相同大小的空间。下面是VirtualAlloc的参数。可读可写不可以执行。0012ECA4

 

然后调用ReadFile从当前Image的文件中读取数据,一次读取完毕。0012ECA0001D277D

然后关闭文件。001D2828

现在内存中有2份PE文件了,一份原始的壳的Image,还有一个被脱壳后真正做加密工作的Image。

00570000上面是原始壳的PE数据

00200000上面是被脱壳后的PE数据

判断它最后会还原回去,不然没有必要保存一份在内存中。这样可以不用释放文件,监控软件监控不到,非常精明的设计。001D1700

这里调用VirtualProtect来对0x400000位置的当前Image设置可写权限,要开始覆盖当前的数据了。0012FCA0

开始将Image的内容清空:001D17C6

00400000_1

然后从debug021:001D17E9开始的一大段代码用于填充新数据到当前的Image中去。好大的一段代码,因为内存中的PEOffset与加载到内存后的Offset不一样,所以需要小心的分段加载PE数据,这里的代码类似一个进程加载器。00400000_2

写完后的数据已经与上面释放出来的PE一样了(对照0x200000地址数据),这里发生了进程替换,它肯定还要找到新的PEEntryPoint开始新的调用。

001D34C2

调用RtlZeroMemory把释放出来的在内存中的做加密的PEImage擦除掉。

00200000_1

然后调用VirtualFree释放掉内存(清理现场)。

 

现在调用VirtualProtect把在0x400000位置的新的PEImage页属性设为只读。012FCB0

PEHeader,第一个页面。继续设置代码段为可执行可读。

012FCAC

到这里,新的PE已经加载完毕了,应该可以准备执行了。001D377A

这里会把一些API用到的都取出来保存在栈上,后面用。有一些,不过我最关注的是CreateThread,然后它会清理ShellCode,清理犯罪现场。通过使用下面的API调用。清理的过程分几个阶段,头部的ShellCode还保留,只是把除了头部以外的都清理成0.77475C90

0012FCDC

可以看到从F6之后都是0了。001D0000

然后调用VirtualFree把这整段Code删除掉。到这里ShellCode的使命已经完成,简单来说它的目的就是将内部的一个PE释放出来,并替换当前进程,并把当前进程的数据缓存到内存中。

然后,程序会执行CreateThread,执行新的PE代码。7682375D

405152就是新的进程的EntryPoint,可以通过对提取的PEImage的分析佐证。00405152

至此,脱壳完成并且成功的分析到了这个壳的运作机制。

flow

=========================运行测试==========================

这3个Sample的行为观测很相似,但是从反汇编角度分析的话,技术在不断的演进。虽然都没有检测到壳的存在,但是在运行的过程中会从代码段或数据段释放code出来执行,也许有进程替换。也怀疑有虚拟机检查,因为都没有行为出现。

========================脱壳后分析========================

脱壳后的程序可以正常运行,但是之前也讲过即使在物理机器上也没有跑出行为,所以进一步分析原因,以及分析Locky的整体运作方式。

这段程序的entrypoint据我判断是手动编写的,非经过编译器产生。他在开始调用C runtime的入口之前做了一些自定义的隐蔽工作,这并非编译器的行为。它的诡异行为如下面所述:reloc.reloc是一个配置块,它的数据结构用C来表述如下:tagLockyConfigure

程序会根据这个结构的信息做对应的处理,例如如果选择模仿svchost,它就会伪装成svchost,如果选择自动启动,就会在注册表中注册自己,如果选择了地区保护,就会只针对相应的地区展开攻击,等待时间被用来隐蔽自身,不立即运行。

 

  1. 脱壳后的程序在它的PE文件中伪装了一个.reloc的段,这个段中藏了4个IP地址。iplist

其中有2个目前仍然可以ping通。这些IP地址在内存中的位置,被它保存到了一个全局指针中。

 

  1. 它会去Hook NtQueryVirtualMemory,并针对所有的可执行内存页查询返回篡改后的结果。这个操作用来AntiDebug。77486259

000D0000_3

MEM_IMAGE

0x1000000

Indicates that the memory pages within the region are mapped into the view of an image section.

 

  1. 程序会取出真正的.reloc段,并在内存中分配一块内存,把当前的Image拷贝到新的内存地址,并用当前Image与新内存中的Image的diff addr去处理.reloc段,使得执行新内存时能够定位到正确的函数地址(外部导入)。

这个操作使得原始Image程序的main函数不会被进入,反调试。003D6689


Jump2NewImage会跳转到已经解决了重定位问题的新的在内存中Image中,并开始从pop这条指令开始执行,从下面图可以看出指令是一致的。00086691

这里已经分析出了它实际会返回到调用指令的下一跳指令中,只不过不再是同一个内存中的Image,解决这个问题直接打掉patch就可以。如下图:0040525E

将PerpareForRunInOtherImage打上Patch,这个func的目的是反调试,会坚持程序断点,同时它会跳到一个内存中的其它位置执行相同的程序,同时调整重定向表,因此在这个函数内部打patch没用,因为重定向表被修改后会导致程序崩溃。00FE525E

最简单的修改方法。现在,所有的障碍都已经扫除可以调试并分析这个Sample了。

程序在进入主函数之后,立即安装一个顶级的未处理异常处理器,会在发生无法解决的异常后重新启动自己。SetUnhandleExceptionFilterTopLevelExceptionFilter

然后程序会把藏在上述中起迷惑作用的.reloc段中的4个Ip地址以及包含Ip地址的数据结构取出来,Ip地址会被放到一个vector中保存起来。

从上面的一张内存截图可以看到,IP地址是使用逗号分隔的。split_ipAddr

查找逗号在String中。push_back

加入全局的std::vector<std::string>中。

接下来它会检查藏在.reloc中的数据结构中的相应的值。ContryProtection

它会避开俄语区用户,LABEL_21处有个function,调用它会先将当前程序的File找到,然后移除所有属性,随后生成一个临时目录,将Image移动过去,可能会失败,会采用MOVEFILE_DELAY_UNTIL_REBOOT来调用MoveFileEx标记为重启删除。紧接着构造一个“cmd.exe /C del /Q /F”串并把原路径文件加入到这个串的后面,调用CreateProcess来删除文件,最后退出程序。

接下来会根据藏在.reloc中的数据然后根据数值去等待,目前等待的时间超过3天,这是我们无论如何都没有跑出行为的原因。waittime

这里打上patch,跳过这些逻辑,使得程序可以正常运行,避免一些内部逻辑导致的程序不运作不能反映出真实行为。

接下来的代码会通过”GetVolumeNameForVolumeMountPoint”API,根据Volume获取一个GUID,然后对这个GUID进行hash得到一个hash值,再通过这个hash值得到一个字符串并把它链接到HKLM\Software\{xxxxxxxxxxx}用于创建一个注册表项。registry

紧接着它会用藏在.reloc中的数据结构中的值判断是否需要模仿svchost运行,如果是它会将自己Copy到一个随机目录并重命名为svchost.exe,并通过CreateProcess重启进程并结束自己的当前运行。SimulationSvchost

接下来会先去获取本地机器的信息,之前通过Volume的GUID算出的一个hash被用作机器信息的ID,这部分数据会被提交到作者的服务器上面去。sys_info

“id=D2BCC112BD05308A&act=getkey&affid=3&lang=en&corp=0&serv=0&os=Windows+7&sp=1&x64=0”

然后用MD5对这部分数据进行hash:md5

程序接下来将系统信息的hash数据追加到系统信息的头部,作者可以利用这个hash值校验数据一致性,然后通过一个循环移位加异或算法对这部分数据加密,如下图。012e1520

接下来它准备要发送数据到作者的WebServer上面去,最开始说了作者在伪装的.reloc中藏了一些配置信息,同时包含4个主机地址,在程序一开始,这四个主机地址被加入了一个vector中,程序通过获取一个随机数字,并对存储在vector中的ip_addrnum取余数来随机选择一个Ip地址用于访问。evil_ipaddr

它先构建一个http地址:evil_http_0UserAgent使用上面的信息创建一个http对象,然后设置一些参数:SetOpt30000

发送和接收的延迟设置为30000,重试次数1次。

这里发生了问题,首先我可以连接到服务器,但是在向对方发送数据的时候,失败了返回的HttpError 403 Forbidden:资源不可用。服务器理解客户的请求,但拒绝处理它。通常由于服务器上文件或目录的权限设置导致。所以这个sample因为无法将用户信息上传出去,陷入了一个软件异常,不会继续执行加密操作。这里得想办法让他跳过这个发送的步骤直接去加密用户数据。以下是尝试发送的数据,包含了用户的SysInfo和一个用于校验的Hash被循环左移右移异或扰乱后的数据。SysInfoHash

 

修改2处检查点,让它不发送Crypt信息,直接开始加密文件,但我判断在发送了用户的SysInfo后,服务器可能会计算一个PublicKey并通过网络返回到本地,因为Locky如果请求不到网络的话它不会做任何事情,因此判断可能不存在Default的Key。

 

上面的判断是正确的,因为在下面的分析中,当准备开始加密数据的时候,程序调用了一个API函数:CryptImportKey,在这个函数的参数中送入了一个全局变量,通过IDA xref我可以看到这个全局变量既是服务器返回的数据,因此可以判断出程序使用用户机器的相关信息计算出一个PublicKey,用这个Key对数据进行加密(部分,或加密对称加密的Key),因此这个API也许可以去Hook,然后替换一个自己的Key,用于日后解密。

 

我们打上Patch,让他不发送用户信息到服务器上面去,直接开始加密文件,进入到枚举驱动器资源的阶段(我看过的所有Ransomware都有这个步骤),首先它会去枚举网络驱动器,例如Nas,这里我们不管它,因为在枚举网络驱动器的时候虚拟机调试会抛出异常,我也patch了。

 

枚举到的所有网络驱动器资源它会先将它们放到一个vector<std::string>向量描述的链表中保存起来。

EnumerateNetDrive

接下来会继续枚举本地磁盘驱动器,下面会有一堆的判定条件是否要处理这个磁盘,首先会排除所有带有remote属性的,并且如果是可以拔插的如USB也不处理,如果磁盘的总大小低于A00000(1MB)跳过,如果磁盘类型是Fixed、Removable以及Ramdisk都不处理,如果磁盘信息中包含的SystemFlags超过了19也不处理,逻辑如下:GetLogicalDrives

程序将所有符合要求的磁盘依次加入vector中保存起来,最后会返回这个vector(其实这是一个在栈上的vector,程序最后会将这个vector的内容复制到作为入参的外部vector中)。AddDrive2Vector

 

当准备好所有的可以加密的驱动器后,程序会针对每一个驱动器启动一个线程来执行加密工作,在线程加密的同时,程序会删除掉系统还原的影子盘,最后程序会block自己知道wait到了所有的线程执行完毕的event。TR_Encrypt在这个进入waiting之前,它会根据.reloc中的配置选择是不是要安装自启动。这些操作都完毕后,主线程进入wait状态。WaitingAllHandle

接着程序会进入到线程中执行,线程执行的代码是加密用户数据。加密线程首先枚举当前驱动器中的所有文件,并加入到vector中。这个过程中,筛选的文件会过滤掉一些指定的文件路径,也会过滤掉不感兴趣的后缀名文件。

ExcludeNoEncryptList

过滤特定的文件路径

ExcludeExtName过滤特定的后缀

 

Locky也是按照访问时间顺序排序的,也就是说最后被访问的文件最先被加密,不过它存在一个Bug,它会先找最后被访问的文件夹,比如在一个超大文件夹中有一个文件被修改了,那么这个文件夹的访问时间也会被修改,但是这个文件夹中其它的文件很长一段时间都没有被访问,Locky会最先加密这个文件夹中size最小的文件,直到加密完整个文件夹,才去找下一个文件夹,如果下一个文件夹中所有的文件都是最近访问过的,它也会优先加密只有一个文件是最后访问时间的文件夹,Locky没有处理这种情况。

在我的机器中,它会优先加密IDA文件夹。显然Ransomware也存在如何精确定位有价值信息这个问题。

接着,程序开始将通过WebServer计算得到的对应用户SysInfo的PublicKey从内存中取出来,通过API:CryptImportKey来得到一个PublicKey对象。因为这里我们之前分析无法访问Locky作者的WebServer 403拒绝访问了,因此这里会丢出软件异常,无法开始加密数据。ImportPublicKey

这里的pbData是从全局对象中取出来的,因为这里没有publickey,我需要构造一个自己Key送进去,让他继续工作,因此我生成了一个公私钥对送了一个自己的PublicKey给程序。PublicKey

PublicKey

 

这里程序初始化了一个内部使用的CryptObject,它的数据结构如下:CryptObject

其中hKey是已经导入的PublicKey的句柄,剩下的16个字节是对应于用户Machine的系统信息产生的Hash被用于用户标识。

初始化完CryptObject后,程序得到了PublicKey以及User的机器ID,将这个对象传入EncryptFile开始执行加密工作。EncryptFile

加密函数除了Crypt对象外,还吃一个filename,来决定加密哪一个文件。

在内部,程序会根据传进来的filename计算一个hash,然后CryptObj中包含一个User机器的ID,新文件名=UserMachineID+filename_hash+.locky;NewFileName

接着程序会打开需要加密的文件,并把源文件重命名为上面的这种文件名,Locky接着会去检查是否有硬件加速可以用,如果有的话采用增强指令集加速加密的过程。

cpuid

Advanced Encryption Standard Instruction Set (or the Intel Advanced Encryption Standard New Instructions; AES-NI) is an extension to the x86 instruction set architecture for microprocessors from Intel and AMD proposed by Intel in March 2008.[1] The purpose of the instruction set is to improve the speed of applications performing encryption and decryption using the Advanced Encryption Standard (AES).

 

Locky通过WindowsAPI CryptGenRandom生成一个16字节的种子值。

如果有硬件加速可以用(目前CPU基本都支持硬件加速),分配一个以16字节对齐的内存块。接着用16字节种子初始化这个内存块。

随后,Locky使用之前传进来的PublicKey调用API:CryptEncrypt对种子值进行加密。EncryptRandom

Size=16,pbBuffer存储的是通过CryptGenRandom生成的随机值。

 

如果加密后的返回的数据块的大小不等于256,那么会丢出异常,这里的作用是判断生成的Key长度是不是正确。noequal256

 

在这个加密函数中,有一个数据结构和派生自一个基类的两个类值得说一下,其中EncryptBlock这个数据结构大致的作用是用来保存当前加密用的上下文,它保存了两个固定的用于xor的4字节数据,用户的SysInfo的ID,一个128位的种子值(通过CryptGenRandom得到),一个MAX_LEN长度的双字节数组用于保存当前文件的文件名,还有代表当前文件属性的bitmap,以及文件size,结构如下。EncryptBlock

 

另外两个类对象是Encrypto的主体,它们都用Encrypto派生出来,一个带有硬件加速功能EncryptoAC,一个没有带有硬件加速功能EncryptoNoAC。

 

我只分析了带有硬件加速的对象,它有5个方法,它的数据成员中包含一个AESkey,它是用EncryptBlock中的128位随机数生成的(需要知道的是,当用这个随机数初始化完毕AESKey之后,随机数即被作者用PublicKey加密了),生成的算法如下:GenerateAESLogic

 

生成完AESKey之后,加密对象会先将当前加密上下文用AESKey加密了,

vtableAesEncryptData1

紧接着,将文件待加密的文件读到内存中,然后加密文件。

vtableAesEncryptData2

这里可能发现参数不一样了,实际上因为存在两个加密对象,根据是否支持硬件加速选择不同的加密对象,所以IDA这里翻译的有点问题,不用在意,实际上就是一个function。

紧接着会把加密后的文件写入磁盘,完成一个文件的加密工作,剩下的工作有写一些帮助(勒索)文件向用户提示应该如何转账之类的操作,不细说了。下面总结一下Locky的加密逻辑。

 

  1. 准备用户信息,通过用户的磁盘Volume上面的GUID以及系统信息计算一个Hash并作为用户标识。
  2. 上传用户SysInfo,服务器根据上传的资料生成一个PublicKey。
  3. 枚举所有文件,找最新被使用的文件,根据文件名计算Hash,来定位文件。
  4. 构造解密块,Size=0x344,内部有两个常量DWORD,可能用于Locky作者鉴别Locky的Magic以及Version,内部还包含加密文件的原路径,文件大小,文件属性。包含用于鉴别用户的MachineID,同时包含一个128位随机数。
  5. 导入之前从服务器获得的PublicKey,并用解密块中的128位随机数生成一个AESKey,之后立刻用PublicKey加密128位随机数。
  6. 打开文件,用AESKey加密文件。
  7. 用AESKey加密解密块(部分,只加密文件名,文件信息,文件尺寸等信息,头部的用用户MachineID和被RSA加密的随机数部分不在进行二次加密)。
  8. 将被加密的文件写到新文件,新文件的文件名为UserMachineID+FileHash+”.Locky”。
  9. 在新文件的尾部追加解密块,完成整个加密过程。

 

 

解密逻辑,猜测:

  1. 通过UserMachineID检索用户的PrivateKey。
  2. 打开文件,取出解密块头部,用PrivateKey解密?随机数?,用随机数生成AESKey。
  3. 用AESKey解开揭秘块,取出文件原名。
  4. 用AESKey解开文件,并写到新文件,并恢复原名。

 

目前解法就是暴力破解随机数,看起来是不可行的。

 

从solution的角度,有两个点,一个是生成随机数的地方,Hook:CryptGenRandrom,然后替换一个自己的随机数,这样我们可以用自己的随机数推出AESKey,这样就可以解开文件。

 

另外一个办法,Hook:CryptImportKey,将从WebServer返回的PublicKey替换成自己的Key,这样我们可以用自己的PrivateKey解开解密块中的随机值,推出AESKey,也可以解开文件。

 

下面有一组测试用的资料:由于安全原因就不上传了,有兴趣可以@ tedzhang2891@gmail.com与我联系。


原文地址: http://ec2-52-196-167-189.ap-northeast-1.compute.amazonaws.com/wordpress/index.php/2016/08/07/ransomware-locky-analysis/

0 0
原创粉丝点击