2022CTF培训(三)windows&linux&安卓平台调试机制原理

时间:2022-11-27 22:52:24

附件下载链接

windows平台调试机制原理

手动编写一个简易调试器

创建待调试进程

使用 CreateProcess 函数创建待调试进程,创建时指定 dwCreationFlags 参数为 DEBUG_ONLY_THIS_PROCESS 将会告诉操作系统我们需要让当前调用者(线程)接管所有子进程的调试事件,包括进程创建、进程退出、线程创建、线程退出以及最重要的运行时异常等等。

STARTUPINFO si = { 0 };
PROCESS_INFORMATION pi = { 0 };
void CreateDebuggee() {

	ZeroMemory(&si, sizeof(si));
	si.cb = sizeof(si);
	ZeroMemory(&pi, sizeof(pi));
	CreateProcess(ProcessNameToDebug, NULL, NULL, NULL, FALSE,
		DEBUG_ONLY_THIS_PROCESS, NULL, NULL, &si, &pi
	);
	/*
		specifying DEBUG_ONLY_THIS_PROCESS as the sixth parameter (dwCreationFlags).
		With this flag, we are asking the Windows OS to communicate this thread for all debugging events,
		including process creation/termination, thread creation/termination, runtime exceptions, and so on.
	*/
}

Debugger Loop

Debugger Loop(调试器循环)是所有调试器最核心的部分,它主要围绕 WaitForDebugEvent API 进行工作,WaitForDebugEvent 接受两个参数,一个参数为指向 DEBUG_EVENT 的指针,另一个参数为 timeout(我们一般指定为 INFINITE),只需要包含 Windows 相关头文件即可使用这个 API,它的结构如下所示:

BOOL WaitForDebugEvent(DEBUG_EVENT* lpDebugEvent, DWORD dwMilliseconds);

其中,DEBUG_EVENT 结构包括调试中产生的所有事件的信息,它的定义如下:

typedef struct _DEBUG_EVENT {
    DWORD dwDebugEventCode;
    DWORD dwProcessId; // 进程 pid
    DWORD dwThreadId;  // 线程 tid
    union {
        EXCEPTION_DEBUG_INFO Exception;
        CREATE_THREAD_DEBUG_INFO CreateThread;
        CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
        EXIT_THREAD_DEBUG_INFO ExitThread;
        EXIT_PROCESS_DEBUG_INFO ExitProcess;
        LOAD_DLL_DEBUG_INFO LoadDll;
        UNLOAD_DLL_DEBUG_INFO UnloadDll;
        OUTPUT_DEBUG_STRING_INFO DebugString;
        RIP_INFO RipInfo;
    } u;
} DEBUG_EVENT, *LPDEBUG_EVENT;

一旦 WaitForDebugEvent 函数返回(它是阻塞的,没有消息的情况下它是不会返回的),我们就需要去使用自己定义的函数对异常进行处理,处理完毕之后,我们需要调用 ContinueDebugEvent API,告诉操作系统我们准备好接受下一个 DebugEvent,我们编写一个简易的 DebugLoop 如下:

for (;;)
	{
		if (!WaitForDebugEvent(&debug_event, INFINITE))
			return 0;
		DWORD dbg_status;
		ProcessDebugEvent(&debug_event, &dbg_status);  // User-defined function, not API
		ContinueDebugEvent(
			debug_event.dwProcessId,
			debug_event.dwThreadId,
			dbg_status
		);
	}

其中,我们调用 ContinueDebugEvent 传递的 dbg_status 由我们自己的 ProcessDebugEvent 函数进行设置,dwProcessId 和 dwThreadId 指定进程以及线程,这个参数我们可以直接沿用 WaitForDebugEvent 返回的结果。

处理调试事件

Windows 中主要有 9 种调试事件,在异常事件的类别下有多达 20 种不同的异常事件类型。

