附件下载链接
使用 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(调试器循环)是所有调试器最核心的部分,它主要围绕 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;
}
}
}
上述代码将会处理两种调试消息:
运行程序,我们会发现我们的调试器正常接管到了子进程新线程创建的消息,退出子程序,我们能够接管到子进程线程退出的消息。
我们通过 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 函数返回的值即可。
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 处下断点是为了在第一次循环中跳过输入
由于后续循环中会跳过 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 。
另外还要将 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
#include
#include
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();
}
}
调试的本质是调试器进程与被调试进程的跨进程通信,而在现代操作系统中进程与进程之间是隔离的,无法直接访问。因此跨进程通信需要操作系统的参与,调试也不例外。
在Linux平台中,调试需要通过ptrace系统调用来向操作系统申请交互,通过控制参数来实现不同的功能,包括开启调试、读写寄存器、读写内存等。
以Hyper-V上的Kali虚拟机,IDA Pro(64位版本不限)为例进行远程调试。
测试网络连通,保证虚拟机和物理机之间可以互相访问。
将IDA根目录下的dbgsrv文件夹中的linux_server64复制到目标机器中,赋予执行权限,直接执行。注意如果需要附加进程则必须以root用户启动,否则只能调试Start process产生的子进程。
在IDA中点击Deubgger - select debugger,选择Remote Linux Debugger。点击Debugger - process options,设置hostname为虚拟机IP。
附加进程则点击Deubbger - attach to process, 启动新进程则点击Debugger - start process。注意启动新进程时交互是在debugger的对应终端下,与gdb类似,不会另起新的终端。
反调试分为以下三种
ptrace冲突 - 操作系统仅允许进程同时被一个进程调试,因此反调试可以通过调用PTRACE_TRACEME使自己进入被父进程调试的状态。如果已经被调试则返回-1表示错误,否则成功并无法再被其他进程调试。
绕过方法:patch原文件或者调试的时候手动修改结果。
系统信息 - 操作系统允许调试以后会记录一些信息,例如/proc/$pid/status中的TracePid会显示调试器进程的Pid。
绕过方法同上。
调试特性 - 操作系统会将被调试进程的异常等转交给调试进程进行处理,而非调试状态下的异常等会让被调试进程自己处理。
安卓分为Java+Native两层,分开讨论。
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 权限。
在 root 后还需要检查一下 ro.debuggable 是否开启。
查看 ro.debuggable 的命令是:adb shell getprop ro.debuggable
由于刷机时安装了 Magisk,因此可以使用该软件开启。
JEB是一款用于逆向分析和调试安卓Java层的软件。
注意 JEB 有 3 个版本:
这里不建议采用普通版的。
本文附件中提供了破解版的 JEB Pro,按照 readme 中提供的破解方法破解即可。如果破解失败就到bin/app/目录下,找到jeb-license.txt,把里面的证书信息清除就可以了。
使用如下命令使用安卓管理器用调试模式启动软件:
软件启动后等待调试器附加。
用 JEB 附加进程,可以看到程序在函数开始处断下来。
MainActivity 中的 check 函数实际上是调用的动态链接库的函数,这就需要在 Native 层对该动态链接库进行调试。
在 apk 的 Libraries 目录下可以找到该 apk 使用的动态链接库。这里我采用的是安卓真机调试,因此需要调试的是 armeabi 目录下的动态链接库。
将 android_server 传到手机上并启动。
另外还要设置手机和电脑的 23946 端口的映射。
使用 ida 打开该动态链接库并在对应函数处下断点。
选择远程的安卓调试
ip 设置为本地,端口选 23946
选择附加的进程
在软件中输入内容使其调用动态链接库
成功在断点处断下来。
Java层的反调试只有一个APIIsDebuggerConnected可以检测调试器存在,对抗只需要检查它是否存在即可,绕过可以通过Patch或动态修改结果。
Native层的反调试则跟Linux完全一致,可以通过ptrace的冲突,也可以访问/proc/pid/status来检查。