APIHOOK之在NT系列操作系统里让自己“消失”中2

本文详细介绍如何在NT系列操作系统中实现进程隐身,包括通过全局挂钩、感染新进程、挂钩DLL及修改内存读取等功能,确保进程不被发现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在NT系列操作系统里让自己“消失”中
2007-03-31 09:04
=====[ 7.2 全局挂钩 ]=======================================

     枚举进程通过前面提到的API函数NtQuerySystemInformation来完成。因为系统中还有一些内部native进程,所以使用重写函数第一个指令的方法来挂钩。对每个正在运行的进程我们需要做的都一样。首先在目标进程里分配一部分内存用来写入我们用来挂钩函数的新代码,然后把每个函数开始的5个字节改为跳转指令(jmp),这个跳转会转为执行我们的代码。所以当被挂钩的函数被调用时跳转指令能立刻被执行。我们需要保存每个函数开始被改写的指令,需要它们来调用被挂钩函数的原始代码。保存指令的过程在"挂钩Windows API"的3.2.3节有描述。
     首先通过NtOpenProcess打开目标进程并获取句柄。如果我们没有足够权限的话就会失败。

     NTSTATUS NtOpenProcess(
         OUT PHANDLE ProcessHandle,
         IN ACCESS_MASK DesiredAccess,
         IN POBJECT_ATTRIBUTES ObjectAttributes,
         IN PCLIENT_ID ClientId OPTIONAL
     );

     ProcessHandle是指向保存进程对象句柄的指针。DesiredAccess应该被设置为PROCESS_ALL_ACCESS。我们要在ClientId结构里设置UniqueProcess为目标进程的PID,UniqueThread应该为0。被打开的句柄可以通过NtClose关闭。

     #define PROCESS_ALL_ACCESS 0x001F0FFF

     现在我们为我们的代码分配部分内存。这通过NtAllocateVirtualMemory来完成。

     NTSTATUS NtAllocateVirtualMemory(
         IN HANDLE ProcessHandle,
         IN OUT PVOID BaseAddress,
         IN ULONG ZeroBits,
         IN OUT PULONG AllocationSize,
         IN ULONG AllocationType,
         IN ULONG Protect
     );

     ProcessHandle是来自NtOpenProcess相同参数。BaseAddress是一个指针,指向被分配虚拟内存基地址的开始处,它的输入参数应该为NULL。AllocationSize指向我们要分配的字节数的变量,同样它也用来接受实际分配的字节数大小。最好把AllocationType在设置成MEM_COMMIT之外再加上MEM_TOP_DOWN因为内存要在接近DLL地址的尽可能高的地址分配。

     #define MEM_COMMIT     0x00001000
     #define MEM_TOP_DOWN     0x00100000    


     然后我们就可以通过调用NtWriteVirtualMemory来写入我们的代码。

     NTSTATUS NtWriteVirtualMemory(
         IN HANDLE ProcessHandle,
         IN PVOID BaseAddress,
         IN PVOID Buffer,
         IN ULONG BufferLength,
         OUT PULONG ReturnLength OPTIONAL
     );

     BaseAddress是NtAllocateVirtualMemory返回的地址。Buffer指向我们要写入的字节,BufferLength是我们要写入的字节数。

     现在我们来挂钩单个进程。被加载入所有进程的动态链接库只有ntdll.dll。所以我们要检查被导入进程要挂钩的函数是否来自ntdll.dll。但是这些来自其它DLL的函数所在的内存可能已经被分配,这时重写它的代码会在目标进程里导致错误。这就是我们必须去检查我们要挂钩的函数来自的动态链接库是否被目标进程加载的原因。
     我们需要通过NtQueryInformationProcess获取目标进程的PEB(进程环境块)。

     NTSTATUS NtQueryInformationProcess(
         IN HANDLE ProcessHandle,
         IN PROCESSINFOCLASS ProcessInformationClass,
         OUT PVOID ProcessInformation,
         IN ULONG ProcessInformationLength,
         OUT PULONG ReturnLength OPTIONAL
     );

     我们把ProcessInformationClass设置为ProcessBasicInformation,然后PROCESS_BASIC_INFORMATION结构会返回到ProcessInformation缓冲区中,大小为给定的ProcessInformationLength。

     #define ProcessBasicInformation 0

     typedef struct _PROCESS_BASIC_INFORMATION {
         NTSTATUS ExitStatus;
         PPEB PebBaseAddress;
         KAFFINITY AffinityMask;
         KPRIORITY BasePriority;
         ULONG UniqueProcessId;
         ULONG InheritedFromUniqueProcessId;
     } PROCESS_BASIC_INFORMATION, *PPROCESS_BASIC_INFORMATION;

     PebBaseAddress就是我们要寻找的东西。在PebBaseAddress+0C处是PPEB_LDR_DATA的地址。这些通过调用NtReadVirtualMemory来获得。

     NTSTATUS NtReadVirtualMemory(
         IN HANDLE ProcessHandle,
         IN PVOID BaseAddress,
         OUT PVOID Buffer,
         IN ULONG BufferLength,
         OUT PULONG ReturnLength OPTIONAL
     );

     变量和NtWriteVirtualMemory的很相似。
     在PPEB_LDR_DATA+01C处是InInitializationOrderModuleList的地址。它是被加载进进程的动态链接库的列表。我们只对这个结构中的一些部分感兴趣。

     typedef struct _IN_INITIALIZATION_ORDER_MODULE_LIST {
         PVOID Next,
         PVOID Prev,
         DWORD ImageBase,
         DWORD ImageEntry,
         DWORD ImageSize,
         ...
     );

     Next是指向下一个记录的指针,Prev指向前一个,最后一个记录的会指向第一个。ImageBase是内存中模块的地址,ImageEntry是模快的入口点,ImageSize是它的大小。
    
     对所有我们想要挂钩的库我们需要获得它们的ImageBase(比方调用GetModuleHandle或者LoadLibrary)。然后把这个ImageBase和InInitializationOrderModuleList的ImageBase比较。
     现在我们已经为挂钩准备就绪。因为我们是挂钩正在运行的进程,所以可能我们正在改写代码的同时代码被执行,这时就会导致错误。所以首先我们就得停止目标进程里的所有线程。它的所有线程列表可以通过设置了SystemProcessAndThreadInformation的NtQuerySystemInformation来获得。有关这个函数的描述参考第4节。但是还得加入SYSTEM_THREADS结构的描述,用来保存线程的信息。

     typedef struct _SYSTEM_THREADS {
         LARGE_INTEGER KernelTime;
         LARGE_INTEGER UserTime;
         LARGE_INTEGER CreateTime;
         ULONG WaitTime;
         PVOID StartAddress;
         CLIENT_ID ClientId;
         KPRIORITY Priority;
         KPRIORITY BasePriority;
         ULONG ContextSwitchCount;
         THREAD_STATE State;
         KWAIT_REASON WaitReason;
     } SYSTEM_THREADS, *PSYSTEM_THREADS;

     对每个线程调用NtOpenThread获取它们的句柄,通过使用ClientId。

     NTSTATUS NtOpenThread(
         OUT PHANDLE ThreadHandle,
         IN ACCESS_MASK DesiredAccess,
         IN POBJECT_ATTRIBUTES ObjectAttributes,
         IN PCLIENT_ID ClientId
     );

     我们需要的句柄被保存在ThreadHandle。我们需要把DesiredAccess设置为THREAD_SUSPEND_RESUME。

     #define THREAD_SUSPEND_RESUME 2

     ThreadHandle用来调用NtSuspendThread。

     NTSTATUS NtSuspendThread(
         IN HANDLE ThreadHandle,
         OUT PULONG PreviousSuspendCount OPTIONAL
     );


     被挂起的进程就可以被改写了。我们按照"挂钩Windows API"里3.2.2节里描述的方法处理。唯一的不同是使用其它进程的函数。

     挂钩完后我们就可以调用NtResumeThread恢复所有线程的运行。

     NTSTATUS NtResumeThread(
         IN HANDLE ThreadHandle,
         OUT PULONG PreviousSuspendCount OPTIONAL
     );