结合之前我们讨论过的 DEBUG_EVENT 结构体的定义,WaitForDebugEvent 在成功返回之后会将 DEBUG_EVENT 结构体中的数据填充好,dwDebugEventCode 指定的是调试事件的类型,根据类型的不同,联合类型 u 包括事件的具体信息,我们应该只使用 union 中相应类型的信息(例如如果发生的调试事件类型为 OUTPUT_DEBUG_STRING_EVENT,则我们应该使用 union 中的 OUTPUT_DEBUG_STRING_INFO。

编写自己的 ProcessDebugEvent 函数,这个函数的基本框架如下:

void ProcessDebugEvent(DEBUG_EVENT* dbg_event, DWORD* dbg_status) {
    switch (dbg_event->dwDebugEventCode) {
        case EXIT_THREAD_DEBUG_EVENT:
        {
            printf("The thread %d exited with code: %d\n",
                dbg_event->dwThreadId,
                dbg_event->u.ExitThread.dwExitCode
            );
            *dbg_status = DBG_CONTINUE;
            break;
        }
        case CREATE_THREAD_DEBUG_EVENT:
        {
            printf("Thread 0x%x (Id: %d) created at: 0x%x\n",
                dbg_event->u.CreateThread.hThread,
                dbg_event->dwThreadId,
                dbg_event->u.CreateThread.lpStartAddress
            );
            *dbg_status = DBG_CONTINUE;
            break;
        }
    }    
}

上述代码将会处理两种调试消息:

  • EXIT_THREAD_DEBUG_EVENT,有线程退出
  • CREATE_THREAD_DEBUG_EVENT,有线程创建

运行程序,我们会发现我们的调试器正常接管到了子进程新线程创建的消息,退出子程序,我们能够接管到子进程线程退出的消息。

实现断点

我们通过 INT3 异常实现一个断点,在调试器中我们编写如下函数:

BYTE Bp(LPVOID BpAddr) {
	BYTE INT3 = 0xcc;
	BYTE ORG = 0;
	ReadProcessMemory(pi.hProcess, BpAddr, &ORG, sizeof(BYTE), NULL);
	WriteProcessMemory(pi.hProcess, BpAddr, &INT3, sizeof(BYTE), NULL);
	FlushInstructionCache(pi.hProcess, BpAddr, 1);
	return ORG;
}

这个函数将会对当前正在调试的进程的 BpAddr 位置设置一个 INT3 断点,注意,在使用 WriteProcessMemory 函数进行内存写入操作之后,如果写入的是目标程序的代码,则需要进行 FlushInstructionCache 操作来刷新指令缓存。

随后我们需要在自己的 ProcessDebugEvent 函数中实现对 INT3 异常的接管:

case EXCEPTION_DEBUG_EVENT: // 异常消息
	EXCEPTION_DEBUG_INFO& exception = dbg_event->u.Exception;
		PVOID& addr = exception.ExceptionRecord.ExceptionAddress;
		switch (exception.ExceptionRecord.ExceptionCode) {
			case STATUS_BREAKPOINT:  // Same value as EXCEPTION_BREAKPOINT
                // 在此处响应
                break;
            default:
        }
}

如果我们需要继续运行程序,则需要首先恢复未设置断点的状态,然后将 Eip 减一(因为调试器接到的线程上下文中,Eip 的值是发生异常的位置 +1 的值),并通知操作系统继续运行程序。

用来恢复断点的函数如下:

void Recover(LPVOID Addr, BYTE data) {
	WriteProcessMemory(pi.hProcess, Addr, &data, sizeof(BYTE), NULL);
	FlushInstructionCache(pi.hProcess, Addr, 1);
}

第二个参数使用我们 Bp 函数返回的值即可。

使用调试器完成 CTF VM 题的爆破

观察程序

