文章目录
- 前言
- Hook So
- 有导出so层hook
- 无导出so层hook
- So层实战
- hook脚本的编写
- hook脚本的效果
- 总结
前言
我在前面的一篇博客 CTF逆向-EasySo世界SO层反汇编 中记录了对一道 CTF 逆向题目的 Android APP 的 So 层函数进行基础的逆向分析的过程,通过 IDA 反汇编查看 So 层代码并分析获得了 Flag 值。
生命在于折腾~本文将记录尝试通过 Frida hook 目标 APP 的 So 层函数并直接修改返回值(如同以前熟悉的 hook Java 层的函数一样),从而直接绕过对应的函数校验。
Hook So
在对目标 APP 进行 Hook 之前,先来学习下大佬 “移动安全头” 的文章 frida hook native / frida hook so层 实例代码讲解,该文介绍了 so 层代码的 hook 方法,而且大佬很贴心还在 B 站录了视频 移动安全头BiliBili,读者可学习一下。
Frida 是一个轻便好用的工具,不仅支持 Java 层的 hook,同样支持 so 层的 hook。那么 frida hook so 是如何实现的呢?其原理是通过内存地址进行函数 hook,可以将 hook so 层的实例根据场景的不同分为“有导出”和“无导出”两类。
有导出 | 无导出 |
函数名可以在导出表找到,通过导出表的数据结构,用函数名称进行函数的定位 | 函数名在导出表找不到,这里需要根据函数特征(比如字符串等),手动搜索关键字符串定位函数地址 |
有导出so层hook
【原理】通过导出表的结构找到函数名对应的汇编代码的地址。
有导出适用场景
一般情况下,要 hook 的函数名可以在导出表找到。找不到的只有下面两种情况:
- 写代码时使用
attribute((visibility("hidden")))
关键字隐藏导出; - 编译后被开发者、加固壳或者第三方框架修改
elf
格式被加密抹去相关信息。
下面先讨论正常可以在导出表看到的情况。这里我写了两个函数:
extern "C" void func_exp()
{
LOGD("exp");
}
//这里没有extern "C"关键字 默认是c++风格导出的
//hook时要注意名称粉碎
void func_exp_cpp()
{
LOGD("exp_cpp");
}
这里只要不做啥骚操作,导出表绝对是可以看到的,如下图:
frida hook so层有导出代码这里 func_exp
函数是 c 风格导出,所以函数名直接填写就可以了。但是 func_exp_cpp
函数这里要进入 ida 里面看具体函数名,如图:
编写 hook 脚本如下:
var str_name_so = "libnative-lib.so"; //要hook的so名
var str_name_func = "func_exp"; //要hook的函数名
//var str_name_func = "_Z12func_exp_cppv"; //这里注意名称粉碎
var n_addr_func = Module.findExportByName(str_name_so , str_name_func);
console.log("func addr is ---" + n_addr_func);
Interceptor.attach(n_addr_func, {
//在hook函数之前执行的语句
onEnter: function(args)
{
console.log("hook on enter")
},
//在hook函数之后执行的语句
onLeave:function(retval)
{
console.log("hook on leave")
}
});
hook 效果如下图:
无导出so层hook
无导出这里要使用一个关键字才能达到无导出的效果:
__attribute__((visibility("hidden")))
这里写一个例子:
//extern "C" c语言格式导出
__attribute__((visibility("hidden"))) void func_no_exp()
{
LOGD("hidden");
}
生成 so 文件后,导出表是看不到这个函数的,如下图:
因为无导出的函数无法通过函数名去定位地址,所以这里只能通过手动定位去找到函数对应的偏移,注意确定偏移的时候要注意使用的 so 是v7 arm32
还是 v8 arm64
。这里可以根据情况用字符串或者看上下级调用定位到偏移,这里函数 func_no_exp
的偏移是 0x7078
(这种函数在 IDA 里面一般是 sub_xxx xxx
是 16 进制的地址),如下图:
frida hook so层无导出代码
var str_name_so = "libnative-lib.so"; //要hook的so名
var n_addr_func_offset = 0x7078; //要hook的函数在函数里面的偏移
//加载到内存后 函数地址 = so地址 + 函数偏移
var n_addr_so = Module.findBaseAddress(str_name_so);
var n_addr_func = parseInt(n_addr_so, 16) + n_addr_func_offset;
var ptr_func = new NativePointer(n_addr_func);
Interceptor.attach(ptr_func,
{
onEnter: function(args)
{
console.log("hook on enter no exp");
},
onLeave:function(retval)
{
console.log("hook on Leave no exp");
}
});
frida hook so 无导出效果图:
So层实战
介绍完 So 层的 hook 的基本理论方法,下面开始回到目标 APP 进行实战分析演示。
hook脚本的编写
回首看看目标 APK 的 libcyberpeace.so
文件在 IDA 中的反汇编结果,可以判定要 hook 的 So 层目标函数Java_com_testjava_jack_pingan2_cyberpeace_CheckString
为“有导出”类型的:
直接编写对应的 Hook 脚本 hook_check.js
,如下:
console.warn("[*] Starting Hook Script.");
//要hook的so名
var str_name_so = "libcyberpeace.so";
//要hook的函数名
var str_name_func = "Java_com_testjava_jack_pingan2_cyberpeace_CheckString";
var n_addr_func = Module.findExportByName(str_name_so , str_name_func);
console.log("[*] 目标hook函数的内存地址是: " + n_addr_func);
Interceptor.attach(n_addr_func,{
//在hook函数之前执行的语句
onEnter: function(args)
{
//console.warn("[*] Success Hook So!");
},
//在hook函数之后执行的语句
onLeave:function(retval)
{
console.warn("[*] 原始的So层函数返回值是:"+retval);
var change=1;
retval.replace(change);
console.error("[*] 篡改的So层函数返回值是:"+retval);
}
});
hook脚本的效果
下面就是见证奇迹的时候,在模拟器运行 frida server 并注入目标脚本,然后在目标 APP 的主界面任意输入字符,点击 check
按钮,可成功篡改校验结果:
此处一开始在内存在寻找不到目标函数的原因可能是该函数尚未加载,重新加载 js 脚本(增加空格并保存),即可成功寻找到目标函数的内存地址,然后点击 check
按钮即可成功 hook!
总结
本文的案例虽然 hook 成功后没有得到 flag 值(因为该 CTF 题目是需要逆向分析 So 层函数并计算 flag 后再返回 APP 进行 check 校验的),但是通过实例学习了如何 hook so 层函数并篡改其返回值。虽然在金融 APP 实战中尚未遇到数据包加解密函数放在 so 层的(可能是自信加了壳之后就安全了,没必要放在 So 层吧…),但是知识储备总是未雨绸缪,相信未来会遇到的!