Android Hook看这篇就够了!(建议收藏)
共 25484字,需浏览 51分钟
·
2022-09-18 00:50
大家好,我是皇叔,最近开了一个安卓进阶涨薪训练营,可以帮助大家突破技术&职场瓶颈,从而度过难关,进入心仪的公司。
详情见文章:没错!皇叔开了个训练营
来源:看雪论坛
看雪论坛作者ID:随风而行aa
一
前言
二
编译原理
1.编译过程
预处理阶段:预处理器(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)链接方式
(2)链接库
命名规范为libXXX.a
库函数会被连接进可执行程序,可执行文件体积较大
可执行文件运行时,不需要从磁盘载入库函数,执行效率较高
库函数更新后,需要重新编译可执行程序
命名规范为libXXX.so
库函数不被连接进可执行程序,可执行文件体积较小
可执行文件运行时,库函数动态载入
使用灵活,库函数更新后,不需要重新编译可执行程序
2.可执行文件(ELF)
(1)ELF文件结构
(2)GOT和PLT
(1)如果是在其他中间文件中已经定义了的函数,链接阶段可以直接重定位到函数地址,比如我们从头文件访问另一个函数。
(2)如果是在动态库中定义了的函数,链接阶段无法直接重定位到函数地址,只能生成额外的小片段代码,也就是PLT表,然后重定位到该代码片段。
三
NDK基础知识
1.Android so文件的类型
adb shell
cat /proc/cpuinfo
2.so文件加载
//加载的是libnative-lib.so,注意的是这边只需要传入"native-lib"
System.loadLibrary("native-lib");
//传入的是so文件完整的绝对路径
System.load("/data/data/应用包名/lib/libnative-lib.so")
public static void load(String pathName) {
Runtime.getRuntime().load(pathName, VMStack.getCallingClassLoader());
}
public static void loadLibrary(String libName) {
Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
}
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) {...
public static void load(String filename) {
Runtime.getRuntime().load0(VMStack.getStackClass1(), filename);
}
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
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);
}
四
各类hook技术原理分析
1.Xposed hook技术
2.Frida hook技术
accessflags = native
entry_point_fromjni = 自定义代码的入口
entry_point_from_quick_compiledcode = art_quick_generic_jni_trampoline函数的地址
entry_point_frominterpreter = artInterpreterToCompiledCodeBridge函数地址
3.inlinehook 技术
(1)基本原理
(2)inlineHook组成
(3)inlineHook实现
(4)Android-Inline-Hook和SandHook 技术
4.PLT/GOT hook技术
5.Unicorn hook技术
五
各类hook技术实操
1.Xposed hook实操
(1)环境安装
(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
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
fastboot flash recovery twrp-3.4.0-0-hammerhead.img
(2)Xposed插件编写
<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文件,
compile fileTree(includes:['*.jar'],dir:'libs')
替换成
provided fileTree(includes:['*.jar'],dir:'libs')
现在provided变为 compileOnly
如果使用compile,可以正常编译生成插件apk,但是当安装到手机上后,xposed会报错,无法正常工作
public class Xposed01 implements IXposedHookLoadPackage {
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);
}
}
}
2.frida hook实操
(1)环境安装
pip install frida==12.8.0
pip install frida-tools==5.3.0
pip install objection==1.8.4
frida --version
objection --help
(2)frida使用
常见的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 //打印具体方法的参数、返回值、堆栈信息
attach方式 frida -U com.example.test -l hook.js
spwan启动 frida -U -f com.example.test -l demo1.js --no-pause
3.inlinehook实操
(1)Android-lnine-Hook
<1>编写目标函数so文件
<2>导入文件
<3>修改配置文件
<4>编写hook代码
源码解析:
(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:函数名
我们对一个目标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实操
<1>导入文件
<2>配置环境
cmake {
arguments '-DBUILD_TESTING=OFF'
cppFlags "-frtti -fexceptions -Wpointer-arith"
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
<3>编写hook代码
(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实操
<1>获得so模块的加载地址
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表的位置
<3>定位到节表的地址
//读取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;
<4>定位到got表的位置和函数位置
//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);
}
}
}
}
5.Unicorn hook使用
六
实验总结
参考文献
参考书目:《程序员的自我修养——链接、装载与库》
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/
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
https://www.likecs.com/show-203321775.html
https://www.lmlphp.com/user/65342/article/item/709806/
为了防止失联,欢迎关注我防备的小号
微信改了推送机制,真爱请星标本公号👇