main 函数如下,输入的数据对应的字符串的指针作为数据的一部分传给 VM 函数,执行完 VM 函数后将 输入的字符串 input 与 dst 比较。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v3; // ecx
  int data[162]; // [esp+0h] [ebp-2CCh] BYREF
  char input[32]; // [esp+288h] [ebp-44h] BYREF
  unsigned __int8 op_code[32]; // [esp+2A8h] [ebp-24h] BYREF

  puts("Input your flag: \n");
  memset(input, 0, sizeof(input));
  scanf("%s", input);
  *(_DWORD *)op_code = 0x3040500;               // [0, 5, 4, 3, 15, 5, 14, 5, 12, 2, 3, 1, 8, 6, 5, 1, 9, 8, 5, 10, 3, 1, 8, 7, 1, 5, 8, 2, 1, 5, 11, 13]
  *(_DWORD *)&op_code[4] = 0x50E050F;
  *(_DWORD *)&op_code[8] = 16974348;
  *(_DWORD *)&op_code[12] = 17106440;
  *(_DWORD *)&op_code[16] = 168101897;
  *(_DWORD *)&op_code[20] = 117965059;
  *(_DWORD *)&op_code[24] = 34080001;
  *(_DWORD *)&op_code[28] = 218825985;
  data[0] = (int)input;
  data[1] = 0;
  data[2] = 22;
  data[3] = 23;
  data[4] = 204;
  data[5] = 1;
  data[6] = -25;
  data[7] = 22;
  data[8] = 23;
  data[9] = 204;
  data[10] = 1;
  data[11] = -25;
  data[12] = 22;
  data[13] = 23;
  data[14] = 204;
  data[15] = 1;
  data[16] = -25;
  data[17] = 22;
  data[18] = 23;
  data[19] = 204;
  data[20] = 1;
  data[21] = -25;
  data[22] = 22;
  data[23] = 23;
  data[24] = 204;
  data[25] = 1;
  data[26] = -25;
  data[27] = 22;
  data[28] = 23;
  data[29] = 204;
  data[30] = 1;
  data[31] = -25;
  data[32] = 22;
  data[33] = 23;
  data[34] = 204;
  data[35] = 1;
  data[36] = -25;
  data[37] = 22;
  data[38] = 23;
  data[39] = 204;
  data[40] = 1;
  data[41] = -25;
  data[42] = 22;
  data[43] = 23;
  data[44] = 204;
  data[45] = 1;
  data[46] = -25;
  data[47] = 22;
  data[48] = 23;
  data[49] = 204;
  data[50] = 1;
  data[51] = -25;
  data[52] = 22;
  data[53] = 23;
  data[54] = 204;
  data[55] = 1;
  data[56] = -25;
  data[57] = 22;
  data[58] = 23;
  data[59] = 204;
  data[60] = 1;
  data[61] = -25;
  data[62] = 22;
  data[63] = 23;
  data[64] = 204;
  data[65] = 1;
  data[66] = -25;
  data[67] = 22;
  data[68] = 23;
  data[69] = 204;
  data[70] = 1;
  data[71] = -25;
  data[72] = 22;
  data[73] = 23;
  data[74] = 204;
  data[75] = 1;
  data[76] = -25;
  data[77] = 22;
  data[78] = 23;
  data[79] = 204;
  data[80] = 1;
  data[81] = -25;
  data[82] = 22;
  data[83] = 23;
  data[84] = 204;
  data[85] = 1;
  data[86] = -25;
  data[87] = 22;
  data[88] = 23;
  data[89] = 204;
  data[90] = 1;
  data[91] = -25;
  data[92] = 22;
  data[93] = 23;
  data[94] = 204;
  data[95] = 1;
  data[96] = -25;
  data[97] = 22;
  data[98] = 23;
  data[99] = 204;
  data[100] = 1;
  data[101] = -25;
  data[102] = 22;
  data[103] = 23;
  data[104] = 204;
  data[105] = 1;
  data[106] = -25;
  data[107] = 22;
  data[108] = 23;
  data[109] = 204;
  data[110] = 1;
  data[111] = -25;
  data[112] = 22;
  data[113] = 23;
  data[114] = 204;
  data[115] = 1;
  data[116] = -25;
  data[117] = 22;
  data[118] = 23;
  data[119] = 204;
  data[120] = 1;
  data[121] = -25;
  data[122] = 22;
  data[123] = 23;
  data[124] = 204;
  data[125] = 1;
  data[126] = -25;
  data[127] = 22;
  data[128] = 23;
  data[129] = 204;
  data[130] = 1;
  data[131] = -25;
  data[132] = 22;
  data[133] = 23;
  data[134] = 204;
  data[135] = 1;
  data[136] = -25;
  data[137] = 22;
  data[138] = 23;
  data[139] = 204;
  data[140] = 1;
  data[141] = -25;
  data[142] = 22;
  data[143] = 23;
  data[144] = 204;
  data[145] = 1;
  data[146] = -25;
  data[147] = 22;
  data[148] = 23;
  data[149] = 204;
  data[150] = 1;
  data[151] = -25;
  data[152] = 22;
  data[153] = 23;
  data[154] = 204;
  data[155] = 1;
  data[156] = -25;
  data[157] = 22;
  data[158] = 23;
  data[159] = 204;
  data[160] = 1;
  data[161] = -25;
  VM(op_code, data);
  v3 = 0;
  while ( input[v3] == dst[v3] )
  {
    if ( ++v3 >= 24 )
      goto LABEL_6;
  }
  puts("Never Give Up\n");