=====[ 7.3 新进程 ]================================================

     感染所有正在运行的进程并不能影响将要被运行的进程。我们可以每隔一定时间获取一次进程的列表,然后感染新的列表里的进程。但这种方法很不可靠。
     更好的方法是挂钩新进程开始时肯定会调用的函数。因为所有系统中正在运行的进程都已经被挂钩,所以这种方法不会漏掉任何新的进程。我们可以挂钩NtCreateThread,但这不是最简单的方法。我们可以挂钩NtResumeThread,因为它也是每当新进程创建时被调用,它在NtCreateThread之后被调用。
     唯一的问题在于,这个函数并不只在新进程被创建时调用。但我们能很容易解决这点。NtQueryInformationThread能给我们指定线程是属于哪个进程的信息。最后我们要做的就是检查进程是否已经被挂钩了。这通过读取我们要挂钩的函数的开始5个字节来完成。

     NTSTATUS NtQueryInformationThread(
         IN HANDLE ThreadHandle,
         IN THREADINFOCLASS ThreadInformationClass,
         OUT PVOID ThreadInformation,
         IN ULONG ThreadInformationLength,
         OUT PULONG ReturnLength OPTIONAL
     );

     ThreadInformationClass是信息分类,在这里它被设置为ThreadBasicInformation。ThreadInformation是保存结果的缓冲区,大小按字节计算为ThreadInformationLength。

     #define ThreadBasicInformation 0

     对ThreadBasicInformation返回这个结构:

     typedef struct _THREAD_BASIC_INFORMATION {
         NTSTATUS ExitStatus;
         PNT_TIB TebBaseAddress;
         CLIENT_ID ClientId;
         KAFFINITY AffinityMask;
         KPRIORITY Priority;
         KPRIORITY BasePriority;
     } THREAD_BASIC_INFORMATION, *PTHREAD_BASIC_INFORMATION;

     ClientId是线程所属进程的PID。

     现在我们来感染新进程。问题就是新进程的地址空间中只有ntdll.dll,其他的模块在调用NtResumeThread之后被加载。有几种方法可以解决这个问题,比方说我们可以挂钩一个名为LdrInitializeThunk的API函数,它在进程初始化时被调用。

     NTSTATUS LdrInitializeThunk(
         DWORD Unknown1,
         DWORD Unknown2,
         DWORD Unknown3
     );

     首先我们先运行原始的代码,然后挂钩新进程里所有要挂钩的函数。但最好对LdrInitializeThunk解除挂钩,因为这个函数在之后要被调用很多次,我们并不需要重新再挂钩所有的函数。这时在程序执行第一个指令前所有工作已经完成。这就是为什么在我们挂钩它之前它没有机会调用任何一个被挂钩过的函数的原因。
     对自己挂钩和动态挂钩正在运行的进程一样,只是这里我们不需要关心正在运行的线程。


