在Guloader加载器代码的开头,第一层shellcode解密完成后,接着会对比DJB2算法得到的哈希来获取特定的API函数。
如下对比特定hash来导入API函数:
F0D2CE31 == ntdll_12.LdrLoadDll
0C79894C == ntdll_12.ZwGetContextThread
E2C26F81 == ntdll_12.RtlAddVectoredExceptionHandler
Guloader加载器使用ZwGetContextThread是用来实现反调试,对于RtlAddVectoredExceptionHandler函数则是一方面用于反调试,另一方面也用于干扰动静态分析。Guloader下载器在最近几年公开的分析报告中并未提及到RtlAddVectoredExceptionHandler函数反调试干扰分析的细节,因此这里简单记录一下。
通过动态调试发现会注册向量异常处理程序(VEH)作用是反调试,输入的参数为1表明一旦发生异常则被第一个处理。
函数定义如下:
PVOID AddVectoredExceptionHandler(
ULONG First,
PVECTORED_EXCEPTION_HANDLER Handler
);
参数说明:
First:应调用处理程序的顺序。如果参数不为零,则处理程序是第一个被调用的处理程序。如果参数为零,则处理程序是最后一个被调用的处理程序。
Handler:指向要调用的处理程序的指针。有关详细信息,请参阅VectoredHandler。
返回值:如果函数成功,则返回值是异常处理程序的句柄。如果函数失败,则返回值为NULL。
通过查询MSDN找到如下VectoredHandler结构体信息,后续会利用这些结构体来理解shellcode实现干扰分析的原理。
PVECTORED_EXCEPTION_HANDLER PvectoredExceptionHandler;
LONG PvectoredExceptionHandler(
[in] _EXCEPTION_POINTERS *ExceptionInfo
)
{...}
参数
[in] ExceptionInfo
指向接收异常记录的EXCEPTION_POINTERS结构的指针
EXCEPTION_POINTERS结构体如下:
typedef struct _EXCEPTION_POINTERS {
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
成员
ExceptionRecord
指向 包含与机器无关的异常描述的EXCEPTION_RECORD结构的指针。
ContextRecord
指向 CONTEXT结构的指针,该结构包含异常时处理器状态的特定于处理器的描述。
EXCEPTION_RECORD结构体如下:
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;
在32位环境中,CONTEXT结构体定义如下。
#define MAXIMUM_SUPPORTED_EXTENSION 512
typedef struct _CONTEXT {
//
// The flags values within this flag control the contents of
// a CONTEXT record.
//
// If the context record is used as an input parameter, then
// for each portion of the context record controlled by a flag
// whose value is set, it is assumed that that portion of the
// context record contains valid context. If the context record
// is being used to modify a threads context, then only that
// portion of the threads context will be modified.
//
// If the context record is used as an IN OUT parameter to capture
// the context of a thread, then only those portions of the thread's
// context corresponding to set flags will be returned.
//
// The context record is never used as an OUT only parameter.
//
DWORD ContextFlags;
//
// This section is specified/returned if CONTEXT_DEBUG_REGISTERS is
// set in ContextFlags. Note that CONTEXT_DEBUG_REGISTERS is NOT
// included in CONTEXT_FULL.
//
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_FLOATING_POINT.
//
FLOATING_SAVE_AREA FloatSave;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_SEGMENTS.
//
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_INTEGER.
//
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_CONTROL.
//
DWORD Ebp;
DWORD Eip;
DWORD SegCs; // MUST BE SANITIZED
DWORD EFlags; // MUST BE SANITIZED
DWORD Esp;
DWORD SegSs;
//
// This section is specified/returned if the ContextFlags word
// contains the flag CONTEXT_EXTENDED_REGISTERS.
// The format and contexts are processor specific
//
BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;
地址1637C3作为异常处理函数入口(也就是PVECTORED_EXCEPTION_HANDLER Handler函数),如下。
0x80000003属于BreakPoint异常,不用做修改,shellcode执行逻辑会进行修改EIP值作跳转。
第二次生成EXCEPTION_SINGLE_STEP异常(代码0x80000004),如果不做修改,shellcode会直接退出表明存在调试行为。
该shellcode注册的Handler函数地址基于shellcode代码开头偏移为137C3h,主要作用是引导后续的执行流,当执行到shellcode代码里内嵌的int 3(CC)指令时,发生BreakPoint异常,于是异常处理程序来到地址1637C3入口(也就是PVECTORED_EXCEPTION_HANDLER Handler函数),在这个函数中会修改计算得到异常结束后下一次执行最终的EIP值进行跳转执行。得到最终EIP值的实现算法为取shellcode内嵌的int 3(CC)指令的下一个地址的字节值A,将A与0xE5进行异或得到B,将B与当前int 3(CC)指令的当前地址进行累加得到C,将C赋值给_EXCEPTION_POINTERS->ContextRecord->Eip后,等异常处理完毕将恢复状态时,EIP就会恢复为上述保存的值,从而执行时发生跳转,shellcode利用RtlAddVectoredExceptionHandler注册的Handler函数地址实现的逻辑如下。
因为后续在调试shellcode的过程中大量的遇到int3中断导致的异常来干扰调试分析,因此有必要需要进行修复。首先需要经过逆向来理解shellcode利用int3中断跳转执行干扰分析的原理。
举个例子,目前提取的部分shellcode的代码数据如下,当执行流执行到000A2121地址会产生BreakPoint异常。
seg000:000A2121 CC
seg000:000A2122 ED
seg000:000A2123 BC
......
seg000:000A2129 51
之后经过对1637C3入口(也就是PVECTORED_EXCEPTION_HANDLER Handler函数)的逻辑进行逆向,得到伪代码实现如下:
例子1如下:
ED ^ E5 = 8h (CC的下一个地址的数据ED与0xE5进行异或得到8h)
000A2121 + 8h = 000A2129(将发生中断的地址加上8h得到最终执行的地址,也就是EIP会被替换的值)
000A2123 + offset = 000A2129(这里为了利用jmp指令进行二进制补丁,我们可以在发生中断的地址偏移+2处经过计算得到EB操作码后续的偏移值offset)
EB offset == EB 06(因为直接修改EIP的过程类似跳转执行,所以对其补丁的时候可以转换为jmp指令的操作码,只需在中断的位置修改两个字节数据即可,可以对shellcode进行补丁来适当修复)
例子2如下(原理如上):
F6 ^ E5 = 13
000A212A + 13 = 000A213D
(000A212A + 2) + offset = 000A213D
EB offset == EB 11
因此我们可以编写IDAPython脚本进行补丁修复或者直接对shellcode二进制数据进行修复,在修复之前我们需要获取到shellcode执行过程中出现int3指令的地址,这个算是一个难点,因为没有完全获取到所有int3中断时的地址,所以只能是部分修复,修复后对于后续的反调试与反虚拟机机制调试起来也就比较容易了。
目前想到的是可以利用ollydbg的条件记录断点功能,在获取中断地址的逻辑处设置断点,一旦命中就进行记录,之后在日志记录窗口获取到所有int3中断时所在的地址,将地址保存后就可以本地利用Python脚本对其进行二进制补丁。直接利用静态反汇编得到的结果来获取int3中断时的地址是可以的,但是无法覆盖大部分地址,因为静态反汇编会存在混乱。如果是要利用模拟执行框架来获取int3中断时的地址,感觉意义不大,因为内嵌int3中断(CC)很多,掺杂在反调试与反虚拟机机制中。
简单实现二进制补丁的脚本如下:
def main():
list_offset = []
with open("od_bp_log.txt", 'r', encoding="utf-8") as f: # from ollydbg disam
diasm = f.readlines()
for l in diasm:
line = l.strip()
if "int3" in line:
print(line)
print(type(line[2:8]))
# offset = int(line[2:8], 16) - 0x110000
print(line[-6:])
offset = int(line[-6:], 16) - 0x110000
list_offset.append(offset)
with open("decode_shellcode_2", 'rb') as f:
data = f.read()
shellcode = bytearray(data)
for n in list_offset:
int3_next = shellcode[n+1]
# value = int.from_bytes(int3_next, byteorder='litten', signed=False)
print(int3_next)
print(type(int3_next))
jmp_value = (int3_next ^ 229) - 2
print(str(jmp_value) + " " + str(type(jmp_value)))
# code = jmp_value.encode('utf-8')
# print(type(code))
shellcode[n] = 0xEB
shellcode[n + 1] = jmp_value
# end_vaule = n + (int3_next ^ 229)
# a = n + 2
# for a in range(a, end_vaule):
# shellcode[a] = 0x90
with open("decode_shellcode_3", 'wb') as fw:
fw.write(shellcode)
if __name__ == '__main__':
try:
main()
except Exception as e:
print('Exception', e)