首页
社区
课程
招聘
[原创]Android Hook技术学习——常见的Hook技术方案总结
2022-5-17 21:56 51744

[原创]Android Hook技术学习——常见的Hook技术方案总结

2022-5-17 21:56
51744

Android Hook技术学习——常见的hook技术方案

目录

一、前言

最近一段时间在研究Android加壳和脱壳技术,其中涉及到了一些hook技术,于是将自己学习的一些hook技术进行了一下梳理,以便后面回顾和大家学习。

 

本文第二节主要讲述编译原理,了解编译原理可以帮助进一步理解hook技术

 

本文第三节主要讲述NDK开发的一些基础知识

 

本文第四节主要讲述各类hook技术的实现原理

 

本文第五节主要讲述各hook技术的实现步骤和案例演示

二、编译原理

1.编译过程

image-20210918161352124

 

我们可以借助gcc来实现上面的过程:

1
2
3
4
5
6
7
8
9
10
预处理阶段:预处理器(cpp)根据以字符#开头的命令修给原始的C程序,结果得到另一个C程序,通常以.i作为文件扩展名。主要是进行文本替换、宏展开、删除注释这类简单工作。
    命令行:gcc -E hello.c hello.i
编译阶段:将文本文件hello.i翻译成hello.s,包含相应的汇编语言程序
汇编阶段:将.S文件翻译成机器指令,然后把这些指令打包成一种可重定位目标程序的格式,并把结果保存在目标文件.o中(汇编——>机器)
    命令行:gcc -c hello.c hello.o
链接阶段:hello程序调用了printf函数,链接器(Id)就把printf.o文件并入hello.o文件中,得到hello可执行文件,然后加载到存储器中由系统执行。
    函数库包括静态库和动态库
    静态库:编译链接时,把库文件代码全部加入可执行文件中,运行时不需要库文件,后缀为.a。
    动态库:编译链接时,不加入,在程序执行时,由运行时链接文件加载库,这样节省开销,后缀为.so。(gcc编译时默认使用动态库)
再经过汇编器和连接器的作用后输出一个目标文件,这个目标文件为可执行文件

这里我们对编译过程做了一个初步的讲解,详细大家可以去看《程序员的自我修养——链接、装载与库》一书,下面我们主要介绍链接方式、链接库、可执行目标文件几个基本概念。

(1)链接方式

静态链接:

1
对于静态库,程序在编译链接时,将库的代码链接到可执行文件中,程序运行时不再需要静态库。在使用过程中只需要将库和我们的程序编译后的文件链接在一起就可形成一个可执行文件。

缺点:

1
2
1、内存和磁盘空间浪费:静态链接方式对于计算机内存和磁盘的空间浪费十分严重。假如一个c语言的静态库大小为1MB,系统中有100个需要使用到该库文件,采用静态链接的话,就要浪费进100M的内存,若数量再大,那浪费的也就更多。
2.更新麻烦:比如一个程序20个模块,每个模块只有1MB,那么每次更新任何一个模块,用户都得重新下载20M的程序

动态链接:

1
由于静态链接具有浪费内存和模块更新困难等问题,提出了动态链接。基本实现思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将他们链接在一起形成一个完整的程序,而不是像静态链接那样把所有的程序模块都链接成一个单独的可执行文件。所以动态链接是将链接过程推迟到了运行时才进行。

例子:

1
同样,假如有程序1,程序2,和Lib.o三个文件,程序1和程序2在执行时都需要用到Lib.o文件,当运行程序1时,系统首先加载程序1,当发现需要Lib.o文件时,也同样加载到内存,再去加载程序2当发现也同样需要用到Lib.o文件时,则不需要重新加载Lib.o,只需要将程序2和Lib.o文件链接起来即可,内存中始终只存在一份Lib.o文件。

image-20210918161352124

 

优点:

1
2
3
4
1)毋庸置疑的就是节省内存;
2)减少物理页面的换入换出;
3)在升级某个模块时,理论上只需要将对应旧的目标文件覆盖掉即可。新版本的目标文件会被自动装载到内存中并且链接起来;
4)程序在运行时可以动态的选择加载各种程序模块,实现程序的扩展。

(2)链接库

我们在链接的过程中,一般会链接一些库文件,主要分为静态链接库和动态链接库。静态链接库一般为Windows下的.lib和Linux下的.a,动态链接库一般为Windows下的.dll和Linux下的.so,这里考虑到我们主要是对so文件hook讲解,下面我们主要介绍linux系统下的情况。

 

静态库:

1
2
3
4
命名规范为libXXX.a
库函数会被连接进可执行程序,可执行文件体积较大
可执行文件运行时,不需要从磁盘载入库函数,执行效率较高
库函数更新后,需要重新编译可执行程序

动态库:

1
2
3
4
命名规范为libXXX.so
库函数不被连接进可执行程序,可执行文件体积较小
可执行文件运行时,库函数动态载入
使用灵活,库函数更新后,不需要重新编译可执行程序

2.可执行文件(ELF)

目前PC平台比较流行的可执行文件格式主要是Windows下的PE和Linux下的ELF,它们都是COFF格式的变种。在Windows平台下就是我们比较熟悉的.exe文件,而Linux平台下现在便是统称的ELF文件。这里我们主要介绍一下Linux下的ELF文件。

 

ELF文件的类型:

1
2
3
可重定位目标文件:包含二进制代码和数据,其形式可以和其他目标文件进行合并,创建一个可执行目标文件。比如linux下的.o文件
可执行目标文件:包含二进制代码和数据,可直接被加载器加载执行。 比如/bin/sh文件
共享目标文件:可被动态的加载和链接。比如.so文件

ELF文件的结构:

 

elf文件在不同的平台上有不同的格式,在Unix和x86-64 Linux上称ELF:

(1)ELF文件结构

目标文件既要参与程序链接,又要参与程序执行:

 

image-20220517112124280

1
2
3
(1)文件开始处:是一个ELF头部(ELF Header),用来描述整个文件的组织。节区部分包含链接视图的大量信息:指令、数据、符号表、重定位信息等。
(2)程序头部表(Program Header Table):如果存在的话,会告诉系统如何创建进程映像。用来构造进程映像的目标文件必须具有程序头部表,可重定位文件不需要这个表。
(3)节区头部表(Section Header Table):包含了描述文件节区的信息,每个节区在表中都有一项,每一项给出诸如节区名称、节区大小这类信息。用于链接的目标文件必须包含节区头部表,其他目标文件可以有,也可以没有这个表。