LABEL_6:
  system("pause");
  return 0;
}

VM 函数大致分析如下,这个虚拟机主要有一个栈+栈顶指针,两个通用寄存器和一个标志寄存器组成。
从对 VM 函数的分析来看,输入 input 的在 VM 中是以字符串指针的形式存放在虚拟机的栈中。

int __fastcall VM(unsigned __int8 *op_code, int *data)
{
  Info *info; // edi
  DWORD *stack; // eax
  DWORD reg2; // edx
  BOOL reg3; // esi
  int v7; // eax
  int esp; // ecx
  int v9; // ecx
  DWORD *v10; // eax
  int v11; // edx
  int v12; // eax
  DWORD *v13; // ecx
  DWORD v14; // edx
  int v15; // eax
  _DWORD *v16; // ecx
  int v17; // edx
  int v18; // eax
  DWORD *v19; // ecx
  DWORD v20; // edx
  int v21; // ecx
  DWORD v22; // edx
  int v23; // ecx
  DWORD v24; // edx
  int v25; // eax
  DWORD *v26; // edx
  const char *v27; // ecx
  DWORD *v28; // ecx
  int v29; // eax
  DWORD *v30; // edx
  unsigned __int8 *v31; // ecx
  DWORD v32; // ecx
  int v33; // eax
  DWORD *v34; // ecx
  _BYTE *v35; // esi
  DWORD v37; // [esp+Ch] [ebp-1Ch]
  DWORD reg1; // [esp+14h] [ebp-14h]
  BOOL v40; // [esp+1Ch] [ebp-Ch]
  DWORD v41; // [esp+20h] [ebp-8h]

  info = (Info *)malloc(0xCu);
  if ( !info )
    printf((char)"Create Stack Malloc Fail CODE 1");
  stack = (DWORD *)malloc(0x100u);
  info->stack = stack;
  if ( !stack )
    printf((char)"Create Stack Malloc Fail CODE 2");
  reg2 = 0;
  info->field_0 = 64;                           // ?没有用到
  reg3 = 0;
  info->_esp = 0;
  v41 = 0;
  reg1 = 0;
  v40 = 0;
  while ( op_code )
  {
    v7 = *++op_code;
    switch ( v7 )
    {
      case 1:                                   // 将 reg2 push 到栈顶
        info->stack[++info->_esp] = reg2;
        break;
      case 2:                                   // 将栈顶数据 pop 到 reg2 中
        esp = info->_esp;
        reg2 = info->stack[esp];
        v41 = reg2;
        info->_esp = esp - 1;
        break;
      case 3:                                   // 将 reg1 push 到栈顶
        info->stack[++info->_esp] = reg1;
        goto LABEL_22;
      case 4:                                   // 将栈顶数据 pop 到 reg1 中
        v9 = info->_esp;
        reg1 = info->stack[v9];
        info->_esp = v9 - 1;
        break;
      case 5:                                   // 从 data 中取一个值 push 到栈顶
        v10 = info->stack;
        v11 = *data;
        ++info->_esp;
        ++data;
        v10[info->_esp] = v11;
        goto LABEL_22;
      case 6:                                   // 取栈顶的存放的地址指向的位置的一个字节放在栈顶(应该只能针对 input 字符串操作)
        v29 = info->_esp;
        v30 = info->stack;
        v31 = (unsigned __int8 *)v30[v29--];
        info->_esp = v29++;
        v32 = *v31;
        info->_esp = v29;
        v30[v29] = v32;
        goto LABEL_22;
      case 7:                                   // 从栈中弹出一个字符串指针和一个值,然后将字符串指针指向的位置赋值为该值
        v33 = info->_esp;
        v34 = info->stack;
        v35 = (_BYTE *)v34[v33];
        info->_esp = v33 - 1;
        v37 = v34[v33 - 1];
        info->_esp = v33 - 2;
        *v35 = v37;
        goto LABEL_21;
      case 8:                                   // 弹出栈顶的值,然后将这个值加到新的栈顶,然后判断栈顶是否为 0 ,结果写到 reg3 中
        v12 = info->_esp;
        v13 = info->stack;
        v14 = v13[v12--];
        info->_esp = v12;
        v13[v12] += v14;
        goto LABEL_11;
      case 9:                                   // pop 栈顶的值后然后将新的栈顶的值减去原来栈顶的值
        v15 = info->_esp;
        v16 = info->stack;
        v17 = v16[v15--];
        info->_esp = v15;
        v16[v15] -= v17;
LABEL_11:                                       // 判断栈顶是否为 0 ,结果存到 reg3 中
        v40 = info->stack[info->_esp] == 0;
        goto LABEL_21;
      case 10:                                  // pop 栈顶的值后然后将新的栈顶的值异或原来栈顶的值
        v18 = info->_esp;
        v19 = info->stack;
        v20 = v19[v18--];
        info->_esp = v18;
        v19[v18] ^= v20;
        goto LABEL_22;
      case 11:                                  // 将 opcode 的位置加上栈顶 pop 出的值
        v21 = info->_esp;
        v22 = info->stack[v21];
        info->_esp = v21 - 1;
        op_code += v22;
        goto LABEL_22;
      case 12:                                  // 弹出栈顶的值,根据 reg3 判断是否将 opcode 加上这个值
        v23 = info->_esp;
        v24 = info->stack[v23];
        info->_esp = v23 - 1;
        if ( reg3 )
          op_code += v24;
        goto LABEL_22;
      case 13:                                  // 释放栈
        free(info->stack);
        free(info);
        return 1;
      case 14:                                  // 判断栈顶的两个值是否相等,结果存在 reg3 中
        v28 = &info->stack[info->_esp];
        v40 = *(v28 - 1) == *v28;
LABEL_21:
        reg3 = v40;
        goto LABEL_22;
      case 15:                                  // 计算栈顶存放的字符串指针对应的字符串长度存放到栈顶
        v25 = info->_esp;
        v26 = info->stack;
        v27 = (const char *)v26[v25];
        info->_esp = v25 - 1;
        info->_esp = v25;
        v26[v25] = strlen(v27);
LABEL_22:
        reg2 = v41;
        break;
      default:
        break;
    }
  }
  return 0;
}

