天天看点

Android 反调试技巧之Self-Debuging/proc 文件系统检测、调试断点探测

<a href="http://jaq.alibaba.com/community/art/show?articleid=849">android 反调试技巧之self-debuging/proc 文件系统检测、调试断点探测</a>

Android 反调试技巧之Self-Debuging/proc 文件系统检测、调试断点探测

首先,我们来看看bluebox security(一家移动数据保护的公司)所描述的反调试方法。gdvm是一个类型为dvmglobals的全局变量,用来收集当前进程所有虚拟机相关的信息,其中,它的成员变量vmlist指向的就是当前进程中的dalvik虚拟机实例,即一个javavmext对象。以后每当需要访问当前进程中的dalvik虚拟机实例时,就可以通过全局变量gdvm的成员变量vmlist来获得,避免了在函数之间传递该dalvik虚拟机实例。

gdvm的存在使得直接访问jdwp相关数据变得很容易。例如,gdvm。jdwpstate指向包含全局调试数据和函数指针的结构。操作数据会导致jdwp线程发生故障或崩溃。下图就是bluebox security的具体方法:

jniexport jboolean jnicall java_com_example_disable(jnienv* env, jobject dontuse ){

// gdvm==struct dvmglobals

gdvm.jdwpstate = null;

return jni_true;

}

不过,libart.so会将jdwp相关类的一些vtables导出为全局符号。但是到目前,我们还搞不清楚其中的原因,以及这是否正常,但是这个方法却给了我们修改jdwp线程提供了一些很好的提示。这其中就包括jdwpsocketstate和jdwpadbstate这两个分别通过网络套接字和adb端处理的jdwp连接:刚开始,反调试实现起来并不容易,因为没有指向重要数据结构的全局符号。虽然我们有一个指向主结构体的jdwpstate,但是 gjdwpstate只是一个本地符号,所以链接器不会解决不了这个问题。

Android 反调试技巧之Self-Debuging/proc 文件系统检测、调试断点探测

我们可以以各种方式覆盖这个指针,简单的归零它们不是一个好主意,因为这会让整个过程崩溃。于是,我们找到的一个使用“jdwpadbstate :: shutdown()”的地址来覆盖“jdwpadbstate :: processincoming()”地址的好方法,这个方法看起来如下:

#include &lt;jni.h&gt;

#include &lt;string&gt;

#include &lt;android/log.h&gt;

#include &lt;dlfcn.h&gt;

#include &lt;sys/mman.h&gt;

#include &lt;jdwp/jdwp.h&gt;#define log(fmt, ...) __android_log_print(android_log_verbose, "jdwpfun", fmt, ##__va_args__)// vtable structure. just to make messing around with it more intuitivestruct vt_jdwpadbstate {

unsigned long x;

unsigned long y;

void * jdwpsocketstate_destructor;

void * _jdwpsocketstate_destructor;

void * accept;

void * showmanyc;

void * shutdown;