下面我们来从分别从连接视角和程序执行的视角来看ELF文件:

 

image-20210918161352124

1
2
3
ELF Header:描述了描述了体系结构和操作系统等基本信息并指出Section Header Table和Program Header Table在文件中的什么位置
Program Header Table: 保存了所有Segment的描述信息;在汇编和链接过程中没有用到,所以是可有可无的
Section Header Table:保存了所有Section的描述信息;Section Header Table在加载过程中没有用到,所以是可有可无的

下面我们来看一张更加详细的ELF结构图

 

image-20210918161352124

 

从中我们可以详细的知道ELF文件各个字段的含义,其他字段的含义如下图

 

image-20210918161352124

(2)GOT和PLT

上面我们简单的分析了ELF的文件结构,而这里我们介绍一下其中两个重要的节表GOT(全局偏移表)PLT(程序链接表)

 

首先,我们需要理解为什么需要GOT表和PLT表

 

经过上面的分析,我们知道程序在经历了编译流程后,就来到了链接过程,链接过程就是将一个或者多个中间文件(.o文件)通过链接器将它们链接成一个可执行文件,主要要完成以下事情:

1
2
3
1.各个中间文之间的同名section合并
2.对代码段,数据段以及各符号进行地址分配
3.链接时重定位修正

但是当我们程序运行起来,glibc动态库也装载了,函数地址也确定了,那我们程序如何去调用动态库中的函数呢,这个时候就需要理解一下重定位的概念:

 

重定位:

1
2
3
4
5
1.链接重定位:将一个或多个中间文件(.o文件)通过链接器将它们链接成一个可执行文件,一般分为两种情况:
    1)如果是在其他中间文件中已经定义了的函数,链接阶段可以直接重定位到函数地址,比如我们从头文件访问另一个函数
    2)如果是在动态库中定义了的函数,链接阶段无法直接重定位到函数地址,只能生成额外的小片段代码,也就是PLT表,然后重定位到该代码片段
2.运行重定位:运行后加载动态库,把动态库中的相应函数地址填入GOT表,由于PLT表是跳转到GOT表的,这就构成了运行时重定位
3.延迟重定位:只有动态库函数在被调用时,才会进行地址解析和重定位工作,这时候动态库函数的地址才会被写入到GOT表项中

这里我们就可以明白流程,程序在加载动态库中函数时,需要两部分:

1
2
需要存放外部函数的代码段表(PLT表)
存放函数地址的数据表(GOT表)

这里我用一个实例加深大家的理解,例如程序在链接时发现scanf定义在动态库时,链接器生成一小段代码scanf_stub,这就是我们的PLT表,然后scanf_stub地址取代原来的scanf,因此程序此时就转换为链接scanf_stub,这个过程叫链接重定位,然后在运行时动态库glibc中的scanf_libc地址填入GOT表,然后程序通过scanf_stub访问到scanf_libc,这个过程叫运行时重定位。

 

讲到这里,其实我们对PLT和GOT表的作用已经了解了,PLT(程序链接表)就是链接时需要存放外部函数的数据段,GOT(全局偏移表)是存放函数地址的代码

 

PLT和GOT的结构:

1
2
3
4
5
6
PLT表中的第一项为公共表项,剩下的是每个动态库函数为一项,每项PLT都从对应的GOT表项中读取目标函数地址
 
GOT表中前3个为特殊项,分别用于保存 .dynamic段地址、本镜像的link_map数据结构地址和_dl_runtime_resolve函数地址
dynamic段:提供动态链接的信息,例如动态链接中各个表的位置
link_map:已加载库的链表,由动态库函数的地址构成的链表
_dl_runtime_resolve:在第一次运行时进行地址解析和重定位工作

根据操作系统规定不允许修改代码段,只能修改数据段,所以PLT表是不变的,GOT表是可以改变的

.plt 代码段 RE(可读,可执行) .plt section 实际就是通常所说的过程链接表(Procedure Linkage Table, PLT)
.plt.got 代码段 RE .plt.got section 用于存放 __cxa_finalize 函数对应的 PLT 条目
.got 数据段 RW(可读,可写) .got section 中可以用于存放全局变量的地址;.got section 中也可以用于存放不需要延迟绑定的函数的地址。
.got.plt 数据段 RW .got.plt section 用于存放需要延迟绑定的函数的地址
 

因此我们可以看一下程序调用PLT表和GOT表的逻辑

 

image-20220517105915852

 

最后我们来详细看一下程序调用函数的变化流程:

 

程序第一次调用函数时:

 

image-20220517105915852

 

此时第一步由函数调用跳入到PLT表中,然后第二步PLT表跳到GOT表中,可以看到第三步由GOT表回跳到PLT表中,这时候进行压栈,把代表函数的ID压栈,接着第四步跳转到公共的PLT表项中,第5步进入到GOT表中,然后_dl_runtime_resolve对动态函数进行地址解析和重定位,第七步把动态函数真实的地址写入到GOT表项中,然后执行函数并返回,此时GOT表中就存放了函数的真实地址

 

之后函数被调用时:

 

image-20220517105915852

 

第一步还是由函数调用跳入到PLT表,但是第二步跳入到GOT表中时,由于这个时候该表项已经是动态函数的真实地址了,所以可以直接执行然后返回

三、NDK基础知识

这里我们主要介绍Android中的so文件加载的原理,为后面hook技术讲解做铺垫:

1.Android so文件的类型

NDK开发的so不再具备跨平台特性,需要编译提供不同平台支持

 

image-20210919152200104

 

我们从官网可以得知so文件在不同架构下也不同,这里依次对应arm32位和64位,x86_32位和64位

 

我们可以使用指令查看我们手机的架构:

1
2
adb shell
cat /proc/cpuinfo

image-20210919155021588

2.so文件加载

Android中我们通常使用系统提供的两种API:System.loadLibrary或者System.load来加载so文件:

1
2
3
4
//加载的是libnative-lib.so,注意的是这边只需要传入"native-lib"
System.loadLibrary("native-lib");
//传入的是so文件完整的绝对路径
System.load("/data/data/应用包名/lib/libnative-lib.so")

System.loadLibrary()和System.load()的区别:

1
2
3
4
5
6
1)loadLibray传入的是编译脚本指定生成的so文件名称,一般不需要包含开头的lib和结尾的.so,而load传入的是so文件所在的绝对路径
2)loadLibrary传入的不能是路径,查找so时会优先从应用本地路径下(/data/data/${package-name}/lib/arm/)进行查找,不存在的话才会从系统lib路径下(/system/lib、/vendor/lib等)进行查找;而load则没有路径查找的过程
3)load传入的不能是sdcard路径,会导致加载失败,一般只支持应用本地存储路径/data/data/${package-name}/,或者是系统lib路径system/lib等这2类路径
4)loadLibrary加载的都是一开始就已经打包进apk或系统的so文件了,而load可以是一开始就打包进来的so文件,也可以是后续从网络下载,外部导入的so文件
5)重复调用loadLibrar,load并不会重复加载so,会优先从已加载的缓存中读取,所以只会加载一次
6)加载成功后会去搜索so是否有"JNI_OnLoad",有的话则进行调用,所以"JNI_OnLoad"只会在加载成功后被主动回调一次,一般可以用来做一些初始化的操作,比如动态注册jni相关方法等

源码分析:

 

Android 6.0:

 

[System.java] java.lang.System:

1
2
3
4
5
6
7
  public static void load(String pathName) {
        Runtime.getRuntime().load(pathName, VMStack.getCallingClassLoader());
   }
 
   public static void loadLibrary(String libName) {
       Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
}

[Runtime.java] java.lang.Runtime:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void load(String absolutePath, ClassLoader loader) {
        if (absolutePath == null) {
            throw new NullPointerException("absolutePath == null");
        }
        String error = doLoad(absolutePath, loader);
        if (error != null) {
            throw new UnsatisfiedLinkError(error);
        }
    }
public void loadLibrary(String nickname) {
        loadLibrary(nickname, VMStack.getCallingClassLoader());
    }
 