在调试分析过程中发现:input 每个位置的字符只影响结果对应位置的字符,因此考虑逐字节爆破。

首先创建调试进程并在 0x004010A3 ,0x004010B2 和 0x00401717 除下断点。

    Debug dbg("..\\virtual_waifu2.exe");

    dbg.bp(0x004010A3);
    dbg.bp(0x004010B2);
    dbg.bp(0x00401717);

0x004010A3 处下断点是为了在第一次循环中跳过输入
2022CTF培训(三)windows&linux&安卓平台调试机制原理
由于后续循环中会跳过 0x004010B2 处平衡 scanf 参数的堆栈,因此在 0x004010A3 处要手动平衡堆栈。

            case 0x004010A3: {
                context.Eip += 0x4;
                context.Esp += 0x10;
                dbg.setContext(context);
                dbg.bc(0x004010A3);
                break;
            }

0x004010B2 处除了要跳过平衡堆栈的代码外还要设置输入内容。分析汇编代码可以看出,此时 eax 指向 input 。

            case 0x004010B2: {
                context.Eip += 0x2;
                dbg.setContext(context);
                dbg.writeBytes(context.Eax, ans);
                break;
            }

0x00401717 处下断点可以通过 esp 获取到 VM 函数处理过的 input 。
2022CTF培训(三)windows&linux&安卓平台调试机制原理
另外还要将 eip 置为 0x004010A8 进行下一次爆破。

            case 0x00401717: {
                BYTE res[sizeof(dst)]{};
                LPVOID pRes = nullptr;
                dbg.readData(context.Esp, pRes);
                dbg.readData(pRes, res);
                if (!memcmp(dst, res, ans.size())) {
                    std::cout << "[+] ans: " << ans << std::endl;
                    if (ans.size() == sizeof(dst)) {
                        return 0;
                    }
                    ans.push_back(0);
                } else {
                    ans.back()++;
                }
                context.Eip = 0x004010A8;
                dbg.setContext(context);
                break;
            }

