全面绕过KEPServerEX反调试技术:从代码补丁到调试器隐藏

本文详细分析了KEPServerEX的反调试机制,包括进程调试端口检查、文件签名验证和线程隐藏调试器技术,并提供了具体的代码补丁方法和Python实现脚本。

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指令来检查调试器是否附加到当前进程

第一次绕过尝试

为了修复这个函数,我计划通过移除底部检查来简单地修补它。

1
2
xor eax, eax 
ret

我开始用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

   
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计