过Frida检测
先hook一下dlopen,也就是android_dlopen_ext
为什么要Hook dlopen呢?
因为App的Frida检测代码一般都在so层实现,这些检测代码会在对应的so加载时初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | function hook_dlopen() {
var android_dlopen_ext = Module.findExportByName(null,"android_dlopen_ext");
console.log("addr_android_dlopen_ext", android_dlopen_ext);
Interceptor.attach(android_dlopen_ext, {
onEnter: function (args) {
var pathptr = args[0];
if(pathptr != null && pathptr != undefined) {
var path = ptr(pathptr).readCString();
console.log("android_dlopen_ext:", path);
}
},
onLeave: function (retvel) {
}
})
}
function main() {
hook_dlopen()
}
setImmediate(main)
|
Frida进程会被杀死,同时手机也会卡死,而且也加载了特征so
这是为什么呢?
![]()
1 2 3 4 5 | 每隔几毫秒检查一次
↓
发现了Frida的痕迹
↓
执行反制措施:卡死界面 + 杀进程
|
我们的反制措施为Hook Clone函数
Clone函数为Linux创建线程的底层调用,Hook这个函数我们可以知道每个线程的详细信息,例如:谁创建的,线程函数在哪,什么时候创建的
这样我们就可以定位到反调试线程,然后分析它,干掉它
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 | function hook_clone() {
var clone = Module.findExportByName('libc.so','clone');
Interceptor.attach(clone, {
onEnter: function (args) {
console.log("═══ Clone Called ═══");
console.log("args[0] (wrapper):", args[0]);// __pthread_start
console.log("args[1] (stack) :", args[1]);
console.log("args[2] (flags) :", args[2]);
console.log("args[3] (tls) :", args[3]);//// 线程局部存储(TLS)
if(args[3] != 0) {
try{
// 读取真正的线程函数
var real_func = args[3].add(96).readPointer();
var module = Process.findModuleByAddress(real_func);
if(module) {
var offset = real_func.sub(module.base);
console.log(" 真正的线程函数:");
console.log(" SO名称:", module.name);
console.log(" 函数地址:", real_func);
console.log(" 偏移:", ptr(offset));
if(module.name.includes("DexHelper")) {
console.log(" 检测到目标so!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
}
}
}catch(e) {
console.log("解析失败:", e);
}
}
}
});
}
setImmediate(hook_clone);
|
为什么要在var real_func = args[3].add(96).readPointer(); 读取?
我们需要了解一下 pthread_internal_t 结构体也就是pthread_t
这是 Android Bionic 库中用来管理线程的内部结构:
那么什么时候会创建这个结构体呢?肯定是线程被创建的时候,也就是pthread_create函数
Android创建线程分析
安卓平台上总共有三种线程:
1. Java 线程:Android 虚拟机线程,具有运行 Java 代码的 Runtime
2. Native 线程(只能执行 C/C++):纯粹的 Linux 线程
3. Native 线程(还能执行 Java):既能执行 C/C++ 代码,也能执行 Java 代码
Java线程创建流程
java层:Thread.start()
1 2 3 4 5 6 7 | // /libcore/libart/src/main/java/java/lang/Thread.java
publicsynchronizedvoidstart() {
checkNotStarted();// 保证线程只启动一次
hasBeenStarted =true;
// 调用 native 方法创建线程
nativeCreate(this, stackSize, daemon);
}
|
nativeCreate为JNI方法,对应C++层的Thread_nativeCreate
JNI方法映射
1 2 3 4 5 6 | // /art/runtime/native/java_lang_Thread.cc
// 宏定义
#define NATIVE_METHOD(className, functionName, signature) \
{ #functionName, signature,reinterpret_cast<void*>(className ## _ ## functionName) }
// 方法注册
NATIVE_METHOD(Thread, nativeCreate,"(Ljava/lang/Thread;JZ)V"),
|
展开后,nativeCreate 映射到 Thread_nativeCreate 函数。
Thread_nativeCreate
1 2 3 4 5 6 7 | // /art/runtime/native/java_lang_Thread.cc
staticvoidThread_nativeCreate(JNIEnv* env, jclass, jobject java_thread,
jlong stack_size, jboolean daemon) {
// 创建 Native 线程
Thread::CreateNativeThread(env, java_thread, stack_size, daemon == JNI_TRUE);
}
|
CreateNativeThread
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 | // /art/runtime/thread.cc
voidThread::CreateNativeThread(JNIEnv* env, jobject java_peer,
size_tstack_size,boolis_daemon) {
Thread* self =static_cast<JNIEnvExt*>(env)->self;
Runtime* runtime = Runtime::Current();
// 1. 创建 ART 的 Thread 对象
Thread* child_thread =newThread(is_daemon);
// 2. 关联 Java 层的 Thread 对象(jpeer)
child_thread->tlsPtr_.jpeer = env->NewGlobalRef(java_peer);
// 3. 修正栈大小
stack_size = FixStackSize(stack_size);
// 4. 在 Java Thread 对象中设置 native peer 指针
env->SetLongField(java_peer, WellKnownClasses::java_lang_Thread_nativePeer,
reinterpret_cast<jlong>(child_thread));
// 5. 创建 JNI 环境
std::unique_ptr<JNIEnvExt> child_jni_env_ext(
JNIEnvExt::Create(child_thread, Runtime::Current()->GetJavaVM()));
// 6. 设置线程属性并创建 pthread
pthread_t new_pthread;
pthread_attr_t attr;
pthread_attr_init(&attr);
child_thread->tlsPtr_.tmp_jni_env = child_jni_env_ext.get();
// 7. 调用 pthread_create 创建线程
intpthread_create_result = pthread_create(
&new_pthread,//返回线程句柄
&attr,
Thread::CreateCallback,// 线程入口函数
child_thread// 传递给线程的参数
);
if(pthread_create_result == 0) {
child_jni_env_ext.release();
return;
}
// 创建失败的处理...
}
|
- 创建 ART 虚拟机的 Thread 对象
- 关联 Java 和 Native 的 Thread 对象(双向引用)
- 创建 JNI 环境,使线程能够调用 Java 代码
- 调用 pthread_create 创建真正的操作系统线程
Thread::CreateCallback
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 | // /art/runtime/thread.cc
void* Thread::CreateCallback(void* arg) {
Thread* self =reinterpret_cast<Thread*>(arg);
Runtime* runtime = Runtime::Current();
// 1. 附加到 ART 虚拟机
self->Init(runtime->GetThreadList(), runtime->GetJavaVM());
// 2. 初始化线程相关资源
self->InitCardTable();
self->InitTid();
self->InitAfterFork();
// 3. 调用 Java 层的 run() 方法
{
ScopedObjectAccess soa(self);
self->NotifyThreadBirth();
// 获取 Thread.run() 方法
ArtMethod* run_method =
WellKnownClasses::java_lang_Thread_run->GetArtMethod();
// 反射调用 run 方法
JValue result;
run_method->Invoke(self,
reinterpret_cast<uint32_t*>(&self->tlsPtr_.jpeer),
sizeof(void*),
&result,
"V");
}
// 4. 线程执行完毕,清理资源
self->NotifyThreadDeath();
returnnullptr;
}
|
- 线程启动后先初始化 ART虚拟机环境,通过反射调用 Java 层的 run() 方法执行完毕后进行资源清理
pthread_create分析
pthread_create在CreateNativeThread时被调用
1 2 3 4 5 6 | intpthread_create_result = pthread_create(
&new_pthread,//返回线程句柄
&attr,
Thread::CreateCallback,// 线程入口函数
child_thread// 传递给线程的参数
);
|
pthread_create` 会先得到一个`pthread_internal_t`结构体
pthread_create会先得到一个pthread_internal_t结构体
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 56 57 58 59 | // 1. 应用层调用
pthread_tthread;
pthread_create(&thread, NULL, my_function, my_arg);
// 2. pthread_create 内部实现
intpthread_create(pthread_t* thread_out,
constpthread_attr_t* attr,
void* (*start_routine)(void*),
void* arg) {
// 分配并初始化 pthread_internal_t
pthread_internal_t*thread=
reinterpret_cast<pthread_internal_t*>(
calloc(sizeof(pthread_internal_t), 1));
// 设置关键字段
thread->start_routine = start_routine;
thread->start_routine_arg = arg;
// 分配线程栈
thread->stack_base = mmap(...);
thread->stack_size = stack_size;
// 调用 clone 系统调用
intflags = CLONE_VM | CLONE_FS | CLONE_FILES |
CLONE_SIGHAND | CLONE_THREAD |
CLONE_SYSVSEM | CLONE_SETTLS |
CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID;
// 关键:thread 作为 TLS 参数传递给 clone
inttid = clone(__pthread_start,// 包装函数
thread->stack_top(),// 栈顶
flags,// 克隆标志
thread,// TLS (args[3])
&(thread->tid));// parent_tidptr
// 将 pthread_internal_t 加入全局链表
__pthread_internal_add(thread);
// 返回线程句柄
*thread_out =reinterpret_cast<pthread_t>(thread);
return0;
}
// 3. __pthread_start 包装函数
staticint__pthread_start(void* arg) {
pthread_internal_t*thread=
reinterpret_cast<pthread_internal_t*>(arg);
// 设置线程 ID
thread->tid = gettid();
// 调用真正的线程函数
void* result =thread->start_routine(thread->start_routine_arg);
// 线程退出
pthread_exit(result);
return0;
}
|
这个结构体为核心数据结构,包含了线程的所有信息
pthread_create是pthread库中的函数,通过syscall再调用到clone来请求内核创建线程
Linux进程管理
Linux创建进程采用fork()和exec()
- fork: 采用复制当前进程的方式来创建子进程,此时子进程与父进程的区别仅在于pid, ppid以及资源统计量(比如挂起的信号)
- exec:读取可执行文件并载入地址空间执行;一般称之为exec函数族,有一系列exec开头的函数,比如execl, execve等
fork过程复制资源包括代码段,数据段,堆,栈。fork调用者所在进程便是父进程,新创建的进程便是子进程;在fork调用结束,从内核返回两次,一次继续执行父进程,一次进入执行子进程。
进程创建
- Linux进程创建: 通过fork()系统调用创建进程
- Linux用户级线程创建:通过pthread库中的pthread_create()创建线程
- Linux内核线程创建: 通过kthread_create()
Linux线程,也并非”轻量级进程”,在Linux看来线程是一种进程间共享资源的方式,线程可看做是跟其他进程共享资源的进程。
fork, vfork, clone根据不同参数调用do_fork
- pthread_create: flags参数为 CLONE_VM, CLONE_FS, CLONE_FILES, CLONE_SIGHAND
- fork: flags参数为 SIGCHLD
- vfork: flags参数为 CLONE_VFORK, CLONE_VM, SIGCHLD
Fork流程图
进程/线程创建的方法fork(),pthread_create(),最终在linux都是调用do_fork方法。 当然还有vfork其实也是一样的, 通过系统调用到sys_vfork,然后再调用do_fork方法,该方法 现在很少使用,所以下图省略该方法。![]()
fork执行流程:
1. 用户空间调用fork()方法;
2. 经过syscall陷入内核空间, 内核根据系统调用号找到相应的sys_fork系统调用;
3. sys_fork()过程会在调用do_fork(), 该方法参数有一个flags很重要, 代表的是父子进程之间需要共享的资源; 对于进程创建flags=SIGCHLD, 即当子进程退出时向父进程发送SIGCHLD信号;
4. do_fork(),会进行一些check过程,之后便是进入核心方法copy_process.
flags参数
进程与线程最大的区别在于资源是否共享,线程间共享的资源主要包括内存地址空间,文件系统,已打开文件,信号等信息, 如下图蓝色部分的flags便是线程创建过程所必需的参数。![]()
fork采用Copy on Write机制,父子进程共用同一块内存,只有当父进程或者子进程执行写操作时会拷贝一份新内存。 另外,创建进程也是有可能失败,比如进程个数达到系统上限(32768)或者系统可用内存不足。
![]()
在安卓源码对应内容如上图所示
而现在我们需要去分析pthread_internal_t* 结构体中,在哪里存储的线程函数
adb pull /system/lib64/libc.so ./libc64.so![]()
搜索pthread_create
不要忘记了
1 2 3 4 5 6 | intpthread_create_result = pthread_create(
&new_pthread,//返回线程句柄
&attr,
Thread::CreateCallback,// 线程入口函数
child_thread// 传递给线程的参数
);![]()
|
我们向下追踪
发现a3的值赋值给了v54
![]()
所以偏移为0x60的地方为咱们线程函数的基址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | structpthread_internal_t {
void* next;// 0x00 - 链表指针
void* prev;// 0x08 - 链表指针
pid_t tid;// 0x10 - 线程 ID
pid_t cached_pid;// 0x14 - 缓存的进程 ID
// ... 省略一些字段 ...
pthread_mutex_t startup_mutex;// 0x88 - 启动互斥锁
boolstartup_flag;// 0x8C - 启动标志
void* mmap_base;// 0x90 (144) - mmap 分配的基地址
size_tmmap_size;// 0x98 (152) - mmap 分配的大小
void* (*start_routine)(void*);// 0x60 (96) - 线程入口函数(更正!)
void* start_routine_arg;// 0x68 (104) - 传递给线程函数的参数
// ... 其他字段 ...
};// 总大小:704 字节 (0x2C0)
|
我们再进入clone函数![]()
这个函数只是clone函数的包装器,真正的clone为![]()
如果返回值没问题,就调用__start_thread![]()
在这个函数,会初始化tid,以及调用线程函数,线程函数执行后,就退出线程
因此我们通过hook clone即可拦截线程!![]()
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 56 57 58 59 60 61 62 63 64 | console.log("启动反调试绕过...");
var anti_debug_offsets = [
0x4c574,
0x56c10,
0x54584,
0x5c3c4
];
function waitForModule() {
var module = Process.findModuleByName("libDexHelper.so");
if(module) {
console.log("找到 libDexHelper.so 基址:", module.base);
hookAntidebugFunctions(module.base);
}else{
console.log("等待 libDexHelper.so 加载...");
setTimeout(waitForModule, 100);
}
}
function hookAntidebugFunctions(base) {
console.log("开始Hook反调试函数");
var dummy_func =newNativeCallback(function(arg) {
return0;
},'int', ['pointer']);
anti_debug_offsets.forEach(function(offset, index) {
var func_addr = base.add(offset);
var hook_num = index + 1;
console.log("Hook 函数 #"+ hook_num +" 偏移:"+ ptr(offset) +" 地址:"+ func_addr);
try{
Interceptor.replace(func_addr, dummy_func);
console.log("replace 替换成功");
}catch(e1) {
console.log("replace 失败,尝试 attach");
try{
Interceptor.attach(func_addr, {
onEnter: function(args) {
console.log("函数 #"+ hook_num +" 被调用");
for(var i = 0; i < 8; i++) {
try{
args[i] = ptr(0);
}catch(e) {}
}
},
onLeave: function(retval) {
retval.replace(0);
console.log("返回值已改为0");
}
});
console.log("attach 拦截成功");
}catch(e2) {
console.log("attach 也失败:", e2.message);
}
}
});
console.log("所有函数Hook完成");
}
setTimeout(waitForModule, 500);
|
![]()