完整代码如下:

#include<iostream>
#include<Windows.h>
#include <map>

class Debug {
public:
    explicit Debug(const std::string &name) {
        CreateProcessA(name.c_str(), nullptr, nullptr, nullptr, FALSE, DEBUG_ONLY_THIS_PROCESS, nullptr, nullptr, &si, &pi);
    }

    ~Debug() {
        TerminateProcess(pi.hProcess, 0);
    }

    template<typename T>
    void bp(T addr) {
        if (breakPoints.count(LPVOID(addr))) {
            return;
        }
        ReadProcessMemory(pi.hProcess, LPVOID(addr), &breakPoints[LPVOID(addr)], sizeof(BYTE), nullptr);
        WriteProcessMemory(pi.hProcess, LPVOID(addr), &INT3, sizeof(BYTE), nullptr);
        FlushInstructionCache(pi.hProcess, LPVOID(addr), sizeof(BYTE));
    }

    template<typename T>
    void bc(T addr) {
        if (!breakPoints.count(LPVOID(addr))) {
            return;
        }
        WriteProcessMemory(pi.hProcess, LPVOID(addr), &breakPoints[LPVOID(addr)], sizeof(BYTE), nullptr);
        breakPoints.erase(LPVOID(addr));
        FlushInstructionCache(pi.hProcess, LPVOID(addr), sizeof(BYTE));
    }

    bool g() const {
        return ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE);
    }


    std::pair<EXCEPTION_RECORD, CONTEXT> getDbgEvent() const {
        while (true) {
            WaitForDebugEvent((LPDEBUG_EVENT) &debugEvent, INFINITE);
            if (debugEvent.dwDebugEventCode != EXCEPTION_DEBUG_EVENT) {
                ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE);
                continue;
            }
            EXCEPTION_RECORD exception = debugEvent.u.Exception.ExceptionRecord;
            if (exception.ExceptionCode != STATUS_BREAKPOINT) {
                ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED);
                continue;
            }
            CONTEXT context{};
            context.ContextFlags = CONTEXT_ALL;
            GetThreadContext(pi.hThread, &context);
            return {exception, context};
        }
    }

    bool setContext(CONTEXT &context) const {
        return SetThreadContext(pi.hThread, &context);
    }

    template<typename T1, typename T2>
    bool readData(T1 addr, T2 &data) {
        return ReadProcessMemory(pi.hProcess, LPVOID(addr), &data, sizeof(data), nullptr);
    }

    template<typename T1, typename T2>
    bool writeData(T1 addr, T2 &data) {
        return WriteProcessMemory(pi.hProcess, LPVOID(addr), &data, sizeof(data), nullptr);
    }

    template<typename T>
    bool readBytes(T addr, std::string &bytes, int len) const {
        char *buf = new char[len];
        bool ret = ReadProcessMemory(pi.hProcess, LPVOID(addr), buf, len, nullptr);
        bytes = std::string(buf, buf + len);
        delete[]buf;
        return ret;
    }

    template<typename T>
    bool writeBytes(T addr, std::string &bytes) const {
        return WriteProcessMemory(pi.hProcess, LPVOID(addr), bytes.c_str(), bytes.size(), nullptr);
    }

private:
    const BYTE INT3 = 0xCC;
    STARTUPINFOA si{};
    PROCESS_INFORMATION pi{};
    std::map<LPVOID, BYTE> breakPoints;
    DEBUG_EVENT debugEvent{};

    Debug(const Debug &rhs) = delete;

    Debug &operator=(const Debug &rhs) = delete;
};

constexpr BYTE dst[]{0x86, 0x5C, 0xB8, 0x46, 0x4C, 0xBD, 0x4A, 0xA3, 0xBE, 0x4C, 0x8D, 0xA3, 0xBA, 0xF3, 0xA1, 0xAB, 0xA2, 0xFA, 0xF9, 0xA4, 0xAE, 0x80, 0xFD, 0xAE};