void loadLibrary(String libraryName, ClassLoader loader) {
        if (loader != null) {
            String filename = loader.findLibrary(libraryName);
            if (filename == null) {...

我们对比了Android6.0下的System.load和System.loadLibrary:

1
我们可以发现System.loadLibrary()中会修改类加载器,这个在我们后面hook过程可能会报错,而Runtime.loadLibray()中有重写的方法,则可以正确实现

Android 7.0:

 

[System.java] java.lang.System:

1
2
3
4
5
6
7
public static void load(String filename) {
      Runtime.getRuntime().load0(VMStack.getStackClass1(), filename);
  }
 
public static void loadLibrary(String libname) {
      Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
  }

[Runtime.java] java.lang.Runtime:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
synchronized void load0(Class fromClass, String filename) {
       if (!(new File(filename).isAbsolute())) {
           throw new UnsatisfiedLinkError(
               "Expecting an absolute path of the library: " + filename);        }
       if (filename == null) {
           throw new NullPointerException("filename == null");
       }
       String error = doLoad(filename, fromClass.getClassLoader());
       if (error != null) {
           throw new UnsatisfiedLinkError(error);
       }
   }
 
    public void loadLibrary(String libname, ClassLoader classLoader) {
       java.lang.System.logE("java.lang.Runtime#loadLibrary(String, ClassLoader)" +
                             " is private and will be removed in a future Android release");
      loadLibrary0(classLoader, libname);
   }

我们可以发现不同版本的区别:

1
Android 6.0采用的是loadLibrary,6.0之后都采用的是loadLibrary0; 同理 load函数也一样,6.0之后采用的是load0

同时我们分析了loadLibrary0:

1
2
3
1. classLoader存在时,通过classLoader.findLibrary(libraryName)来获取存放指定so文件的路径;
2. classLoader不存在时,则通过getLibPaths()接口来获取
3. 最终调用nativeLoad加载指定路径的so文件

四、各类hook技术原理分析

hook技术就是指截获进程对某个API函数的调用,使得API的执行流程转向我们实现的代码片段,从而实现我们要的功能,在Android中使用hook的方法有很多,常用的Xposed和frida hook技术、inlinehook技术、基于inlinehook的开源框架Sandhook、PLT/Got hook技术、以及当下模拟cpu的Unicorn的hook技术,下面我们将逐一介绍其原理。

1.Xposed hook技术

Xposed的基本原理,我在源码编译(3)——Xposed框架定制中已经给大家做了详细的讲解,其主要就是Android应用进程都是由 zygote 进程孵化而来,zygote对应的可执行程序就是app_process,posed 框架通过替换系统的 app_process 可执行文件以及虚拟机动态链接库,让 zygote 在启动应用程序进程时注入框架代码,进而实现对应用程序进程的劫持。

 

具体怎么实现hook技术,Xposed就是通过修改了Art虚拟机,将需要hook的函数注册为Native函数,当执行这一函数时,虚拟机会优先执行Native函数,然后执行java函数,这样就成功完成了函数的hook。

 

image-20210919155021588

 

具体实现流程:

 

在 Android 系统启动的时候, zygote 进程加载 XposedBridge 将所有需要替换的 Method 通过 JNI 方法 hookMethodNative 指向 Native 方法 xposedCallHandler , xposedCallHandler 在转入 handleHookedMethod 这个 Java 方法执行用户规定的 Hook Func

 

image-20220517133042720

 

dvmCallMethodV会根据accessFlags决定调用native还是java函数,因此修改accessFlags后,Dalvik会认为这个函数是一个native函数,便走向了native分支也就是说Xposed在对java方法进行hook时,先将虚拟机里面这个方法的Method的accessFlag改为native对应的值,然后将该方法的nativeFunc指向自己实现的一个native方法,这样方法在调用时,就会调用到这个native方法,接管了控制权

 

其他的就详细参考上篇文章了

2.Frida hook技术

frida 也是一种动态插桩工具,原理和Xposed hook一样,也是把java method转为native method,但是Art下的实现与Dalivk有所不同,这里就需要了解ART的运行机制,这里主要参考博客:Frida源码分析

 

ART 是一种代替 Dalivk 的新的运行时,它具有更高的执行效率。ART虚拟机执行 Java 方法主要有两种模式:quick code 模式和 Interpreter 模式

1
2
quick code 模式:执行 arm 汇编指令
Interpreter 模式:由解释器解释执行 Dalvik 字节码

即使是在quick code模式中,也有类方法可能需要以Interpreter模式执行。反之亦然。解释执行的类方法通过函数artInterpreterToCompiledCodeBridge的返回值调用本地机器指令执行的类方法;本地机器指令执行的类方法通过函数GetQuickToInterpreterBridge的返回值调用解释执行的类方法

 

这里引用博客中的一张图

 

image-20220517133042720

 

如图,对于一个native方法,ART虚拟机会先尝试使用quickcode的模式去执行,并检查ARTMethod结构中的entry_point_from_quick_compiledcode成员,这里分3种情况:

1
2
3
1.如果函数已经存在quick code, 则指向这个函数对应的 quick code的起始地址,而当quick code不存在时,它的值则会代表其他的意义;
2.当一个 java 函数不存在 quick code时,它的值是函数 artQuickToInterpreterBridge 的地址,用以从 quick 模式切换到 Interpreter 模式来解释执行 java 函数代码;
3.当一个 java native(JNI)函数不存在 quick code时,它的值是函数 art_quick_generic_jni_trampoline 的地址,用以执行没有quick code的 jni 函数

因此,frida将一个java method修改jni mthod 显然是不存在quick code,这时需要将entry_point_from_quick_compiledcode值修改为art_quick_generic_jni_trampoline 的地址

 

总结,frida把java method改为jni method,需要修改ARTMethod结构体中的这几个值:

1
2
3
4
accessflags = native
entry_point_fromjni = 自定义代码的入口
entry_point_from_quick_compiledcode = art_quick_generic_jni_trampoline函数的地址
entry_point_frominterpreter = artInterpreterToCompiledCodeBridge函数地址

3.inlinehook 技术

(1)基本原理

首先,我们先介绍一下什么是inline Hook:

1
2
inline Hook是一种拦截目标函数调用的方法,主要用于杀毒软件、沙箱和恶意软件。一般的想法是将一个函数重定向到我们自己的函数,以便我们可以在函数执行它之前和/或之后执行处理;这可能包括:检查参数、填充、记录、欺骗返回的数据和过滤调用。
hook是通过直接修改目标函数内的代码来放置,通常是用跳转覆盖的前几个字节,允许在函数进行任何处理之前重定向执行。

(2)inlineHook组成

1
2
3
hook:一个5字节的相对跳转,在被写入目标函数以钩住它,跳转将从被钩住的函数跳转到我们的代码
proxy:这是我们指定的函数(或代码),放置在目标函数上的钩子将跳转到该函数(或代码)
Trampoline:用于绕过钩子,以便我们可以正常调用钩子函数

(3)inlineHook实现

image-20210918161352124

 

从示意图上,我们可以这样理解:

1
我们将目标函数MessgeBoxA()中的地址拿出来,然后我们用重写的hook函数替换,然后我们执行完成之后,再回调到函数的执行地址出,保证程序的正常运行

image-20210919152200104

 

我们也可以通过上述示意图去理解inlinehook的基本原理

(4)Android-Inline-Hook和SandHook 技术

Android-lnline-Hook和SandHook都是基于inlinehook的两种开源框架,在Android中对native层hook,使用的比较常见,前者主要针对32位进行hook,后者即可以用于32位也可以用于64位,但是官方表示32位并未进行测试,所以应用在64位上仍然更多

4.PLT/GOT hook技术

前面我们已经很详细的讲述了全局偏移表(GOT)和动态链接表(PLT),Inline Hook能Hook几乎所有函数,但是兼容性较差,不能达到上线标准,相比于inlineHook,GOT Hook兼容性比较好,可以达到上线标准,但是只能Hook基于GOT表的一些函数

 

GOT/PLT Hook 主要是通过解析SO文件,将待hook函数在got表的地址替换为自己函数的入口地址,这样目标进程每次调用待hook函数时,实际上是执行了我们自己的函数

 

这里我们还要理解GOT表中含包含了导入表和导出表

1
2
导出表指将当前动态库的一些函数符号保留,供外部调用
导入表中的函数实际是在该动态库中调用外部的导出函数

例如导入表存放的是一些其他so的函数,例如libc的open,而导出表存放的是一些共其他so调用的函数,比如自己so中编写的函数,而无论导入表还是导出表基本都是针对导出函数,针对非导出函用inlinehook更常用一些

5.Unicorn hook技术

Unicore是一款非常优秀的跨平台模拟执行框架,该框架可以跨平台执行Arm, Arm64 (Armv8), M68K, Mips, Sparc, & X86 (include X86_64)等指令集的原生程序,通过模拟CPU,可以实现很多强大的功能,也可以实现函数级别的Hook

 

参考资料:无名大佬文章Unicorn 在 Android 的应用

 

nicorn 内部并没有函数的概念,它只是一个单纯的CPU, 没有HOOK_FUNCTION的callback,AndroidNativeEmu 中的函数级Hook 并不是真正意义上的Hook,它不仅能Hook存在的函数,还能Hook不存在的函数。AndroidNativeEmu 使用这种技术实现了JNI函数Hook、库函数Hook。 Jni函数是不存的,Hook它只是为了能够用Python 实现 Jni Functions。有一些库函数是存在的,Hook只是为了重新实现它

五、各类hook技术实操

1.Xposed hook实操

(1)环境安装

Xposed环境安装详细可以参考我写的Xposed系列文章,这里只是简单的总结一下:

1
2
3
4
5
6
7
8
9
10
11
12
(1) 4.4以下Android版本安装比较简单,只需要两步即可
    1.对需要安装Xposed的手机进行root
    2.下载并安装xposedInstaller,之后授权其root权限,进入app点击安装即可
    但是由于官网不在维护,导致无法直接通过xposedinstaller下载补丁包
2)Android 5.0-8.0 由于5.0后出现ART,所以安装步骤分成两个部分:xposed.zip
    XposedInstaller.apk,zip文件是框架主体,需要进入Recovery后刷入,apk文件用于Xposed管理
    1.完成对手机的root,并刷入reconvery(比如twrp),使用Superroot
    2.下载你对应的zip补丁包,并进入recovery刷入
    3.重启手机,安装xposedInstaller并授予root权限即可
    官网地址:https://dl-xda.xposed.info/framework/
3)由于Android 8.0后,Xposed官方作者没有再对其更新,我们一般就使用国内大佬riyu的Edxposed框架
    Magisk + riyu + Edxposed

这里我们用的是nexus5进行操作,简单演示一下android6.0的Xposed安装

 

资源准备:

1
2
3
4
asop镜像:https://developers.google.com/android/ota#hammerhead
twrp:     https://twrp.me/
xposed:   https://dl-xda.xposed.info/framework/
xposed installer https://repo.xposed.info/module/de.robv.android.xposed.installer

首先我们先下载n5镜像,然后刷机,这里我们已经安装就不再安装了

 

然后我们刷入 twrp-3.4.0-0-hammerhead.img

1
fastboot flash recovery twrp-3.4.0-0-hammerhead.img

image-20210919152200104

 

然后我们就可以进入recovery模式了

 

然后我们将Supersu拷贝进去,然后将Xposed-v89-sdk.zip拷贝进去

 

image-20210919152200104

 

image-20210919152200104

 

然后我们进入recovery模式,将两个文件依次刷入即可

 

接下来我们安装XposedInstall.apk,来管理Xposed

 

image-20210919152200104

 

如果我们开机后发现xposed框架没有激活,尝试再重启一下,我们可以看见

 

image-20210919152200104

 

这样我们的Xposed框架就成功安装了

(2)Xposed插件编写

Xposed插件编写的流程网上已经有很多了,这里我就简单的讲解一下

1
2
3
4
5
基本流程:
    (1)拷贝XposedBridgeApi.jar到新建工程的libs目录
    (2)修改app目录下的build.gradle文件,在AndroidManifest.xml中增加Xposed相关内容
    (3)新建hook类,编写hook代码
    (4)新建assets文件夹,然后在assets目录下新建文件xposed_init,在里面写上hook类的完整路径

首先,我们查找XposedBridgeApi.jar到新建工程的libs目录:

 

image-20210919152200104

 

然后,修改AndroidManifest.xml文件,在Application标签下增加内容如下:

1
2
3
4
5
6
7
8
9
<meta-data
    android:name="xposedmodule"   //是否配置为Xposed插件,设置为true
    android:value="true"/>
<meta-data
    android:name="xposeddescription"   //模块名称
    android:value="模块描述"/>
<meta-data
    android:name="xposedminversion"   //最低版本号
    android:value="54"/>

修改app目录下的build.gradle文件:

1
2
3
4
5
6
进入app目录下的build.gradle文件,   
    compile fileTree(includes:['*.jar'],dir:'libs')
    替换成
    provided fileTree(includes:['*.jar'],dir:'libs')
现在provided变为 compileOnly
如果使用compile,可以正常编译生成插件apk,但是当安装到手机上后,xposed会报错,无法正常工作

编写hook类:

 

我们新建一个hook类xposed01,并实现接口IXposedHookLoadPackage,并实现里面关键方法handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam),该方法会在每个软件被启动的时候回调,所以一般需要通过目标包名过滤

