前言
国外的Tweet上面的Check Point Research
发布了一篇有趣的推文:
https://research.nccgroup.com/2021/01/23/rift-analysing-a-lazarus-shellcode-execution-method/
在该文章中,攻击者使用了windows api
遍历了大量硬编码UUID
值列表,并且每次都提供指向分配内存(堆)的指针,深入分析后,发现了一种将数据写入堆的方法。
何为UUID
通用唯一识别码(Universally Unique Identifier,缩写:UUID),是用于计算机体系中以识别信息数目的一个128位标识符,根据标准方法生成,不依赖中央机构的注册和分配,UUID具有唯一性。
当然还有一种GUID(全局唯一标识符(英语:Globally Unique Identifier,缩写:GUID)),是一种由算法生成的唯一标识,通常表示成32个16进制数字(0-9,A-F)组成的字符串,如:{21EC2020-3AEA-1069-A2DD-08002B30309D},它实质上是一个128位长的二进制整数,咱们一般所指的UUID实际上是GUID
实际上,uuid 是一种标准, 而guid是uuid的一种实现.,GUID 是微软对UUID这个标准的实现
实现原理
之所以能够利用UUID,和Windows API的两个函数息息相关,UuidFromStringA和EnumSystemLocalesA
首先是UuidFromStringA
函数,它的作用是将字符串转化为UUID

该API需要两个参数:
RPC_CSTR StringUuid,
UUID *Uuid
其实分别对应着指向UUID的字符串表示形式的指针
和以二进制形式返回指向UUID的指针
,注意该API是调用了动态链接库,因此我们在使用过程中也需要进行链接:

再来看EnumSystemLocalesA
函数:

这里的重点在于回调函数,回调函数通过函数指针的方式来进行调用
来看一个例子:
void(*)() #定义一个函数指针,函数返回值为空
((void(*)())exec) #强制类型转换,把exec指针强制转换成函数指针
((void(*)())exec)();#执行入口地址为exec指向的地址的函数
这里我理解的一次就是可以利用该种方式去执行指定指针对应的指定的地址,例如当申请一块堆内存的时候,可以使用该种方式去执行堆内存中的代码
而EnumSystemLocalesA
函数的第一次个参数就是回调函数地址,第二个参数是指定要枚举的语言环境标识符的标志,因此我们同样可以使用:
EnumSystemLocalesA(exec,0);
来实现
因此我们对应的流程图如下:

分为三步:
– 创建并分配堆内存
– 将UUID加密的shellcode写入到堆内存中
– 通过回调函数触发执行shellcode
创建并分配堆内存
HANDLE HeapCreate(
DWORD flOptions,
SIZE_T dwInitialSize,
SIZE_T dwMaximumSize
);
HeapCreate 函数原型如上,在这里第一个参数必须是“HEAP_CREATE_ENABLE_EXECUTE”
,否则当我们试图在来自堆的内存块中执行代码时,系统会抛出EXCEPTION_ACCESS_VIOLATION
异常。其他两个参数设置为0即可,其中dwMaximumSize为0,表示堆内存大小是可增长的,其大小仅受限于系统可用内存大小。
DECLSPEC_ALLOCATOR LPVOID HeapAlloc(
HANDLE hHeap,
DWORD dwFlags,
SIZE_T dwBytes
);
接下来,使用 HeapAlloc 函数在刚创建的堆上分配内存,这里dwBytes
使用1MB即0x100000
为了避免出现Virtualalloc,这里使用Heapalloc,
HeapAlloc
是Windows提供的API,在进程初始化的时候,系统会在进程的地址空间中创建1M大小的堆,称为默认堆,但是在需要大的内存空间时,会自动调用VirtualAlloc函数分配空间,因此我们一般分配1MB空间即可
将shellcode植入堆内存
这里使用了 UuidFromStringA 函数。
RPC_STATUS UuidFromStringA(
RPC_CSTR StringUuid,
UUID *Uuid
);
我们使用UuidFromStringA
将加密后的shellcode
提出来放入上一步分配的堆内存,猜测这里应该并不是通过调用memcpy
来进行内存复制,因为UuidFromStringA
函数是将uuid值转为二进制,二进制则储存在内存当中,因此避免调用memcpy
方式,所以利用 UuidFromStringA
来还原shellcode更利于免杀
感觉上是直接利用赋值(=)来完成内存地址赋值的