int main() {
    Debug dbg("virtual_waifu2.exe");

    dbg.bp(0x004010A3);
    dbg.bp(0x004010B2);
    dbg.bp(0x00401717);

    std::string ans{0};
    while (true) {
        auto[exception, context] = dbg.getDbgEvent();
        switch ((DWORD) exception.ExceptionAddress) {
            case 0x004010A3: {
                context.Eip += 0x4;
                context.Esp += 0x10;
                dbg.setContext(context);
                dbg.bc(0x004010A3);
                break;
            }
            case 0x004010B2: {
                context.Eip += 0x2;
                dbg.setContext(context);
                dbg.writeBytes(context.Eax, ans);
                break;
            }
            case 0x00401717: {
                BYTE res[sizeof(dst)]{};
                LPVOID pRes = nullptr;
                dbg.readData(context.Esp, pRes);
                dbg.readData(pRes, res);
                if (!memcmp(dst, res, ans.size())) {
                    std::cout << "[+] ans: " << ans << std::endl;
                    if (ans.size() == sizeof(dst)) {
                        return 0;
                    }
                    ans.push_back(0);
                } else {
                    ans.back()++;
                }
                context.Eip = 0x004010A8;
                dbg.setContext(context);
                break;
            }
        }
        dbg.g();
    }
}

运行结果:
2022CTF培训(三)windows&linux&安卓平台调试机制原理

Linux 平台调试机制

调试原理

调试的本质是调试器进程与被调试进程的跨进程通信,而在现代操作系统中进程与进程之间是隔离的,无法直接访问。因此跨进程通信需要操作系统的参与,调试也不例外。
在Linux平台中,调试需要通过ptrace系统调用来向操作系统申请交互,通过控制参数来实现不同的功能,包括开启调试、读写寄存器、读写内存等。

调试方法

以Hyper-V上的Kali虚拟机,IDA Pro(64位版本不限)为例进行远程调试。

  1. 测试网络连通,保证虚拟机和物理机之间可以互相访问。

  2. 将IDA根目录下的dbgsrv文件夹中的linux_server64复制到目标机器中,赋予执行权限,直接执行。注意如果需要附加进程则必须以root用户启动,否则只能调试Start process产生的子进程。
    2022CTF培训(三)windows&linux&安卓平台调试机制原理

  3. 在IDA中点击Deubgger - select debugger,选择Remote Linux Debugger。点击Debugger - process options,设置hostname为虚拟机IP。
    2022CTF培训(三)windows&linux&安卓平台调试机制原理

  4. 附加进程则点击Deubbger - attach to process, 启动新进程则点击Debugger - start process。注意启动新进程时交互是在debugger的对应终端下,与gdb类似,不会另起新的终端。
    2022CTF培训(三)windows&linux&安卓平台调试机制原理

反调试和反反调试

反调试分为以下三种

  1. ptrace冲突 - 操作系统仅允许进程同时被一个进程调试,因此反调试可以通过调用PTRACE_TRACEME使自己进入被父进程调试的状态。如果已经被调试则返回-1表示错误,否则成功并无法再被其他进程调试。
    2022CTF培训(三)windows&linux&安卓平台调试机制原理
    绕过方法:patch原文件或者调试的时候手动修改结果。
    2022CTF培训(三)windows&linux&安卓平台调试机制原理

  2. 系统信息 - 操作系统允许调试以后会记录一些信息,例如/proc/$pid/status中的TracePid会显示调试器进程的Pid。
    2022CTF培训(三)windows&linux&安卓平台调试机制原理
    2022CTF培训(三)windows&linux&安卓平台调试机制原理
    绕过方法同上。

  3. 调试特性 - 操作系统会将被调试进程的异常等转交给调试进程进行处理,而非调试状态下的异常等会让被调试进程自己处理。

安卓平台调试机制

调试原理

安卓分为Java+Native两层,分开讨论。

  • Java层:即虚拟机层,以Dalvik或ART虚拟机来执行代码。调试时也需要虚拟机通过jdwp协议进行调试。调试需要在apk的AndroidManifest.xml中写明android:debuggable=true才可以进行调试,但root机器包括调试器可以强制忽略这个设置。
  • Native层:即原生层,以原生汇编来执行代码,通常由Java层通过动态链接库的形式调起。调试时与Linux调试动态链接库基本无区别,在root权限下直接附加进程即可。

环境搭建

adb