1
2
3
4
5
6
7
8
9
10
public class Xposed01 implements IXposedHookLoadPackage {
 
    @Override
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
        if(loadPackageParam.packageName.equals("com.example.xposedlesson2")){  //判断目标包名
            XposedBridge.log("XLZH"+loadPackageParam.packageName);  //打出包名的信息
            Log.i("Xposed01",loadPackageParam.packageName);
        }
    }
}

新建assets文件夹,然后在assets目录下新建文件xposed_init,在里面写上hook类的完整路径

 

image-20210919152200104

 

这里面可以写多个hook类,每个类写一个,我们就完成了基本的Xposed框架的编写

 

最后勾选模块,并重启即可生效

 

image-20210919152200104

 

image-20210919152200104

 

我们可以发现我们的xposed插件生效了,将我们系统中进程名打印出来了,说明hook成功了

2.frida hook实操

(1)环境安装

frida安装,使用frida过程中我们可以安装objection来进一步助力我们的hook工作,这个参考肉丝大佬的知识星球

 

工具安装(也可以选用其他版本):

1
2
3
pip install frida==12.8.0
pip install frida-tools==5.3.0
pip install objection==1.8.4

安装成功后,查看frida和objection,确定版本正确

1
2
frida --version
objection --help

然后将frida_server推送到/data/local/tmp下,并启动:(下载地址:https://github.com/frida/frida/releases)

 

image-20220517153822302

(2)frida使用

然后我们就可以使用自动化工具objection和编写js脚本进行hook了

 

objection使用(详细参考肉丝大佬github的教程):

1
2
3
4
5
6
7
8
9
10
常见的hook命令:
objection -g com.android.settings explore  //注入设置应用
android hooking list activities  //查看Activity,service相同
android intent launch_activity com.android.settings.DisplaySettings  //实现Activity跳转
android heap search instances com.android.settings.DisplaySettings   //搜索类的实例
android heap execute 0x2526 getPreferenceScreenResId    //主动调用实例
android hooking list classes  //列出内存中所有类
android hooking search methods display  //列出内存中所有的方法
android hooking watch class android.bluetooth.BluetoothDevice  //hook相关类的所有方法
android hooking watch class_method android.bluetooth.BluetoothDevice.getName --dump-args --dump-return --dump-backtrace  //打印具体方法的参数、返回值、堆栈信息

编写脚本:

 

image-20220517154213936

 

启动方式:

1
2
attach方式 frida -U com.example.test -l hook.js
spwan启动 frida -U -f com.example.test -l demo1.js --no-pause

这样我们就可以成功注入了,更加复杂的脚本编写可以参考frida博客

 

详细案例实操,这里可以参考之前我的文章:Android恶意样本分析——frida破解三层锁机样本

3.inlinehook实操

这里我们分别实现基于inlinehook的两个开源框架的具体使用方法

(1)Android-lnine-Hook

开源地址:https://github.com/ele7enxxh/Android-Inline-Hook

 

该框架只能针对32位的so文件进行hook

 

我们对so文件进行hook时,可以按照如下步骤进行:

1
2
3
4
1)查看so文件中的目标函数
2)编写Xposed hook代码,hook目标程序
3)编写so层hook代码,hook so中的函数地址
4)链接Java层和so层
<1>编写目标函数so文件

image-20220517160033161

 

我们编写案例,很明显这里会打印失败,然后我们使用inline-hook框架进行hook

<2>导入文件

我们将该框架中如下文件导入我们的项目中

 

我们需要使用inlineHook文件夹,并把这些文件直接拷贝到我们的工作目录:

 

image-20210919152200104

 

image-20210919152200104

<3>修改配置文件

