-
-
Frida过反调试实战
-
发表于: 1天前 933
-
frida初学者最近看了几篇文章,尝试跟着复现一下,并且了解一下frida API的用法(写在注释里了)。有的地方理解还是不到位,欢迎讨论哈
过老版爱加密(hook检测线程)
同样是在启动后就Process terminated的情况。(但要注意的是,检测到之后应用没有退出,frida被杀掉了)
第一步hook android_dlopen_ext,查看检测代码在哪个so文件中。
多次运行此脚本,都是停止在libexecmain.so
处。推测应该是libexecmain.so创建了检测线程,循环检测到就退出。(不是循环检测的话,那可以正常启动,然后frida附加进程,所以肯定是循环检测的)
接下来hook 线程创建的函数,查看是哪个so文件创建了检测线程,我们hook pthread_create函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | function hook_pthread_create(){ Interceptor.attach(Module.findExportByName( null , "pthread_create" ), { onEnter: function (args) { var module = Process.findModuleByAddress(ptr( this .returnAddress)) //this.returnAddress返回当前函数的地址,也就是谁调用的这个函数 if (module != null ) { console.log( "[pthread_create] called from" , module.name) } else { console.log( "[pthread_create] called from" , ptr( this .returnAddress)) } }, } ) } hook_pthread_create() |
可以看到创建线程的其实是libexec.so这个文件,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | try { var p_pthread_create = Module.findExportByName( "libc.so" , "pthread_create" ); var pthread_create = new NativeFunction(p_pthread_create, "int" , [ "pointer" , "pointer" , "pointer" , "pointer" ]); Interceptor.replace(p_pthread_create, new NativeCallback( function (ptr0, ptr1, ptr2, ptr3) { if (ptr1.isNull() && ptr3.isNull()) { console.log( "Possible thread creation for checking. Disabling it" ); return -1; } else { return pthread_create(ptr0, ptr1, ptr2, ptr3); } }, "int" , [ "pointer" , "pointer" , "pointer" , "pointer" ])); } catch (error) { console.log( "Error" , error) } |
直接hook掉线程创建不行,因为正常业务也有可能会调用线程创建,就算成功也不完善。我们加一个限定条件(限定libexec.so这个so创建新线程时才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 | //hook libexec.so的线程创建 //先保存原本的pthread_create函数,再自定义一个,最后替换 function patchPthreadCreate(){ let pthread_create = Module.findExportByName( null , "pthread_create" ) let org_pthread_create = new NativeFunction(pthread_create, "int" , [ "pointer" , "pointer" , "pointer" , "pointer" ]) //nativeFunction主动调用so中的函数 //new NativeFunction(address, returnType, argTypes[, abi]) //address为原函数地址 returnType为返回类型 argTypes为参数类型 //(这里的pointer为指针类型,分别对应线程属性、线程标识符、线程函数地址、线程函数) let my_pthread_create = new NativeCallback( function (a, b, c, d) { let m = Process.getModuleByName( "libexec.so" ); let base = m.base console.log(Process.getModuleByAddress(c).name) // 打印当前调用的库名 if (Process.getModuleByAddress(c).name == m.name) { console.log(m.name + " use pthread_create" ); return 0; } return org_pthread_create(a, b, c, d) }, "int" , [ "pointer" , "pointer" , "pointer" , "pointer" ]) //new NativeCallback(func, returnType, argTypes[, abi]) //func为自定义的JavaScript函数 abcd分别对应线程属性、线程标识符、线程函数地址、线程函数 Interceptor.replace(pthread_create, my_pthread_create) } |
还是不行,我们换一个方法,直接操作pthread_createa
中的内存地址,避免被检测。
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 | function patchPthreadCreateCaller(){ let pthread_create = Module.findExportByName( "libc.so" , "pthread_create" ) Interceptor.attach(pthread_create, { onEnter: function (args) { this .isHook = false let m = Process.getModuleByName( "libexec.so" ); let base = m.base console.log(Process.getModuleByAddress(args[2]).name) if (Process.getModuleByAddress(args[2]).name == m.name) { console.log( "绝对地址: " + args[2], "偏移量: " + args[2].sub(base), "返回地址偏移量: " + this .context.lr.sub(m.base)) // args[2] 线程函数的绝对地址。 // args[2].sub(base) 线程函数相对于目标模块基地址的偏移。 // this.context.lr.sub(m.base) 调用者的返回地址相对于目标模块基地址的偏移。lr代表返回值寄存器的地址。 if (args[2] >= base && args[2] < base.add(m.size)) { Memory.patchCode(args[2], 4, code => { //4 代表内存大小 //Memory.patchCode(address, size, apply) 安全修改内存地址 //address: 要修改的内存地址 size:要修改的内存大小 apply:自定义函数,用于修改内存 console.log( "Patching thread function at " + args[2]); //new Arm64Writer(codeAddress[, { pc: ptr('0x1234') }]) 将代码转为机器码 //这里传入的code代表要写入的地址区域 pc代表当前指令地址 是为了告诉 Arm64Writer,生成指令时,这块内存的起始地址就是 args[2]。 const cw = new Arm64Writer(code, {pc: args[2]}); cw.putLdrRegU64( 'x0' , 0); // 将x0寄存器加载为0,x0寄存器一般存第一个参数或返回值 (修改线程函数的入口代码,使其立即返回一个 0) cw.flush(); // 将修改后的代码写入内存 }); } } } }) } |
还是不行,推测是
- 使用
Memory.patchCode
,修改时可能已经触发了检测逻辑。 - 对于
pthread_create
传入的参数地址还有检测,如果不是指定的地址的话就重复创建。 - patchCode没生效(例如代码段不可写或时间窗口不够)。
那我们不修改内存地址,直接把pthread_create
函数替换掉,我们最后再用下最终方案
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 | function hook_pthread() { var pthread_create_addr = Module.findExportByName( 'libc.so' , 'pthread_create' ); console.log( "pthread_create_addr," , pthread_create_addr); var pthread_create = new NativeFunction(pthread_create_addr, "int" , [ "pointer" , "pointer" , "pointer" , "pointer" ]); Interceptor.replace(pthread_create_addr, new NativeCallback( function (a,b,c,d) { var m = Process.getModuleByName( "libexec.so" ); var base = m.base; var so_name = Process.getModuleByAddress(c).name; var so_path = Process.getModuleByAddress(c).path; var offset = c.sub(base); console.log( "so_name" , so_name, "offset" , offset, "path" , so_path, "parg2" , c); var PC = 0; if ((so_name.indexOf( "libexec.so" ) > -1) || (so_name.indexOf( "xxxx" ) > -1)) { console.log( "find thread func offset" , so_name, offset); // 循环检测偏移量,如果命中则不创建线程,只是打印anti bypss if ((0x4400c === offset)) { console.log( "anti bypass" ); } else if (0x44060 === offset) { console.log( "anti bypass" ); } else { PC = pthread_create(a,b,c,d); console.log( "ordinary sequence" , PC) } } else { PC = pthread_create(a,b,c,d); // console.log("ordinary sequence", PC) } return PC; }, "int" , [ "pointer" , "pointer" , "pointer" , "pointer" ])) } |
使用前。先把0x4400c === offset 改成 1 === offset,然后把打印出的地址都填进去。
成功了,但是……只有旧版本的加固方案可以,新版本疑似对pthread_create加了额外的检测,还需要去hook so文件(待确认)。
过梆梆
还是一样的,先查看检测的so文件
定位到这个libnllvm1717052779924.so
,同样的思路,hook线程创建函数
啥也没hook到,说明检测的内容还在线程创建之前(?),就是说没有通过创建线程进行检测,而是在文件代码中。
接下看下这个检测so文件有没有调用Jni_onload,为后续ida分析做准备把
打开ida,看下jni_onload的偏移量
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 | function hook_dlopen(soName = '' ) { Interceptor.attach(Module.findExportByName( null , "android_dlopen_ext" ), { onEnter: function (args) { var pathptr = args[0]; if (pathptr !== undefined && pathptr != null ) { var path = ptr(pathptr).readCString(); if (path.indexOf(soName) >= 0) { this .is_can_hook = true ; } } }, onLeave: function (retval) { if ( this .is_can_hook) { hook_JNI_OnLoad() } } } ); } function hook_JNI_OnLoad(){ let exportAddress = Module.findExportByName( "libnllvm1717052779924.so" , "JNI_OnLoad" ); Interceptor.attach(exportAddress, { onEnter: function (args) { console.log( "JNI_OnLoad called : " + exportAddress); } }); } setImmediate(hook_dlopen, "libnllvm1717052779924.so" ) |
运行下试试
调用了jni_onload
,那么检测代码很有可能在这里,并且jni_onload
不像.init_proc
函数一样,在dlopen
后已经执行完成,所以我们可以去hookJni_onload
。
没有打印,说明在Jni_onload调用之前,我们要去看init_xxxx函数,让frida代码在检测代码执行之前调用
需要在init_proc中找到一个可以hook的函数作为注入点,这里我发现了一个sprintf函数
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 | function hook_dlopen(soName = '' ) { Interceptor.attach(Module.findExportByName( null , "android_dlopen_ext" ), { onEnter: function (args) { var pathptr = args[0]; if (pathptr !== undefined && pathptr != null ) { var path = ptr(pathptr).readCString(); if (path.indexOf(soName) >= 0) { locate_init() } } } } ); } function locate_init() { var foundFirstMatch = false ; // 用于标记是否已匹配到第一次 Interceptor.attach(Module.findExportByName( null , "sprintf" ), { onEnter: function (args) { if (foundFirstMatch) return ; // 如果已经匹配到第一次,直接返回 var secmodule = Process.findModuleByName( "libnllvm1717052779924.so" ); console.log( "args1 = " + args[0]); console.log( "args2 = " + args[1]); console.log( "args3 = " + args[2].toInt32()); var formatString = args[1].readUtf8String(); console.log( "Format string: " + formatString); console.log( "==============================" ); if (formatString.indexOf( "/proc/%d/cmdline" ) >= 0 && args[0].toString() === "0x7fc82303f8" ) { // 这是.init_proc刚开始执行的地方,是一个比较早的时机点 console.log( "This is init_proc" ); foundFirstMatch = true ; // 设置标记,防止后续再触发 //hook_pthread_create(); } } } ); } function hook_pthread_create(){ var module = Process.findModuleByName( "libnllvm1717052779924.so" ); console.log( "libnllvm1717052779924.so --- " + module.base); Interceptor.attach(Module.findExportByName( "libc.so" , "pthread_create" ),{ onEnter(args){ let func_addr = args[2]; console.log( "The thread function address is " + func_addr); console.log( "调用的module是 : " + Process.findModuleByAddress(func_addr).name); if (func_addr < module.base.add(module.size) && func_addr > module.base){ console.log( "Offset is : " + func_addr.sub(module.base)); args[2].replace // bypass(); } console.log( "-----------------------------------" ); } }) } |
我们只hook第一次的调用,因为其他的都不是init_proc中的调用。
用这个偏移量替换if条件,现在我们就hook到了init_proc,在这里可以执行我们的代码,不会被检测到。
这里再执行下线程创建的枚举
发现了创建线程的函数,我们现在就把它hook掉,可以直接写入机器码,将检测函数nop掉。
还是寄了,给哥们整不会了。
试试老办法
我觉得应该已经过了这部分,只是还有其他检测。