触发执行
最后通过回调函数进行触发:
BOOL EnumSystemLocalesA(
LOCALE_ENUMPROCA lpLocaleEnumProc,
DWORD dwFlags
);
来看第一个参数:

这里可以触发回调函数,因此将我们分配好的堆内存放置在第一个参数时会触发执行已经转化完二进制UUID的shellcode
通过callback方式来触发执行shellcode的方式可以参考:
https://www.freebuf.com/articles/web/269158.html
其中列举了如下API都有callback回调函数可以利用:
1, EnumTimeFormatsA
2, EnumWindows
3, EnumDesktopWindows
4, EnumDateFormatsA
5, EnumChildWindows
6, EnumThreadWindows
7, EnumSystemLocales
8, EnumSystemGeoID
9, EnumSystemLanguageGroupsA
10, EnumUILanguagesA
11, EnumSystemCodePagesA
12, EnumDesktopsW
13, EnumSystemCodePagesW
利用
因此我们还需要将shellcode先转化为uuid,这里使用python进行转化比较方便
#coding=utf-8
import uuid
shellcode = b"""shellcode..."""
import uuid
def convertToUUID(shellcode):
# If shellcode is not in multiples of 16, then add some nullbytes at the end
if len(shellcode) % 16 != 0:
print("[-] Shellcode's length not multiplies of 16 bytes")
print("[-] Adding nullbytes at the end of shellcode, this might break your shellcode.")
print("\n[*] Modified shellcode length: ", len(shellcode) + (16 - (len(shellcode) % 16)))
addNullbyte = b"\x00" * (16 - (len(shellcode) % 16))
shellcode += addNullbyte
uuids = []
for i in range(0, len(shellcode), 16):
uuidString = str(uuid.UUID(bytes_le=shellcode[i:i + 16]))
uuids.append(uuidString.replace("'","\""))
return uuids
u = convertToUUID(shellcode)
print(str(u).replace("'","\""))
在这里选取的是EnumTimeFormatsA
函数来进行回调:
BOOL EnumTimeFormatsA(
TIMEFMT_ENUMPROCA lpTimeFmtEnumProc,
LCID Locale,
DWORD dwFlags
);
当使用常规方式加载shellcode时:
((void(*)())ha)();
这是其在VT上的免杀效果情况:

而使用其他Win API函数进行回调触发执行时:

反而在VT上的查杀更多了一点,不过360都不会进行拦截,这里原因推测可能是杀软增加了对Win API的回调函数类的识别,详细原因等待其他师傅们解答
试了几个使用不同的Win API的免杀率如下:

最后可以通过屏蔽控制台应用程序的窗口稍微隐藏一下
#pragma comment(linker, "/subsystem:windows /ENTRY:mainCRTStartup")
上线都是没有问题的

思考
其实在这里感觉能够免杀的主要原因在于没有使用uuidfromStringA
来通过赋值方式复制内存,并且使用UUID将shellcode进行编码的方式绕过了静态
其实为了进一步混淆shellcode或者有更好的免杀效果,我们还可以完全自己实现UuidFromStringA
甚至还可以自定义UUID
的生成规则
比如通过标准的UuidToString
把shellcode转成UUID后,再对转换后的 UUID做一层处理,在https://github.com/StudyCat404/uuid_exec_shellcode
中便是再将UUID进行一次异或加密,通过密钥的方式,解密的时候再与密钥进行一次异或还原:
var status = UuidFromStringA(cast[RPC_CSTR](gkkaekgaEE(decode($UUIDARR[i]), xorpassword)), cast[ptr UUID](hptr))
这样能够将UUID也进行混淆,过静态查杀应该是足够了
还有类似于利用C#将C++的实现重写一遍,通过DInvoke
的方法从dll中加载回调函数的方式,编译完成后也能够增加免杀效果,接下来的工作可能会多从C#层面对实现进行重写
至于原因在慢慢了解