image-20220517160824232

<4>编写hook代码

我们导入inlinehook头文件就可以开始编写hook代码了

 

image-20220517160918834

 

编译,报错:

 

image-20220517163117937

 

这是因为框架仅仅针对32位,所以我们需要在配置文件里面指定一下

 

image-20220517163519977

 

然后编译,发现能正常通过

 

首先声明hook的就函数,然后编写对应的新函数,这里我们hook的是strstr函数

 

image-20220517185051670

 

然后调用inlinehook进行hook

 

image-20220517185429734

 

最后我们发现就可以成功的hook

 

image-20220517185538606

 

代码分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
源码解析:
    1)dlopen:该函数将打开一个新库,并把它装入内存
        void *dlopen(const char *filename, int flag);
        参数1:文件名就是一个动态库so文件,标志位:RTLD_NOW 的话,则立刻计算;设置的是 RTLD_LAZY,则在需要的时候才计算
        libc.so是一个共享库
        ======================
        参数中的 libname 一般是库的全路径,这样 dlopen 会直接装载该文件;如果只是指定了库名称,在 dlopen 会按照下面的机制去搜寻:
        根据环境变量 LD_LIBRARY_PATH 查找
        根据 /etc/ld.so.cache 查找
        查找依次在 /lib 和 /usr/lib 目录查找。
        flag 参数表示处理未定义函数的方式,可以使用 RTLD_LAZY 或 RTLD_NOW 。 RTLD_LAZY 表示暂时不去处理未定义函数,先把库装载到内存,等用到没定义的函数再说; RTLD_NOW 表示马上检查是否存在未定义的函数,若存在,则 dlopen 以失败告终。
        参考链接:https://blog.nowcoder.net/n/5b2c04bbcccf431e9f1ab34aa02717fe
        =======================
    2)dlsym:在 dlopen 之后,库被装载到内存。 dlsym 可以获得指定函数( symbol )在内存中的位置(指针)。
         void *dlsym(void *handle,const char *symbol);
         参数1:文件句柄  参数2:函数名

inlinehook框架使用正确姿势:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
我们对一个目标so文件hook步骤如下:
    1)我们获取so的handler,使用dlopen函数
        void* libhandler = dlopen("libc.so",RTLD_NOW);
    2)我们获取hook目标函数的地址,使用dlsym函数
        void* strstr_addr = dlsym(libhandler,函数名);
    3)声明原来的函数
        void* (*oldmethod)(char*,char*); //这个格式需要参考hook的函数
        声明现在的函数
        void* newmethod(char* a,char* b){
            return (void *)oldmethod(a,b);
        }
    3)使用registerInlinehook进行重定向,将hook函数地址重定向我们编写的新函数上
        (registerInlineHook((uint32_t) strstr_addr, (uint32_t) new_strstr, (uint32_t **) &old_strstr) != ELE7EN_OK
        //参数一:hook函数的地址 参数二:替换函数的地址  参数3:用来保存原来函数的地址
    5)我们判断我们的hook操作是否成功,并且再次调用实现hook
        (inlineHook((uint32_t) strstr_addr) == ELE7EN_OK)

(2)SandHook实操

因为上面使用inline框架只支持32位,所以这里我们用SandHook实现对64位native函数的hook,sandHook既支持32位、又支持64位

 

开源地址:https://github.com/asLody/SandHook

 

同样是上面的案例,这里我们使用SandHook进行实操

<1>导入文件

我们此路径下SandHook/nativehook/src/main/cpp/文件全部导入

 

image-20220517191708611

<2>配置环境

首先我们在CMakeList中加入c文件

 

image-20220517192031241

 

然后在java代码中修改导入的so库

 

image-20220517192224416

 

直接编译,报错:

 

image-20220517192315719

 

然后我们同理将配置信息加入:

1
2
3
4
5
cmake {
    arguments '-DBUILD_TESTING=OFF'
    cppFlags "-frtti -fexceptions -Wpointer-arith"
    abiFilters 'armeabi-v7a', 'arm64-v8a'
}

再次编译成功

<3>编写hook代码

SandHook使用和上面inlinehook框架基本一样

 

首先声明旧的函数,编写新的函数(目标函数strstr)

 

image-20220517193411921

 

然后进行hook

 

image-20220517193556517

 

最后发现可以成功hook

 

image-20220517193629093

 

SandHook使用姿势:

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
1)导包,将SandHook中cpp文件夹下的包全部导入到项目中,并修改CMakeLists.txt中添加native.cpp, 修改java层导入so库为sandHook-native
2)配置相关的环境
    在配置文件build.gradle中配置
    externalNativeBuild {
          cmake {
        arguments '-DBUILD_TESTING=OFF'
        cppFlags "-frtti -fexceptions -Wpointer-arith"
        abiFilters 'armeabi-v7a', 'arm64-v8a'
          }
        }
3)编译可以成功通过
4)使用
    const char * libc = "/system/lib64/libc.so";
    old_fopen = reinterpret_cast<void *(*)(char *, char *)>(SandInlineHookSym(libc, "fopen",
                                                                         reinterpret_cast<void *>(new_fopen)));
参数2:hook的函数 参数3:新的函数
 
添加原理hook旧函数的声明
void* (*old_fopen)(char*,char*);
实现新的函数功能
void* new_fopen(char* a,char* b){
    __android_log_print(6,"windaa","I am from new open %s",a);
    return old_fopen(a,b);
}
5)运行测试是否成功启动

4.PLT/GOT hook实操

前面我们已经介绍了Got表hook的原理,下面我们实例操作一下导入表函数的hook

 

参考博客:https://www.cnblogs.com/goodhacker/p/9306997.html

 

原理:

1
通过解析elf格式,分析Section header table找出静态的.got表的位置,并在内存中找到相应的.got表位置,这个时候内存中.got表保存着导入函数的地址,读取目标函数地址,与.got表每一项函数入口地址进行匹配,找到的话就直接替换新的函数地址,这样就完成了一次导入表的Hook操作了

image-20220517193556517

 

首先,我们编写demo

 

image-20220517193556517

 

我们编译后使用010Editor打开libnative-lib.so

 

image-20220517193556517

 

image-20220517193556517

 

然后我们用ida打开,并直接跳转到该地址

 

image-20220517193556517

 

在got表中我们找到对应的mywin0函数

 

image-20220517193556517