=====[ 7.4 DLL ]================================================

     系统中每个进程都是一份ntdll.dll拷贝。这意味着我们可以在进程初始化阶段挂钩这个模块里的任意一个函数。但是来自其它模块比如kernel32.dll或advapi32.dll的函数该怎么办呢?还有一些进程只有ntdll.dll,其他模块都是在进程被挂钩之后在运行过程中才被动态加载的。这就是我们还得挂钩加载新模块的函数LdrLoadDll的原因。
    
     NTSTATUS LdrLoadDll(
         PWSTR szcwPath,
         PDWORD pdwLdrErr,      
         PUNICODE_STRING pUniModuleName,
         PHINSTANCE pResultInstance
     );

     这里对我们来说最重要的是pUniModuleName,它保存模块名字。当调用成功后pResultInstance保存模块地址。
     我们首先调用原始的LdrLoadDll然后挂钩被加载模块里所有函数。



=====[ 8. 内存 ]===========================================

     当我们正在挂钩一个函数时我们会修改它开始的字节。通过调用NtReadVirtualMemory任何人都可以检测出函数被挂钩。所以我们还要挂钩NtReadVirtualMemory来防止检测。

     NTSTATUS NtReadVirtualMemory(
         IN HANDLE ProcessHandle,
         IN PVOID BaseAddress,
         OUT PVOID Buffer,
         IN ULONG BufferLength,
         OUT PULONG ReturnLength OPTIONAL
     );

     我们修改了我们挂钩的函数开始的字节并且为我们新的代码分配了内存。我们就需要检查时候有人读取了这些代码。如果我们的代码出现在BaseAddress到BaseAddress+BufferLength中我们就需要在缓冲区中改变它的一些字节。
     如果有人在我们分配的内存中查询字节我们就返回空的缓冲区和错误STATUS_PARTIAL_COPY。这个值用来表示被请求的字节并没有完全被拷贝到缓冲区中,它也同样被用在当请求了未分配的内存时。这时ReturnLength应该被设为0。

     #define STATUS_PARTIAL_COPY 0x8000000D

     如果有人查询被挂钩的函数开始的字节我们就调用原始代码并拷贝原始代码里开始的那些字节到缓冲区中。
     现在新进程已无法通过读取它的内存来检测是否被挂钩了。同样如果你调试被挂钩的进程调试器也会用问题,它会显示原始代码,但却执行我们的代码。

     为了使隐藏更完美,我们还要挂钩NtQueryVirtualMemory。这个函数用来获取虚拟内存的信息。我们挂钩它来防止探测我们分配的虚逆内存。

     NTSTATUS NtQueryVirtualMemory(
         IN HANDLE ProcessHandle,
         IN PVOID BaseAddress,
         IN MEMORY_INFORMATION_CLASS MemoryInformationClass,
         OUT PVOID MemoryInformation,
         IN ULONG MemoryInformationLength,
         OUT PULONG ReturnLength OPTIONAL
     );

     MemoryInformationClass标明了返回数据的类别。我们对开始的2种类型感兴趣。

     #define MemoryBasicInformation 0
     #define MemoryWorkingSetList 1

     对MemoryBasicInformation返回这个结构:

     typedef struct _MEMORY_BASIC_INFORMATION {
         PVOID BaseAddress;
         PVOID AllocationBase;
         ULONG AllocationProtect;
         ULONG RegionSize;
         ULONG State;
         ULONG Protect;
         ULONG Type;
     } MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;

     每个区段都有它的大小RegionSize和它的类型Type。空闲内存的类型是MEM_FREE。(区段对象就是文件映射对象,是可被映射到一个进程的虚逆地址空间的对象)

     #define MEM_FREE 0x10000

     如果我们代码之前一个区段的类型是MEM_FREE我们就在它的RegionSize加上我们代码的区段的大小。如果我们代码之后的区段的类型也是MEM_FREE那么就在之前区段的RegionSize上再加上之后的空闲区段的大小。
     如果我们代码之前的区段是其它类型,我们就对我们代码的区段返回MEM_FREE。它的大小根据之后的区段来计算。

     对MemoryWorkingSetList返回这个结构:

     typedef struct _MEMORY_WORKING_SET_LIST {
         ULONG NumberOfPages;
         ULONG WorkingSetList[1];
     } MEMORY_WORKING_SET_LIST, *PMEMORY_WORKING_SET_LIST;

     NumberOfPages是WorkingSetList中列项的数目。这个数字应该减少一些。我们在WorkingSetList中找到我们代码的区段然后把之后记录前移。WorkingSetList是按DWORD排列的数组,每个元素的高20位标明了区段地址,低12位是标志。
 