void * processincoming;

};extern "c"jniexport void jnicall java_sg_vantagepoint_jdwptest_mainactivity_jdwpfun(

jnienv *env,

jobject /* this */) { void* lib = dlopen("libart.so", rtld_now); if (lib == null) {

log("error loading libart.so");

dlerror();

}else{ struct vt_jdwpadbstate *vtable = ( struct vt_jdwpadbstate *)dlsym(lib, "_ztvn3art4jdwp12jdwpadbstatee"); if (vtable == 0) {

log("couldn't resolve symbol '_ztvn3art4jdwp12jdwpadbstatee'.n");

}else { log("vtable for jdwpadbstate at: %08xn", vtable); // let the fun begin! unsigned long pagesize = sysconf(_sc_page_size);

unsigned long page = (unsigned long)vtable &amp; ~(pagesize-1); mprotect((void *)page, pagesize, prot_read | prot_write); vtable-&gt;processincoming = vtable-&gt;shutdown; // reset permissions &amp; flush cache mprotect((void *)page, pagesize, prot_read); }

一旦此功能运行,任何连接java的调试器都将断开连接,任何进一步的连接尝试都将失败。令人惊讶的是,目前使用这个方法可以进行反调试了,并没有在日志中进行任何解释:

pyramidal neuron:~ berndt$ adb jdwp2926pyramidal neuron:~ berndt$ adb forward tcp:7777 jdwp:2926pyramidal neuron:~ berndt$ jdb -attach localhost:7777java.io.ioexception: handshake failed - connection prematurally closed at com.sun.tools.jdi.sockettransportservice.handshake(sockettransportservice.java:136) at com.sun.tools.jdi.sockettransportservice.attach(sockettransportservice.java:232) at com.sun.tools.jdi.genericattachingconnector.attach(genericattachingconnector.java:116) at com.sun.tools.jdi.socketattachingconnector.attach(socketattachingconnector.java:90) at com.sun.tools.example.debug.tty.vmconnection.attachtarget(vmconnection.java:519) at com.sun.tools.example.debug.tty.vmconnection.open(vmconnection.java:328) at com.sun.tools.example.debug.tty.env.init(env.java:63) at com.sun.tools.example.debug.tty.tty.main(tty.java:1066)

这个方法相当隐秘的,即通过欺骗和隐藏实现,不过我们只尝试了进行adb端口连接,大家在使用这个方法时可能需要修补jdwpsocketstate,以防止java调试。

但是有一个问题:android是建立在linux上的,因此继承了ptrace系统调用。

ptrace 系统调从名字上看是用于进程跟踪的,它提供了父进程可以观察和控制其子进程执行的能力,并允许父进程检查和替换子进程的内核镜像(包括寄存器)的值。其基 本原理是: 当使用了ptrace跟踪后,所有发送给被跟踪的子进程的信号(除了sigkill),都会被转发给父进程,而子进程则会被阻塞,这时子进程的状态就会被 系统标注为task_traced。而父进程收到信号后,就可以对停止下来的子进程进行检查和修改,然后让子进程继续运行。

其原型为:

#include &lt;sys/ptrace.h&gt;

long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data)

ptrace有四个参数:

1.enum __ptrace_request request:指示了ptrace要执行的命令。

2.pid_t pid: 指示ptrace要跟踪的进程。

3.void *addr: 指示要监控的内存地址。

4.void *data: 存放读取出的或者要写入的数据。

ptrace是如此的强大,以至于有很多大家所常用的工具都基于ptrace来实现,如gdb,strace,jtrace和frida。其中一些工具甚至提供了虚拟机自省技术,为与java虚拟机进行交互提供了便利的后门。

许多过去的linux反调试技巧,例如监控proc文件系统和检测内存中的断点,一直都是android上常用的的方法。通常在恶意软件使用的另一种技术是自我调试。该方法利用了一个事实,即只有一个调试器可以随时附加到进程。我们来看一下这个工作原理。

在linux上,ptrace()系统调用用于观察和控制另一个进程(“tracee”)的执行,并检查和更改跟踪的内存和寄存器。它是实现断点调试和系统调用跟踪的主要手段,使用ptrace系统调用进行反调试的最明显的方法是对内存的分配和回收,然后调用ptrace(parent_pid)附加到父进程:

void anti_debug() {

child_pid = fork();

if (child_pid == 0)

{

int ppid = getppid();

int status;

if (ptrace(ptrace_attach, ppid, null, null) == 0)

waitpid(ppid, &amp;status, 0);

ptrace(ptrace_cont, ppid, null, null);

while (waitpid(ppid, &amp;status, 0)) {

if (wifstopped(status)) {

} else {

// process has exited for some reason

_exit(0);

如果按照上述方式来实现,子进程将继续跟踪父进程,直到父进程退出,从而导致将调试器附加到父进程时失败。我们可以通过将代码编译成jni函数并将其打包到我们在设备上运行的应用程序来验证。

假设一切顺利,我们现在就要尝试调试应用程序的逆向工程了。如下图所示,ps不是返回一个进程,而是返回相同命令行的两个进程:

root@android:/ # ps | grep -i anti

u0_a151 18190 201 1535844 54908 ffffffff b6e0f124 s sg.vantagepoint.antidebug

u0_a151 18224 18190 1495180 35824 c019a3ac b6e0ee5c s sg.vantagepoint.antidebug

google 这么多年来,已经把 android 做成了本质上无法分支(fork)的软件,开源只是名义上的,让我们来尝试使用gdbserver附加到父进程来进行验证:

root@android:/ # ./gdbserver --attach localhost:12345 18190

warning: process 18190 is already traced by process 18224

cannot attach to lwp 18190: operation not permitted (1)

exiting

如上图所示,仍然需要进行反向工程:

root@android:/ # kill -9 18224

现在让我们再试一次尝试附加gdbserver:

root@android:/ # ./gdbserver --attach localhost:12345 18190 attached; pid = 18190

listening on port 12345

ptrace调用通常常见的方法包括:

分别跟踪彼此的多个进程

跟踪运行过程,监视子进程

监视/ proc文件系统中的值,例如/ proc / pid / status中的tracerpid。

大家来看一下我们对上述方法的简单改进,在最初的fork()之后,我们在父进程中启动一个额外的线程来连续监视子进程的状态。根据应用程序是否已内置在调试或发布模式(根据manifest中的android:可调试标志),子进程将以下列方式之一运行:

1.在释放模式下,调用ptrace失败,子进程立即退出分段错误(退出代码11)。

2.在调试模式下,调用ptrace工作,子进程预计会无限运行下去。因此,对waitpid(child_pid)的调用不应该返回,如果有的话,会阻碍了整个进程组的运行。

完整的jni实现如下,通过添加jniexport(…)_ antidebug()作为本机方法,可以在自己的项目中自由使用它。

#include &lt;unistd.h&gt;

#include &lt;sys/wait.h&gt;

static int child_pid;

void *monitor_pid(void *) {

waitpid(child_pid, &amp;status, 0);

/* child status should never change. */

_exit(0); // commit seppuku

// process has exited

pthread_t t;

/* start the monitoring thread */

pthread_create(&amp;t, null, monitor_pid, (void *)null);

extern "c"

jniexport void jnicall

java_sg_vantagepoint_antidebug_mainactivity_antidebug(

jobject /* this */) {

anti_debug();

另外,我们将其打包成一个android应用程序,看看它是否有效。就像上文一样,运行应用程序的调试版本时会显示两个进程:

root@android:/ # ps | grep -i anti-debug

u0_a152 20267 201 1552508 56796 ffffffff b6e0f124 s sg.vantagepoint.anti-debug

u0_a152 20301 20267 1495192 33980 c019a3ac b6e0ee5c s sg.vantagepoint.anti-debug

但是,如果我们现在终止子进程,父进程也退出:

root@android:/ # kill -9 20301

root@android:/ # ./gdbserver --attach localhost:12345 20267

gdbserver: unable to open /proc file '/proc/20267/status'

cannot attach to lwp 20267: no such file or directory (2)

为了绕过这个进程,我们有必要稍微修改一下应用程序的进程,最简单的方法是使用nop将调用修改为_exit,或者在libc.so中hook函数_exit。

预防这种反调试其实有很多种方法,比如我们可以修补应用程序漏洞,防止vtable被篡改。如果你不能及时修复,那以后还会再次受到这种攻击。另外就是在其他情况下,使用xposed或frida修改内核模块可能更合适,我们给大家介绍2种方法:

1.修补反调试功能。通过简单地用nop指令覆盖来禁用不需要的行为。请注意,如果反调试机制已经经过深加工了,则可能需要更复杂的修补程序。

2.使用frida或xposed hook本地api,如ptrace()和fork(),或使用内核模块hook相关的系统调用。

本文来自合作伙伴“阿里聚安全”,发表于2017年04月14日 10:07.

阿里聚安全

阿里聚安全(http://jaq.alibaba.com)由阿里巴巴安全部出品,面向企业和开发者提供互联网业务安全解决方案,全面覆盖移动安全、数据风控、内容安全等维度,并在业界率先提出“以业务为中心的安全”,赋能生态,与行业共享阿里巴巴集团多年沉淀的专业安全能力。