<1>获得so模块的加载地址

我们可以使用/proc/self/maps去获得so模块的加载地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
char line[1024];
    int *start;
    int *end;
    int n=1;
    //1.拿到so的起始地址
//    749e5d7000-749e5db000 r--p 000f4000 103:09 441                           /system/bin/linker64
//    749e5db000-749e5dc000 rw-p 000f8000 103:09 441                           /system/bin/linker64
    FILE *fd = fopen("/proc/self/maps","r");
    while (fgets(line,sizeof(line),fd)){
        if(strstr(line,"libnative-lib.so")){
            __android_log_print(6,"windaa","%s",line);
            if(n==1){
                start = reinterpret_cast<int *>(strtoul(strtok(line, "-"),NULL,16));
                end = reinterpret_cast<int *>(strtoul(strtok(NULL, " "),NULL,16));
            }
            else{
                strtok(line,"-");
                end = reinterpret_cast<int *>(strtoul(strtok(NULL, " "),NULL,16));
            }
            n++;
        }
    }
<2>找到got表的位置

我们首先根据段头找到section_header的首地址

 

image-20220517193556517

 

image-20220517193556517

 

然后我们遍历这个表就可以找到.got,然后根据got表地址再轮训找到函数地址

 

因为这种方法不能在内存中直接找到段头,内存中会抹去段头,所以我们可以通过加载so文件来定位

 

image-20220517193556517

<3>定位到节表的地址

然后我们来获得节表的地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//读取elf文件
    Elf64_Ehdr ehd;
    int fp =open("/data/local/tmp/libnative-lib.so", O_RDONLY);
    if(fp == -1){
        __android_log_print(4,"windaa","%s","error");
    }
    //读取elf文件的文件头
    read(fp,&ehd,sizeof(Elf64_Ehdr));
    //读取节表的地址
    unsigned long shof = ehd.e_shoff;
    //读取节表的数量
    int shnum = ehd.e_shnum;
    //读取每个节表的大小
    int shsize = ehd.e_ehsize;
    //记录一下str表的偏移,主要是获取后面got的字符串值
    int shstr = ehd.e_shstrndx;

我们打印一下此事shof的值,验证一下节表的地址

 

image-20220517193556517

 

这里可以发现成功读取

<4>定位到got表的位置和函数位置

然后我们拿到字符串的偏移值进行定位到got表,再进一步定位到函数

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
//2.拿到字符串表
    Elf64_Shdr shdr;
    //定位字符串,节表地址加字符串表偏移×节表个数
    lseek(fp,shof+shstr*shsize,SEEK_SET);
    //此时节表就定位到字符串表开头
    read(fp,&shdr,shsize);
    //分配一个字符串表大小
    char* strtable = (char *)malloc(shdr.sh_size);
    __android_log_print(6,"windaa","shdrsize %p",shdr.sh_offset);
    //将字符串片指针移动到0x34104
    lseek(fp,shdr.sh_offset,SEEK_SET);
    read(fp,strtable,shdr.sh_size);
    //将指针移动到节表开头
    lseek(fp,shof,SEEK_SET);
 
    //遍历查找到got
    for(int i=0;i<shnum;i++){
        //从节表开头开始读取字符串,每次读取一个节表
        read(fp,&shdr,shsize);
        //通过节表的索引找到字符串表中对应的值
        if(strcmp(&strtable[shdr.sh_name], ".got")==0){
            //定位到got表的地址
            int* saddr = start+shdr.sh_addr/4;
            //整个got表的大小
            int size = shdr.sh_size;
            //遍历got表中的函数
            for(int j=0;j<size;j=j+8){
                uint64_t  value = *(uint64_t *)(saddr + j / 4);
                //找到mywind的地址
                if(reinterpret_cast<uint64_t>(mywin0) == value)
                {
                    __android_log_print(6,"windaa","value %p",value);
                    //替换mywind地址
                    // 获取当前内存分页的大小
                    uint64_t page_size = getpagesize();
                    // 获取内存分页的起始地址(需要内存对齐)
                    //page要保护的是函数的绝对地址,而不是相对地址
                    uint64_t entry_page_start = (uint64_t)(saddr+j/4) & (~(page_size - 1));
                    // 修改内存属性为可读可写可执行
                    if(mprotect((uint64_t*)entry_page_start, page_size, PROT_READ | PROT_WRITE | PROT_EXEC) == -1){
                        __android_log_print(6,"windaa","%s","mprotect failed");
                    }
                    value = (uint64_t)mywin1;
                    //将mywind0函数的地址换成mywind1函数的地址
                    memcpy((saddr+j/4),&value,16);
                }
 
 
 
            }
        }
    }

image-20220517193556517

 

这里我们就可以发现成功的hook

 

got hook使用姿势:

1
2
3
4
5
1)使用/proc/self/maps去获得so模块的加载地址
2)使用ElfHeader找到Section的首地址,并计算offset和size来获取StringTable
3)找到got表位置,计算其内存位置,并指针指向got表首地址
4)遍历got表中的函数,找到要hook的函数,使用mprotect进行hook
5)将hook的函数地址替换为我们定义的函数地址

5.Unicorn hook使用

这里我们简单了解一下基于unicorn的框架Unidbg的hook使用

 

开源地址:https://github.com/zhkl0228/unidbg

 

这里我们直接idea将项目拉取下来,然后等下项目环境配置完成

 

image-20220517214320334

 

配置完成后,我们直接启动里面的示例代码查看hook效果

 

image-20220517214506632

 

这里unidbg使用了xHook,xHook是一种PLT hook的方式,当然这只是unidbg强大功能其中的一种,也是hook技术中一种,这里就简单介绍到这,后续再详细讲如何使用

 

unidbg使用参考博客:https://www.qinless.com/670

六、实验总结

本文从程序加载的原理出发,讲解了当下常用的一些基本的hook方式和手段,后续对其中一些hook方式再次深入讲解,实验的一些样本和代码会上传到知识星球和github,文章参考学了了很多大佬的文章和大佬星球的内容,参考文献放在末尾,有什么问题,就请各位大佬一一指出了。

 

github的地址:github

参考文献

参考书籍:

1
参考书目:《程序员的自我修养——链接、装载与库》

GOT和PLT:

1
2
3
4
https://www.geek-share.com/detail/2774116640.html
https://www.jianshu.com/p/0ac63c3744dd
https://www.zhihu.com/question/21249496
https://www.codeleading.com/article/37234101170/

