Gotta KEP-tcha ‘Em All - 绕过KEPServerEX中的反调试方法
背景
最近,我的工作重点是在KEPServerEX中发现潜在漏洞。KEPServerEX是工业界领先的连接平台,为所有应用程序提供单一的工业自动化数据源。用户可以通过一个直观的用户界面连接、管理、监控和控制各种自动化设备和软件应用程序。
该软件采用了多种反调试措施,使得发现漏洞和进行模糊测试变得困难。在这方面,我想分享我对这个问题的看法以及绕过这些措施的策略。
反调试和反附加
KEPServerEX带有几个默认服务,如下图所示:
然而,当尝试将调试器附加到server_runtime.exe进程时,它会立即崩溃。
为了确定崩溃的原因,我配置了Windows来收集进程的崩溃转储。您可以在此处了解更多关于如何执行此操作的信息。
收集到的崩溃转储为我提供了堆栈跟踪,帮助确定了导致崩溃的指令。
1
2
3
4
5
6
7
|
(185c.1460): Security check failure or stack buffer overrun - code c0000409 (first/second chance not available)
For analysis of this file, run !analyze -v
eax=00000001 ebx=028af068 ecx=00000007 edx=000001e9 esi=00000003 edi=6bde5a94
eip=76caeddb esp=028aee38 ebp=028aee4c iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
ucrtbase!abort+0x4b:
76caeddb cd29 int 29h
|
通过分析堆栈跟踪,我观察到libserver.dll负责调用退出进程并生成异常。通过检查[1]中提到的地址,确认它属于libserver!sub_10087540函数,如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
void __noreturn sub_10087540()
{
NTSTATUS (__stdcall *v0)(HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG); // esi
HANDLE CurrentProcess; // eax
NTSTATUS (__stdcall *v2)(HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG); // esi
HANDLE v3; // eax
NTSTATUS (__stdcall *v4)(HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG); // esi
HANDLE v5; // eax
int v6; // [esp+10h] [ebp-24h] BYREF
int v7; // [esp+14h] [ebp-20h] BYREF
int ProcessInformation; // [esp+18h] [ebp-1Ch] BYREF
CPPEH_RECORD ms_exc; // [esp+1Ch] [ebp-18h]
v0 = NtQueryInformationProcess;
if ( NtQueryInformationProcess )
{
ProcessInformation = 0;
CurrentProcess = GetCurrentProcess();
if ( !v0(CurrentProcess, ProcessDebugPort, &ProcessInformation, 4, 0) && ProcessInformation )
ExitProcess(1u);
v7 = 0;
v2 = NtQueryInformationProcess;
v3 = GetCurrentProcess();
if ( !v2(v3, ProcessDebugObjectHandle, &v7, 4, 0) && v7 )
ExitProcess(2u);
v6 = 1;
v4 = NtQueryInformationProcess;
v5 = GetCurrentProcess();
if ( !v4(v5, ProcessDebugFlags, &v6, 4, 0) && !v6 )
ExitProcess(3u);
}
ms_exc.registration.TryLevel = 0;
__asm { int 2Dh; Windows NT - debugging services: eax = type }
ExitProcess(4u);
}
|
初步检查显示,采用了以下方法:
- 使用ntdll!NtQueryInformationProcess函数,使用ProcessInformationClass ProcessDebugPort、ProcessDebugObjectHandle、ProcessDebugFlags进行反调试
- 实现int 2D指令来检查调试器是否附加到当前进程
第一次绕过尝试
为了修复这个函数,我计划通过移除底部检查来简单地修补它。
我开始用xor和ret修补libserver!sub_10087540函数的开头。
1
2
3
4
5
6
7
8
|
.text:10087540 sub_10087540 proc near ; CODE XREF: sub_100836A0+27↑p
.text:10087540 ; sub_10084080+45↑p ...
.text:10087540 xor eax, eax // patch
.text:10087542 retn
.text:10087542 sub_10087540 endp
.text:10087542 ; ---------------------------------------------------------------------------
.text:10087543 db 6Ah ; j
.text:10087544 db 0FEh
|
文件签名检查
修补libserver.dll后,我尝试重新启动KEPServerEX服务运行时。不幸的是,进程无法启动,似乎检查了某些内容并立即退出。
为了获取更多信息,我使用Procmon观察进程启动时的行为。
基于进程在更新libserver.dll后无法启动的事实,很可能是在检查文件的哈希值。
为了进一步调查,我将进程名称过滤为server_runtime.exe,并将操作设置为ReadFile。这使我能够观察到进程正在读取安装目录中的各种dll和exe文件。
通过检查事件,我看到了堆栈跟踪,表明进程正在使用WinVerifyTrust来检查安装目录中的文件。
具体来说,我分析了server_runtime.exe + 0x34101处的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
int __thiscall sub_33000(int this, int a2)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
v37 = a2;
pCertContext = 0;
p_pCertContext = &pCertContext;
v31 = 0;
if ( *(_DWORD *)(this + 20) || (v3 = sub_333E0(), v3 >= 0) )
{
v33.pcwszFilePath = *(LPCWSTR *)(this + 4);
pWVTData.pFile = &v33;
v33.cbStruct = 16;
v33.hFile = 0;
v33.pgKnownSubject = 0;
pgActionID.Data1 = 0xAAC56B;
*(_DWORD *)&pgActionID.Data2 = 0x11D0CD44;
*(_DWORD *)pgActionID.Data4 = 0xC000C28C;
*(_DWORD *)&pgActionID.Data4[4] = 0xEE95C24F;
v35 = 0;
pWVTData.cbStruct = 52;
pWVTData.pPolicyCallbackData = 0;
pWVTData.pSIPClientData = 0;
pWVTData.dwUIChoice = 2;
pWVTData.fdwRevocationChecks = 0;
pWVTData.dwUnionChoice = 1;
pWVTData.dwStateAction = WTD_STATEACTION_VERIFY;
pWVTData.hWVTStateData = 0;
pWVTData.pwszURLReference = 0;
pWVTData.dwProvFlags = 16;
pWVTData.dwUIContext = 0;
v4 = (kepplatform *)WinVerifyTrust(0, &pgActionID, &pWVTData); // [2]
v3 = (int)v4;
v36 = this + 8;
if ( (int)v4 < 0 )
{
v5 = kepplatform::XlatErrorCode(v4);
v6 = ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(
v40,
v5);
ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::operator=(this + 8, v6);
LOBYTE(v31) = 0;
CMFCRibbonInfo::XID::~XID(v40);
pWVTData.dwStateAction = WTD_STATEACTION_CLOSE;
WinVerifyTrust(0xFFFFFFFFh, &pgActionID, &pWVTData);
goto done;
}
pWVTData.dwStateAction = WTD_STATEACTION_CLOSE;
WinVerifyTrust(0xFFFFFFFFh, &pgActionID, &pWVTData);
...
...
}
...
...
}
|
检查提供的代码后,很明显在[2]处调用了WinVerifyTrust函数并检查返回值。根据微软的说法,如果信任提供者验证主题对指定操作是可信的,则返回值为零。除了零之外,不应将任何其他值视为成功返回。因此,将WinVerifyTrust的返回值修改为零足以绕过签名检查并继续执行预期操作。
第二次绕过尝试
观察ASM代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
.text:000330A8 mov [ebp+70h+var_2C], 0
.text:000330AF mov [ebp+70h+pWVTData.cbStruct], 34h ; '4'
.text:000330B6 mov [ebp+70h+pWVTData.pPolicyCallbackData], 0
.text:000330BD mov [ebp+70h+pWVTData.pSIPClientData], 0
.text:000330C4 mov [ebp+70h+pWVTData.dwUIChoice], 2
.text:000330CB mov [ebp+70h+pWVTData.fdwRevocationChecks], 0
.text:000330D2 mov [ebp+70h+pWVTData.dwUnionChoice], 1
.text:000330D9 mov [ebp+70h+pWVTData.dwStateAction], 1
.text:000330E0 mov [ebp+70h+pWVTData.hWVTStateData], 0
.text:000330E7 mov [ebp+70h+pWVTData.pwszURLReference], 0
.text:000330EE mov [ebp+70h+pWVTData.dwProvFlags], 10h
.text:000330F5 mov [ebp+70h+pWVTData.dwUIContext], 0
.text:000330FC call WinVerifyTrust
.text:00033101 mov esi, eax // [3] =>> patch xor esi, esi
.text:00033103 lea edi, [ebx+8]
.text:00033106 mov [ebp+70h+var_28], edi
.text:00033109 test esi, esi
.text:0003310B jns short loc_33156
|
执行WinVerifyTrust后,eax的结果值随后在[3]处移动到esi,然后在esi寄存器上进行验证。
检查Procmon中记录的事件后,发现libsecure.dll利用WinVerifyTrust函数验证安装目录中文件的签名。此外,我按照前面描述的方法实施了补丁。
隐藏调试器
成功附加到server_runtime.exe进程后,我尝试设置断点。然而,当达到断点时,程序立即崩溃。检查崩溃后,得到的堆栈跟踪如下:
1
2
3
4
5
|
eax=02dfa770 ebx=02dfa74c ecx=71c83004 edx=0237f8fc esi=02dfa7c4 edi=02dfa74c
eip=71bbbc00 esp=0237f8e8 ebp=0237f97c iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
libua!kepua::nodeids::NodeHash::operator=+0xa0:
71bbbc00 cc int 3
|
通过在71bbc00处设置断点,调试器无法接收断点异常并无法处理它。结果,异常未被处理,如果进程无法处理它,整个应用程序将终止。有关此问题的更多详细信息,请参阅此资源。
本质上,它将遍历目标进程中的所有线程,并在存在的地方设置hidefromdebugger标志。
KEPServerEX在初始化线程时使用libthread.dll中的函数。函数libthread!kepthread::CKEPThread::Start初始化线程并调用函数libthread!sub_10003960。此函数通过函数ntdll!NtSetInformationThread设置标志THREAD_INFORMATION_CLASS::ThreadHideFromDebugger (0x11)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
bool __cdecl sub_10003960(int a1)
{
HMODULE ModuleHandleW; // eax
if ( dword_100091E0 <= *(_DWORD *)(*((_DWORD *)NtCurrentTeb()->ThreadLocalStoragePointer + TlsIndex) + 4) )
return NtSetInformationThread && !NtSetInformationThread(a1, 0x11, 0, 0);
_Init_thread_header(&dword_100091E0);
if ( dword_100091E0 != -1 )
return NtSetInformationThread && !NtSetInformationThread(a1, 0x11, 0, 0);
ModuleHandleW = GetModuleHandleW(L"ntdll.dll");
NtSetInformationThread = (int (__stdcall *)(_DWORD, _DWORD, _DWORD, _DWORD))GetProcAddress(
ModuleHandleW,
"NtSetInformationThread");
_Init_thread_footer(&dword_100091E0);
return NtSetInformationThread && !NtSetInformationThread(a1, 0x11, 0, 0);
}
|
第三次绕过尝试
绕过此技术的方法与前面提到的反调试绕过类似。我们只需要在libthread!sub_10003960函数的开头进行补丁。
1
2
3
4
5
6
7
8
9
10
|
.text:10003960
.text:10003960 loc_10003960: ; CODE XREF: sub_10001010+7↑p
.text:10003960 ; kepthread::CKEPThread::Start(bool)+CD↑p
.text:10003960 ; __unwind { // SEH_10003960
.text:10003960 xor eax, eax // patch
.text:10003962 retn
.text:10003963 ; ---------------------------------------------------------------------------
.text:10003963 push 0FFFFFFFFh
.text:10003965 push offset SEH_10003960
.text:1000396A mov eax, large fs:0
|
完成后,我们可以轻松附加和调试KEPServerEX进程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
(18f8.1210): Break instruction exception - code 80000003 (first chance)
eax=00e41000 ebx=00000000 ecx=7775db60 edx=7775db60 esi=7775db60 edi=7775db60
eip=77724ce0 esp=0382fefc ebp=0382ff28 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
ntdll!DbgBreakPoint:
77724ce0 cc int 3
0:037> bp libua+5BC00
0:037> g
Breakpoint 0 hit
eax=7a9277e0 ebx=7a9277bc ecx=7ad73004 edx=0b17f518 esi=7a927834 edi=7a9277bc
eip=7acabc00 esp=0b17f504 ebp=0b17f598 iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
libua!kepua::nodeids::NodeHash::operator=+0xa0:
7acabc00 55 push ebp
|
代码补丁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import argparse
import os
#offset for KEPServerEX6-6.12.361.0
def patch(_dir):
server_runtime = os.path.join(_dir, "server_runtime.exe")
libsecure = os.path.join(_dir, "libsecure.dll")
libserver = os.path.join(_dir, "libserver.dll")
libthread = os.path.join(_dir, "libthread.dll")
#
offset_server_runtime = 0x34101 - 0x1000+0x400 #xor esi, esi
offset_libsecure = 0x16A41 - 0x1000+0x400 #xor esi, esi
offset_libserver = 0x87540 - 0x1000+0x400 #xor eax,eax ret
offset_libthread = 0x3960 - 0x1000+0x400 #xor eax,eax ret
|