Android 调试桥 (ADB) 是 Android 开源项目 (AOSP) 的一部分,用于连接安卓设备。
adb可以从这里 下载,本文附件中也有提供。

安卓模拟器

安卓模拟器可以用于调试 Java 层和采用 x86 架构的 Native层,对于 arm 架构的 Native层,安卓模拟器可以利用 libhoudini 将其翻译成 x86 架构的指令运行,但不支持调试。多数 apk 不会提供 x86 架构的动态链接库,因此最好还是有一部安卓手机用于调试。

安卓手机

在安卓逆向方面经常需要使用 root 权限,因此用于调试的安卓手机最好是 root 过的。
我使用的手机型号为 Redmi K20 Pro Premium Edition,由于官方 root 较为繁琐,因此采用第三方的的刷机软件进行 root,这里我采用的是奇兔刷机,当然也可以尝试一下其他的刷机软件。总之操作过程比较简单。
刷机后在 Magisk 中开启 Shell 的 root 权限。
2022CTF培训(三)windows&linux&安卓平台调试机制原理

2022CTF培训(三)windows&linux&安卓平台调试机制原理
在 root 后还需要检查一下 ro.debuggable 是否开启。
查看 ro.debuggable 的命令是:adb shell getprop ro.debuggable
2022CTF培训(三)windows&linux&安卓平台调试机制原理
由于刷机时安装了 Magisk,因此可以使用该软件开启。
2022CTF培训(三)windows&linux&安卓平台调试机制原理

JEB

JEB是一款用于逆向分析和调试安卓Java层的软件。
注意 JEB 有 3 个版本:
2022CTF培训(三)windows&linux&安卓平台调试机制原理这里不建议采用普通版的。
2022CTF培训(三)windows&linux&安卓平台调试机制原理
本文附件中提供了破解版的 JEB Pro,按照 readme 中提供的破解方法破解即可。如果破解失败就到bin/app/目录下,找到jeb-license.txt,把里面的证书信息清除就可以了。

Java 层调试

进程附加

  • 在手机上开启 FlagApp_new ,并在电脑上用 JEB 打开该软件。
  • 选定要附加的进程,开始调试。
    2022CTF培训(三)windows&linux&安卓平台调试机制原理

从函数开始出调试

使用如下命令使用安卓管理器用调试模式启动软件:
2022CTF培训(三)windows&linux&安卓平台调试机制原理
软件启动后等待调试器附加。
2022CTF培训(三)windows&linux&安卓平台调试机制原理
用 JEB 附加进程,可以看到程序在函数开始处断下来。
2022CTF培训(三)windows&linux&安卓平台调试机制原理

Native 层调试

MainActivity 中的 check 函数实际上是调用的动态链接库的函数,这就需要在 Native 层对该动态链接库进行调试。
2022CTF培训(三)windows&linux&安卓平台调试机制原理
在 apk 的 Libraries 目录下可以找到该 apk 使用的动态链接库。这里我采用的是安卓真机调试,因此需要调试的是 armeabi 目录下的动态链接库。
2022CTF培训(三)windows&linux&安卓平台调试机制原理
将 android_server 传到手机上并启动。
2022CTF培训(三)windows&linux&安卓平台调试机制原理
另外还要设置手机和电脑的 23946 端口的映射。
2022CTF培训(三)windows&linux&安卓平台调试机制原理
使用 ida 打开该动态链接库并在对应函数处下断点。
2022CTF培训(三)windows&linux&安卓平台调试机制原理
选择远程的安卓调试
2022CTF培训(三)windows&linux&安卓平台调试机制原理
ip 设置为本地,端口选 23946
2022CTF培训(三)windows&linux&安卓平台调试机制原理
选择附加的进程
2022CTF培训(三)windows&linux&安卓平台调试机制原理
在软件中输入内容使其调用动态链接库
2022CTF培训(三)windows&linux&安卓平台调试机制原理
成功在断点处断下来。
2022CTF培训(三)windows&linux&安卓平台调试机制原理

反调试和反反调试

Java层的反调试只有一个APIIsDebuggerConnected可以检测调试器存在,对抗只需要检查它是否存在即可,绕过可以通过Patch或动态修改结果。
Native层的反调试则跟Linux完全一致,可以通过ptrace的冲突,也可以访问/proc/pid/status来检查。