#define _GNU_SOURCE #include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <elf.h> #include <sys/uio.h> #include <sys/stat.h> #include <dirent.h> #include <asm/ptrace.h> // 修复1: 正确定义ARM64硬件调试寄存器 #ifndef NT_ARM_HW_BREAK #define NT_ARM_HW_BREAK 0x402 // 执行断点 #endif #ifndef NT_ARM_HW_WATCH #define NT_ARM_HW_WATCH 0x403 // 观察点 #endif // 修复2: 正确定义调试寄存器状态结构体 struct user_hwdebug_state { uint32_t dbg_info; // 调试信息 uint32_t pad; // 填充 struct { uint64_t addr; // 地址寄存器 uint64_t ctrl; // 控制寄存器 } dbg_regs[16]; // 最大16个断点 }; // 获取线程ID int get_threads(pid_t pid, pid_t *threads, int max_threads) { char path[64]; int count = 0; snprintf(path, sizeof(path), "/proc/%d/task", pid); DIR *dir = opendir(path); if (!dir) { perror("opendir failed"); return 0; } struct dirent *entry; while ((entry = readdir(dir)) != NULL) { if (count >= max_threads) break; if (entry->d_type == DT_DIR && strcmp(entry->d_name, ".") != 0 && strcmp(entry->d_name, "..") != 0) { threads[count++] = atoi(entry->d_name); } } closedir(dir); return count; } // 附加到进程 bool attach_to_process(pid_t pid) { if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) < 0) { perror("【错误】附加主进程失败"); return false; } int status; if (waitpid(pid, &status, 0) < 0) { perror("【错误】等待主进程失败"); return false; } pid_t threads[64]; int thread_count = get_threads(pid, threads, 64); if (thread_count == 0) { fprintf(stderr, "【警告】未找到任何线程\n"); return false; } for (int i = 0; i < thread_count; i++) { pid_t tid = threads[i]; if (tid == pid) continue; if (ptrace(PTRACE_ATTACH, tid, NULL, NULL) < 0) { perror("【错误】附加子线程失败"); continue; } if (waitpid(tid, &status, 0) < 0) { perror("【错误】等待子线程失败"); ptrace(PTRACE_DETACH, tid, NULL, NULL); continue; } printf("【已附加线程】tid=%d\n", tid); } return true; } // 修复3: 正确设置硬件断点 bool set_hw_breakpoint(pid_t pid, pid_t thread_id, uintptr_t addr, int len, int type) { // 选择寄存器集类型 int nt_type = (type == 0) ? NT_ARM_HW_BREAK : NT_ARM_HW_WATCH; struct user_hwdebug_state dbg_regs; struct iovec iov = { .iov_base = &dbg_regs, .iov_len = sizeof(dbg_regs) }; // 获取当前状态 if (ptrace(PTRACE_GETREGSET, thread_id, nt_type, &iov) < 0) { perror("【错误】PTRACE_GETREGSET获取失败"); return false; } // 查找空闲槽位 int slot = -1; for (int i = 0; i < 16; i++) { if (dbg_regs.dbg_regs[i].addr == 0) { slot = i; break; } } if (slot == -1) { fprintf(stderr, "【错误】线程%d无可用硬件断点\n", thread_id); return false; } // 设置控制寄存器 uint64_t ctrl_value = 0; if (type == 0) { // 执行断点 ctrl_value = (1 << 0) | // 启用 (0b01 << 8); // 用户空间(EL0) } else { // 观察点 // 计算BAS掩码 uint64_t bas = 0; switch (len) { case 1: bas = 0x1; break; case 2: bas = 0x3; break; case 4: bas = 0xF; break; case 8: bas = 0xFF; break; default: fprintf(stderr, "【错误】无效长度: %d\n", len); return false; } ctrl_value = (1 << 0) | // 启用 (0b01 << 8); // 用户空间(EL0) // 设置类型 if (type == 1) { // 读 ctrl_value |= (0b01 << 3); // Load } else if (type == 2) { // 写 ctrl_value |= (0b10 << 3); // Store } else if (type == 3) { // 读写 ctrl_value |= (0b11 << 3); // Load/Store } // 设置长度(BAS) ctrl_value |= (bas << 16); } // 设置地址和控制寄存器 dbg_regs.dbg_regs[slot].addr = addr; dbg_regs.dbg_regs[slot].ctrl = ctrl_value; // 应用设置 if (ptrace(PTRACE_SETREGSET, thread_id, nt_type, &iov) < 0) { perror("【错误】PTRACE_SETREGSET设置失败"); return false; } printf("【线程%d断点设置成功】 地址:0x%llx 类型:%s 长度:%d字节\n", thread_id, (unsigned long long)addr, type == 0 ? "执行" : type == 1 ? "读" : type == 2 ? "写" : "读写", len); return true; } // 清除所有硬件断点 bool clear_all_hw_breakpoints(pid_t thread_id) { struct user_hwdebug_state dbg_regs; struct iovec iov = { .iov_base = &dbg_regs, .iov_len = sizeof(dbg_regs) }; // 清除执行断点 memset(&dbg_regs, 0, sizeof(dbg_regs)); if (ptrace(PTRACE_SETREGSET, thread_id, NT_ARM_HW_BREAK, &iov) < 0) { perror("【错误】清除执行断点失败"); } // 清除观察点 memset(&dbg_regs, 0, sizeof(dbg_regs)); if (ptrace(PTRACE_SETREGSET, thread_id, NT_ARM_HW_WATCH, &iov) < 0) { perror("【错误】清除观察点失败"); return false; } return true; } // 获取寄存器信息 bool get_registers(pid_t tid, struct user_pt_regs *regs) { struct iovec iov = { .iov_base = regs, .iov_len = sizeof(*regs) }; if (ptrace(PTRACE_GETREGSET, tid, NT_PRSTATUS, &iov) < 0) { perror("【错误】PTRACE_GETREGSET获取失败"); return false; } return true; } int main() { pid_t target_pid; printf("【输入目标进程PID】\n"); scanf("%d", &target_pid); if (geteuid() != 0) { fprintf(stderr, "【错误】需要root权限\n"); return 1; } if (!attach_to_process(target_pid)) { fprintf(stderr, "【致命错误】无法附加目标进程\n"); return 1; } uintptr_t bp_addr; printf("【输入断点地址(十六进制)】\n"); scanf("%llx", (unsigned long long *)&bp_addr); int len, type; printf("【选择断点类型(0:执行,1:读,2:写,3:读写)】\n"); scanf("%d", &type); if (type != 0) { printf("【输入断点长度(1,2,4,8字节)】\n"); scanf("%d", &len); // 检查长度有效性 if (len != 1 && len != 2 && len != 4 && len != 8) { fprintf(stderr, "【错误】无效长度,使用默认值4字节\n"); len = 4; } } else { len = 4; // 执行断点长度固定 } pid_t threads[64]; int thread_count = get_threads(target_pid, threads, 64); for (int i = 0; i < thread_count; i++) { pid_t tid = threads[i]; printf("【处理线程】tid=%d\n", tid); if (!set_hw_breakpoint(target_pid, tid, bp_addr, len, type)) { fprintf(stderr, "【警告】线程%d断点设置失败\n", tid); continue; } } printf("【恢复进程执行...】\n"); if (ptrace(PTRACE_CONT, target_pid, NULL, NULL) < 0) { perror("【错误】恢复执行失败"); return 1; } printf("【等待断点触发...】\n"); int status; pid_t wait_pid = waitpid(-1, &status, 0); bool breakpoint_hit = false; for (int i = 0; i < thread_count; i++) { pid_t tid = threads[i]; int nt_type = (type == 0) ? NT_ARM_HW_BREAK : NT_ARM_HW_WATCH; struct user_hwdebug_state dbg_regs; struct iovec iov = { .iov_base = &dbg_regs, .iov_len = sizeof(dbg_regs) }; if (ptrace(PTRACE_GETREGSET, tid, nt_type, &iov) < 0) { perror("【错误】获取调试寄存器失败"); continue; } for (int j = 0; j < 16; j++) { if (dbg_regs.dbg_regs[j].addr == bp_addr && (dbg_regs.dbg_regs[j].ctrl & 0x1)) { breakpoint_hit = true; printf("\n【断点命中!】\n"); printf("触发线程:tid=%d\n", tid); printf("命中地址:0x%llx\n", (unsigned long long)bp_addr); const char *type_str = type == 0 ? "执行" : type == 1 ? "读" : type == 2 ? "写" : "读写"; printf("类型:%s | 长度:%d字节\n", type_str, len); struct user_pt_regs regs; if (get_registers(tid, &regs)) { printf("\n寄存器状态:\n"); printf("PC=0x%llx (程序计数器)\n", (unsigned long long)regs.pc); printf("LR=0x%llx (链接寄存器x30)\n", (unsigned long long)regs.regs[30]); printf("SP=0x%llx (栈指针sp)\n", (unsigned long long)regs.sp); printf("X0=0x%llx, X1=0x%llx, X2=0x%llx, X3=0x%llx\n", (unsigned long long)regs.regs[0], (unsigned long long)regs.regs[1], (unsigned long long)regs.regs[2], (unsigned long long)regs.regs[3]); } break; } } if (breakpoint_hit) break; } if (!breakpoint_hit) { printf("\n【未检测到断点命中,恢复进程执行...】\n"); ptrace(PTRACE_CONT, target_pid, NULL, NULL); } printf("\n【清理断点...】\n"); for (int i = 0; i < thread_count; i++) { pid_t tid = threads[i]; if (!clear_all_hw_breakpoints(tid)) { fprintf(stderr, "【警告】线程%d断点清除失败\n", tid); } else { printf("【已清除线程】tid=%d的断点\n", tid); } } printf("\n【分离调试会话...】\n"); for (int i = 0; i < thread_count; i++) { pid_t tid = threads[i]; if (ptrace(PTRACE_DETACH, tid, NULL, NULL) < 0) { perror("【错误】分离线程失败"); } } return 0; } Android NDK: src/ptrace断点.cpp.bak [arm64-v8a] Compile++ : 无痕hook.sh <= ptrace断点.cpp jni/src/ptrace断点.cpp:25:8: error: redefinition of 'user_hwdebug_state' struct user_hwdebug_state { ^ /data/data/com.aide.ui.mgai/no_backup/ndksupport-1710240003/android-ndk-aide/sysroot/usr/include/aarch64-linux-android/asm/ptrace.h:61:8: note: previous definition is here struct user_hwdebug_state { ^ 1 error generated. make: *** [/data/data/com.aide.ui.mgai/no_backup/ndksupport-1710240003/android-ndk-aide/build/core/build-binary.mk:530: obj/local/arm64-v8a/objs/无痕hook.sh/src/ptrace 断点.o] Error 1 修复好报错问题完整发给我
最新发布
07-14
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值