hook技术

1
2
3
4
5
6
7
8
https://zhuanlan.zhihu.com/p/389889716
https://mabin004.github.io/2018/07/31/Mac%E4%B8%8A%E7%BC%96%E8%AF%91Frida/
https://zhuanlan.zhihu.com/p/269441842
https://blog.csdn.net/sdoyuxuan/article/details/78481239
https://www.cnblogs.com/codingmengmeng/p/6046481.html
https://blog.csdn.net/sssssuuuuu666/article/details/78788369
https://www.malwaretech.com/2015/01/inline-hooking-for-programmers-part-1.html
https://juejin.cn/post/6844903993668272141

got/plt hook:

1
2
https://www.likecs.com/show-203321775.html
https://www.lmlphp.com/user/65342/article/item/709806/

[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2022-6-16 10:55 被随风而行aa编辑 ,原因:
收藏
点赞51
打赏
分享
打赏 + 100.00雪花
打赏次数 1 雪花 + 100.00
 
赞赏  Editor   +100.00 2022/06/20 恭喜您获得“雪花”奖励,安全圈有你而精彩!
最新回复 (43)
雪    币: 1367
活跃值: (2121)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
危楼高百尺 1 2022-5-18 10:38
2
0
第一时间来支持郭老板
雪    币: 4633
活跃值: (2054)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
hekes 2022-5-18 11:21
3
1
先mark,回头在看
雪    币: 7809
活跃值: (21266)
能力值: ( LV12,RANK:550 )
在线值:
发帖
回帖
粉丝
随风而行aa 10 2022-5-18 14:29
4
0
危楼高百尺 第一时间来支持郭老板
温总
雪    币: 7809
活跃值: (21266)
能力值: ( LV12,RANK:550 )
在线值:
发帖
回帖
粉丝
随风而行aa 10 2022-5-18 14:29
5
0
hekes 先mark,回头在看
可以哦
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_zpajwtis 2022-5-18 14:47
6
0
学习下~
雪    币: 7809
活跃值: (21266)
能力值: ( LV12,RANK:550 )
在线值:
发帖
回帖
粉丝
随风而行aa 10 2022-5-18 18:28
7
0
mb_zpajwtis 学习下~
可以可以
雪    币: 427
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
Kwork 2022-5-19 17:14
8
0
请教下frida hook内部类的非静态变量怎么处理呢?这样貌似获取不到
Java.choose("android.view.WindowManager$LayoutParams", {
        onMatch: function (instance) {
            console.log('‘LayoutParams flags: ’' + instance.flags.value)
        },
        onComplete: function () {
        }
    });
雪    币: 7809
活跃值: (21266)
能力值: ( LV12,RANK:550 )
在线值:
发帖
回帖
粉丝
随风而行aa 10 2022-5-23 11:06
9
0
Kwork 请教下frida hook内部类的非静态变量怎么处理呢?这样貌似获取不到 Java.choose("android.view.WindowManager$LayoutParams" ...
这种不行 尝试反射获取该类下面的声明的所有类,进行判别后再hook试试
雪    币: 33
活跃值: (74)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
BlackMaGou 2022-5-26 08:45
10
0
菜鸟看的一脸懵
雪    币: 422
活跃值: (2614)
能力值: ( LV4,RANK:45 )
在线值:
发帖
回帖
粉丝
t0hka1 1 2022-5-26 11:11
11
0
mark
雪    币: 7809
活跃值: (21266)
能力值: ( LV12,RANK:550 )
在线值:
发帖
回帖
粉丝
随风而行aa 10 2022-5-26 19:45
12
0
BlackMaGou 菜鸟看的一脸懵
雪    币: 7809
活跃值: (21266)
能力值: ( LV12,RANK:550 )
在线值:
发帖
回帖
粉丝
随风而行aa 10 2022-5-26 19:46
13
0
雪    币: 411
活跃值: (2223)
能力值: ( LV3,RANK:35 )
在线值:
发帖
回帖
粉丝
轻快笑着行 2022-6-1 14:37
14
0
mark
雪    币: 7809
活跃值: (21266)
能力值: ( LV12,RANK:550 )
在线值:
发帖
回帖
粉丝
随风而行aa 10 2022-6-1 16:48
15
0
雪    币: 62
活跃值: (545)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
万里星河 2022-6-3 14:05
16
0
看大佬的文章 如沐春风
雪    币: 23
活跃值: (424)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
林蓝 2022-6-3 15:59
17
0
大佬出品 必属精品
雪    币: 7809
活跃值: (21266)
能力值: ( LV12,RANK:550 )
在线值:
发帖
回帖
粉丝
随风而行aa 10 2022-6-4 14:07
18
0
万里星河 看大佬的文章 如沐春风
谢谢支持
雪    币: 7809
活跃值: (21266)
能力值: ( LV12,RANK:550 )
在线值:
发帖
回帖
粉丝
随风而行aa 10 2022-6-4 14:07
19
0
林蓝 大佬出品 必属精品
谢谢支持
雪    币: 2063
活跃值: (3823)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
lhxdiao 2022-6-7 16:32
20
0
大佬,希望可以出一期ptrace原理的文章
雪    币: 7809
活跃值: (21266)
能力值: ( LV12,RANK:550 )
在线值:
发帖
回帖
粉丝
随风而行aa 10 2022-6-8 08:59
21
0
lhxdiao 大佬,希望可以出一期ptrace原理的文章
可以的
雪    币: 166
活跃值: (334)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
AppTen 2022-6-14 11:48
22
0
先点赞、收藏,后观看!这是美德,大佬写文章不容易。
雪    币: 7809
活跃值: (21266)
能力值: ( LV12,RANK:550 )
在线值:
发帖
回帖
粉丝
随风而行aa 10 2022-6-14 12:34
23
0
AppTen 先点赞、收藏,后观看!这是美德,大佬写文章不容易。
谢谢支持
雪    币: 1000
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_susbpfxl 2022-6-21 15:00
24
0
大佬真是厉害!学习了
雪    币: 7809
活跃值: (21266)
能力值: ( LV12,RANK:550 )
在线值:
发帖
回帖
粉丝
随风而行aa 10 2022-6-21 15:59
25
0
mb_susbpfxl 大佬真是厉害!学习了
游客
登录 | 注册 方可回帖
返回