0x01 frida寻找符号地址frida hook Native 的基础就是如何寻找到符号在内存中的地址,在frida中就仅仅是一句话 findExportByName或者通过枚举符号遍历,今天我们就一起看一看他是如何实现…
0x01 frida寻找符号地址
frida hook Native 的基础就是如何寻找到符号在内存中的地址,在frida中就仅仅是一句话 findExportByName或者通过枚举符号遍历,今天我们就一起看一看他是如何实现的。
寻找模块基址
首先就要找到他是哪里实现的,frida源码这么大而我们之前有没有阅读ts的经验,所以选择了直接grep寻找函数名称的方法,在frida目录输入下面命令
grep -rl findExportByName
发现了如下文件中有我们要的函数名称
frida-core/tests/test-host-session.vala//使用findExportByName
frida-core/src/darwin/agent/xpcproxy.js//使用findExportByName
frida-core/src/darwin/agent/launchd.js//使用findExportByName
frida-gum/tests/gumjs/script.c//使用findExportByName
frida-gum/bindings/gumjs/runtime/core.js//定义其他函数
frida-gum/bindings/gumjs/gumv8module.cpp//函数的定义
frida-gum/bindings/gumjs/gumquickmodule.c//函数定义
在这之中我们发现了2个文件中都有findExportByName的实现,那么如何分辨呢,我们选择了加一条日志重新编译来看一看到底是哪一个
//frida-gum/bindings/gumjs/gumv8module.cpp
GUMJS_DEFINE_FUNCTION (gumjs_module_find_export_by_name)
{
__android_log_print(6,"r0ysue","i am from v8")
......
}
// frida-gum/bindings/gumjs/gumquickmodule.c
GUMJS_DEFINE_FUNCTION (gumjs_module_find_export_by_name)
{
__android_log_print(6,"r0ysue","i am from qucik");
.......
}
可以看到是quick中的代码,那么我们只要阅读这当中的代码即可,首先进入了gum_module_find_export_by_name函数中,地址就是它的返回值,
// frida-gum/bindings/gumjs/gumquickmodule.c
GUMJS_DEFINE_FUNCTION (gumjs_module_find_export_by_name)
{
...
address = gum_module_find_export_by_name (module_name, symbol_name);//得到返回值
...
}
// frida-gum/gum/backend-linux/gumprocess-linux.c
GumAddress
gum_module_find_export_by_name (const gchar * module_name,
const gchar * symbol_name)
{
GumAddress result;
void * module;
#ifdef HAVE_ANDROID//这一段主要判断是否是低版本dlopen之类的特殊函数,由于我们是高版本android而且在分析通用符号寻找所以不管
if (gum_android_get_linker_flavor () == GUM_ANDROID_LINKER_NATIVE &&
gum_android_try_resolve_magic_export (module_name, symbol_name, &result))
return result;
#endif
if (module_name != NULL)
{
module = gum_module_get_handle (module_name);/获得模块基址
if (module == NULL)
return 0;
}
else
{
module = RTLD_DEFAULT;
}
result = GUM_ADDRESS (gum_module_get_symbol (module, symbol_name));//寻找符号地址
if (module != RTLD_DEFAULT)
dlclose (module);
return result;
}
接着跟进gum_module_get_handle继续分析如何得到的模块地址,这里它分了2种形式,正如我们之前写的文章,高版本的android不能打开系统白名单之外的so所以上面是修改的dlopen,下面是linux的dlopen
gum_module_get_handle (const gchar * module_name)
{
#ifdef HAVE_ANDROID
if (gum_android_get_linker_flavor () == GUM_ANDROID_LINKER_NATIVE)
return gum_android_get_module_handle (module_name);//无限制的dlopen
#endif
return dlopen (module_name, RTLD_LAZY | RTLD_NOLOAD);//普通的dlopen
}
寻找linker相关的信息
普通的dlopen之前的文章领着大家看过这里就只分析无限制的dlopen是如何实现的了,跟进gum_android_get_module_handle,这里有2个函数一个是gum_enumerate_soinfo,一个是gum_store_module_handle_if_name_matches,我们分开看,先看一个他是如何枚举soinfo的,跟进去发现gum_linker_api_get函数来获得linker中的api,跟进去看看如何实现的,这里首先获得了linker的首地址如下,最终到了gum_try_parse_linker_proc_maps_line函数中,这个函数就是遍历了maps来获得linker(目前好像只有这一种获得linker首地址的方法),虽然这种方法很准确但是frida太谨慎了,又校验了魔术字段,又校验了只读权限,所以这是一个antifrida的关键点
void *
gum_android_get_module_handle (const gchar * name)
{
GumGetModuleHandleContext ctx;
ctx.name = name;
ctx.module = NULL;
gum_enumerate_soinfo (
(GumFoundSoinfoFunc) gum_store_module_handle_if_name_matches, &ctx);//赋值ctx的module
return ctx.module;
}
static void
gum_enumerate_soinfo (GumFoundSoinfoFunc func,
gpointer user_data)
{
api = gum_linker_api_get ();//获得linker中的函数地址
.......
}
static GumLinkerApi *
gum_linker_api_get (void)
{
.....
g_once (&once, (GThreadFunc) gum_linker_api_try_init, NULL);//用宏定义调用gum_linker_api_try_init函数
.......
}
static GumLinkerApi *
gum_linker_api_try_init (void)
{
....
linker = gum_android_open_linker_module ();//找到linker
....
}
GumElfModule *
gum_android_open_linker_module (void)
{
const GumModuleDetails * linker;
linker = gum_android_get_linker_module_details ();
return gum_elf_module_new_from_memory (linker->path,
linker->range->base_address);//构造结构体无具体逻辑
}
const GumModuleDetails *
gum_android_get_linker_module_details (void)
{
static GOnce once = G_ONCE_INIT;
g_once (&once, (GThreadFunc) gum_try_init_linker_details, NULL);//找到linker的首地址
if (once.retval == NULL)//抛出异常在第二篇里面就用到了这里,通过主动抛出这个异常的方式来antifrida
{
g_critical ("Unable to locate the Android linker; please file a bug");
g_abort ();
}
return once.retval;
}
static const GumModuleDetails *
gum_try_init_linker_details (void)
{
const GumModuleDetails * result = NULL;
gchar * linker_path;
GRegex * linker_path_pattern;
gchar * maps, ** lines;
gint num_lines, vdso_index, i;
linker_path = gum_find_linker_path ();//得到linker的路径包括安卓10之上或者低版本android
linker_path_pattern = gum_find_linker_path_pattern ();
g_file_get_contents ("/proc/self/maps", &maps, NULL, NULL);//通过glibc的库函数打开maps
lines = g_strsplit (maps, "\n", 0);
num_lines = g_strv_length (lines);
vdso_index = -1;
for (i = 0; i != num_lines; i++)
{
const gchar * line = lines[i];
if (g_str_has_suffix (line, " [vdso]"))
//这里有一个分叉,目的就是我们可以通过安卓源码得知linker的内存排布在[vdso]之后,所以我们可以以[vdso]为基准向上下遍历maps快速的寻找linker地址
{
vdso_index = i;
break;
}
}
if (vdso_index == -1)
goto no_vdso;
for (i = vdso_index + 1; i != num_lines; i++)
{
if (gum_try_parse_linker_proc_maps_line (lines[i], linker_path,
linker_path_pattern, &gum_dl_module, &gum_dl_range))
//遍历maps寻找linker在内存中的地址,这里逻辑很简单里面就一个魔术字段的判断,找到了linker的首地址和我们之前写的遍历maps寻找linker差不多,这里可以用来检测frida
{
result = &gum_dl_module;
goto beach;
}
}
for (i = vdso_index - 1; i >= 0; i--)
{
if (gum_try_parse_linker_proc_maps_line (lines[i], linker_path,
linker_path_pattern, &gum_dl_module, &gum_dl_range))
{
result = &gum_dl_module;
goto beach;
}
}
goto beach;
no_vdso://没有vdso就从头开始一个一个的判断
for (i = num_lines - 1; i >= 0; i--)
{
if (gum_try_parse_linker_proc_maps_line (lines[i], linker_path,
linker_path_pattern, &gum_dl_module, &gum_dl_range))
{
result = &gum_dl_module;
goto beach;
}
}
.......
}
static gboolean
gum_try_parse_linker_proc_maps_line (const gchar * line,
const gchar * linker_path,
const GRegex * linker_path_pattern,
GumModuleDetails * module,
GumMemoryRange * range)
{
GumAddress start, end;
gchar perms[5] = { 0, };
gchar path[PATH_MAX];
gint n;
const guint8 elf_magic[] = { 0x7f, 'E', 'L', 'F' };//elf魔术字段头4个字节
n = sscanf (line,
"%" G_GINT64_MODIFIER "x-%" G_GINT64_MODIFIER "x "
"%4c "
"%*x %*s %*d "
"%s",
&start, &end,
perms,
path);//字符串扫描
if (n != 4)
return FALSE;
if (!g_regex_match (linker_path_pattern, path, 0, NULL))//路径匹配
return FALSE;
if (perms[0] != 'r')//可读匹配,不可读就没办法看下面的魔术字段
return FALSE;
if (memcmp (GSIZE_TO_POINTER (start), elf_magic, sizeof (elf_magic)) != 0)//判断魔术字段,通过这个可以antifrida,因为魔术字段在运行过程中没啥太大作用,这是一个点
return FALSE;
module->name = strrchr (linker_path, '/') + 1;//下面就是保存下来
module->range = range;
module->path = linker_path;
range->base_address = start;
range->size = end - start;
return TRUE;
}
之后我们一起看一看,他是如何寻找linker中函数的地址,也就是如何初始化的api,可以看到和我们之前的方法差不多都是从节头表索引,也只有遍历节头表这一种方式能够得到linker中的dlopen这种符号了因为linker没有导出符号,最终到了gum_store_linker_symbol_if_needed函数中,保存需要的符号类似do_dlopen等,经此之后我们就有了直接从maps中得到的linker中的do_dlopen和do_dlsym等,保存到了api中
//接上文gum_linker_api_try_init的下半段逻辑
static GumLinkerApi *
gum_linker_api_try_init (void)
{
.....
api_level = gum_android_get_api_level ();//得到手机的安卓版本
gum_elf_module_enumerate_symbols (linker,
(GumElfFoundSymbolFunc) gum_store_linker_symbol_if_needed, &pending);//将linker中的符号提取出来包括do_dlopen,do_dlsym等
.....
}
void
gum_elf_module_enumerate_symbols (GumElfModule * self,
GumElfFoundSymbolFunc func,
gpointer user_data)
{
gum_elf_module_enumerate_symbols_in_section (self, SHT_SYMTAB, func,
user_data);
}
static void
gum_elf_module_enumerate_symbols_in_section (GumElfModule * self,
GumElfSectionHeaderType section,
GumElfFoundSymbolFunc func,
gpointer user_data)
{
......
if (!gum_elf_module_find_section_header_by_type (self, section, &scn, &shdr))//寻找linker节头之前解析elf都讲过就不带着大家看了,从节头表中寻找类型为2的节,我们是通过name判断的他这种更巧妙
.......
for (symbol_index = 0;
symbol_index != symbol_count && carry_on;
symbol_index++)
{//遍历节符号表中所有的符号,如果是我们需要的就保存下来
......
carry_on = func (&details, user_data);//执行上面的gum_store_linker_symbol_if_needed函数,遍历所有的符号表,如果有我们需要的符号就保留下来
}
}
static gboolean
gum_store_linker_symbol_if_needed (const GumElfSymbolDetails * details,
guint * pending)
{//这里列出了不同版本的dlopen的符号名称,遍历前面的节头表即可通过字符串匹配得到
/* Restricted dlopen() implemented in API level >= 26 (Android >= 8.0). */
GUM_TRY_ASSIGN (dlopen, "__dl___loader_dlopen"); /* >= 28 */
GUM_TRY_ASSIGN (dlsym, "__dl___loader_dlvsym");/* >= 28 */
GUM_TRY_ASSIGN (dlopen, "__dl__Z8__dlopenPKciPKv");/* >= 26 */
GUM_TRY_ASSIGN (dlsym, "__dl__Z8__dlvsymPvPKcS1_PKv"); /* >= 26 */
/* Namespaces implemented in API level >= 24 (Android >= 7.0). */
GUM_TRY_ASSIGN_OPTIONAL (do_dlopen,
"__dl__Z9do_dlopenPKciPK17android_dlextinfoPv");
GUM_TRY_ASSIGN_OPTIONAL (do_dlsym, "__dl__Z8do_dlsymPvPKcS1_S_PS_");
GUM_TRY_ASSIGN (dl_mutex, "__dl__ZL10g_dl_mutex"); /* >= 21 */
GUM_TRY_ASSIGN (dl_mutex, "__dl__ZL8gDlMutex");/* < 21 */
GUM_TRY_ASSIGN (solist_get_head, "__dl__Z15solist_get_headv"); /* >= 26 */
GUM_TRY_ASSIGN_OPTIONAL (solist, "__dl__ZL6solist"); /* >= 21 */
GUM_TRY_ASSIGN_OPTIONAL (libdl_info, "__dl_libdl_info"); /* < 21 */
GUM_TRY_ASSIGN (solist_get_somain, "__dl__Z17solist_get_somainv"); /* >= 26 */
GUM_TRY_ASSIGN_OPTIONAL (somain, "__dl__ZL6somain"); /* "any" */
GUM_TRY_ASSIGN (soinfo_get_path, "__dl__ZNK6soinfo12get_realpathEv");
beach:
return *pending != 0;
}
处理我们要寻找的符号
那么这里我们就可以通过linker中的soinfo链表来遍历所有的soinfo,然后再通过dlZ17solist_get_somainv函数来匹配so的名字,最后通过我们之前得到的dlopen函数调用来,得到该so的handle
//继续接上文gum_enumerate_soinfo函数
static void
gum_enumerate_soinfo (GumFoundSoinfoFunc func,
gpointer user_data)
{
......
somain = api->solist_get_somain ();//得到somain指针,这里又是另一个anti的点,就是可以清空somain因为他没判断是否为空
gum_init_soinfo_details (&details, somain, api, &ranges);//将它初始化到detail中
carry_on = func (&details, user_data);//通过调用gum_store_module_handle_if_name_matches匹配路径
for (si = api->solist_get_head (); carry_on && si != NULL; si = next)//通过链表遍历所有的soinfo指针,当carry_on为false或者链表后没有元素的时候退出循环
{
carry_on = func (&details, user_data);//使用gum_store_module_handle_if_name_matches函数判断,如果路径一致就返回false
....
}
.....
}
static gboolean
gum_store_module_handle_if_name_matches (const GumSoinfoDetails * details,
GumGetModuleHandleContext * ctx)
{
GumLinkerApi * api = details->api;
if (gum_linux_module_path_matches (details->path, ctx->name))//通过名字匹配,就是上面的通过链表索引出来的soinfo文件名是否一样
{
GumSoinfoBody * sb = details->body;
int flags = RTLD_LAZY;
void * caller_addr = GSIZE_TO_POINTER (sb->base);//dlopen的第三个参数,只有成功找到了do_dlopen的第三个参数才能成功的找到符号
if (gum_android_is_vdso_module_name (details->path))
return FALSE;
if ((sb->flags & GUM_SOINFO_NEW_FORMAT) != 0)
{
GumSoinfo * parent;
parent = (sb->parents.head != NULL)
? sb->parents.head->element
: NULL;//通过指针找到该so的爸爸的首地址
if (parent != NULL)
{
caller_addr = GSIZE_TO_POINTER (gum_soinfo_get_body (parent)->base);
}
if (sb->version >= 1)
{
flags = sb->rtld_flags;
}
}
if (gum_android_get_api_level () >= 21)
{
flags |= RTLD_NOLOAD;
}
if (api->dlopen != NULL)
{
/* API level >= 26 (Android >= 8.0) */
ctx->module = api->dlopen (details->path, flags, caller_addr);//调用我们之前得到的dlopen来获得该so的handle
}
else if (api->do_dlopen != NULL)
{
/* API level >= 24 (Android >= 7.0) */
ctx->module = api->do_dlopen (details->path, flags, NULL, caller_addr);
}
else
{
ctx->module = dlopen (details->path, flags);
}
return FALSE;
}
return TRUE;
}
至此我们解析完了frida构造没有限制的dlopen的过程,那么接下来就看看他是如何找到dlsym的
//接上文gum_module_find_export_by_name
GumAddress
gum_module_find_export_by_name (const gchar * module_name,
const gchar * symbol_name)
{
....
result = GUM_ADDRESS (gum_module_get_symbol (module, symbol_name));
....
}
static void *
gum_module_get_symbol (void * module,
const gchar * symbol)
{
GumGenericDlsymImpl dlsym_impl = dlsym;
#ifdef HAVE_ANDROID
if (gum_android_get_linker_flavor () == GUM_ANDROID_LINKER_NATIVE)
gum_android_find_unrestricted_dlsym (&dlsym_impl);//和上面一样构造没限制的dlsym
#endif
return dlsym_impl (module, symbol);
}
gboolean
gum_android_find_unrestricted_dlsym (GumGenericDlsymImpl * generic_dlsym)
{
if (!gum_android_find_unrestricted_linker_api (NULL))//初始化我们的api
return FALSE;
*generic_dlsym = gum_call_inner_dlsym;//最终赋值
return TRUE;
}
static void *
gum_call_inner_dlsym (void * handle,
const char * symbol)
{
return gum_dl_api.dlsym (handle, symbol, NULL, gum_dl_api.trusted_caller);//在&gum_dl_api函数中完成api的初始化,在高版本中也就是__dl___loader_dlvsym
}
到此为止我们就分析完了,frida是如何找到高版本的dlopen和dlsym,其实就是从节头表找到几个函数的地址,期间还意外的发现了anti frida的方法(其实具体逻辑不在这里,因为即使dlopen为空也能正常找到符号地址,但是attach需要验头,下文再说)
0x02 于frida缺陷的反frida
frida发展了这么久,发展出了2种anti方式,一种是以字符串为基准的旧检测方式,一种是以frida代码为基准的各种各样的崩溃基址,旧方案现在很多地方还在使用,但是旧方案的绕过方式就太多了,比如hook fgets函数,甚至出现了hluda这种傻瓜式的绕过方式,所以有必要开发新的anti方式,这就是这篇文章的主旨希望能找到一个新的,难以被发现的antifrida的方式
旧方案之maps
检测法
在字符串的检测方案中,大部分用的都是这种,但是这种也很容易被感知,它的代码结构如下,主要就是检测maps文件种是否有frida-agent字符串,当然这种取自maps的方式太容易被感知了,随便hook一下就知道我们遍历了maps,所以有以下的改进版本,通过遍历链表的方式来获得so的名称,查看是否有frida字样的so。
void anti3(){
while (1) {
sleep(1);
char line[1024];
FILE *fp = fopen("/proc/self/maps", "r");
while (fgets(line, sizeof(line), fp)) {
if (strstr(line, "frida-agent")) {
__android_log_print(6, "r0ysue", "i find frida from anti3");
}
}
}
}
改进
void fridafind(){
char line[1024];
int *start;
int *end;
int n=1;
int m=1;
int *start1;
FILE *fp=fopen("/proc/self/maps","r");
while (fgets(line, sizeof(line), fp)) {
if (strstr(line, "linker64") ) {
__android_log_print(6,"r0ysue","%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++;
}
if (strstr(line, "libopenjdkjvm.so") ) {
__android_log_print(6,"r0ysue","%s", line);
if(m==1){
start1 = reinterpret_cast<int *>(strtoul(strtok(line, "-"), NULL, 16));
}
m++;
}
}//获得liner首地址
int dlopenoff=findsym("/system/bin/linker64","__dl__Z8__dlopenPKciPKv");
int headeroff=findsym("/system/bin/linker64","_dl__ZL6solist");//得到soinfo链表的头部
long header= *(long *) ((char *) start + headeroff);
for ( _QWORD *result = (_QWORD *)header; result; result = (_QWORD *)result[5] )//遍历所有的soinfo对象{
if(strstr((const char*)*(_QWORD *)((__int64)result + 408),"frida"))
__android_log_print(6,"r0ysue","%s",*(_QWORD *)((__int64)result + 408));//得到so的name
}
}
这种还有一个版本,就是检查/data/local/tmp目录下面有没有frida依赖so所组成的文件夹,就是说frida-server在启动的时候会将依赖的so放在
/data/local/tmp这个文件夹下面,所以我们可以扫描有没有这个文件夹,类似与下面这样的代码,当然上面提到的两种方法都能被简单的绕过,比如典型了hluda,就可以轻松的绕过,或者直接hook strstr函数也能发现校验的关键点,就不是很好,所以其实也可以改成逐比特用等号对比,当然也是很好绕过就对了
void anti4(){
int a= access("/data/local/tmp/re.frida.server",0);
if(a ==0)
__android_log_print(6,"r0ysue","i find frida from anti4");
}
当然这里还有一些原理性的检测方法,比如和xposed一样检测ArtMethod的AccessFlags值来判断一个确定为Java的函数是否变成了Native函数,这个和java hook的原理有关,这个是frida绕不开的,就是想hookjava函数就一定要将java函数改成native函数,但是这种方式如果不 hook java函数直接搞Native层就拉了,所以这种方案也不太行。
// jclass myclass=env->FindClass("com/roysue/myanti/MainActivity");
// jmethodID mymethod=env->GetMethodID(myclass,"encr", "()I");
// a1=mymethod
void anti7(__int64 a1){
while (1) {
sleep(1);
__android_log_print(6,"r0ysue","i find frida %x", (~*(_DWORD *)(a1 + 4) & 0x80000) );
if((~*(_DWORD *)(a1 + 4) & 0x80000) !=0)
__android_log_print(6,"r0ysue","i find frida %x", (~*(_DWORD *)(a1 + 4) & 0x80000) );
}
}
当然后来又有大佬搞出了一个方案,见贴https://bbs.pediy.com/thread-268586.htm
,这种方式提供了一个新思路,就是从frida的变化入手,例如检测frida的inline hook,这种方法就相当的好用了,因为frida作者也说了异常处理有一个bug必须要hook PrettyMethod函数,所以这种script boy就是无论如何都绕不开的
function fixupArtQuickDeliverExceptionBug (api) {// frida源码
const prettyMethod = api['art::ArtMethod::PrettyMethod'];
if (prettyMethod === undefined) {
return;
}
/*
* There is a bug in art::Thread::QuickDeliverException() where it assumes
* there is a Java stack frame present on the art::Thread's stack. This is
* not the case if a native thread calls a throwing method like FindClass().
*
* We work around this bug here by detecting when method->PrettyMethod()
* happens with method == nullptr.
*/
Interceptor.attach(prettyMethod.impl, artController.hooks.ArtMethod.prettyMethod);
Interceptor.flush();
}
所以说这种的anti 代码就如下,这种就靠谱多了,但是有可能会误杀,现在很多inline hook 都会采用x16跳转这种形式
// as=findsym("/system/lib64/libart.so","_ZN3art9ArtMethod12PrettyMethodEb");
void anti6(long * as){
while (1) {
pthread_mutex_lock(&mutex);
//__android_log_print(6, "r0ysue", "i find frida 1 %p",*as);
//long long as = *(long long *) reinterpret_cast<long>(libnative[n]);
if (*as == 0xd61f020058000050) {
__android_log_print(6, "r0ysue", "i find frida from anti6 ");
}
sleep(2);
pthread_mutex_unlock(&mutex);
}
}
所以说旧方式都是形式上的anti frida,都是可见的,都是在frida对系统的更改,那么新方式就是从frida的bug出发,要寻找frida在做寻找符号过程中容易发出异常的点,来主动抛出这些异常。
新方案之attach流程中寻找anti点
在上篇文章中我们发现了frida调用了一个函数gum_android_open_linker_module
来获取linker的地址,但是有一个缺陷,导致我们可以根据这一点反制frida,接下来我们就来一起看一下这个问题。
首先写一个简单的demo,逻辑很简单就是在主函数里面加一个anti7函数,从maps里面遍历linker64,然后把它开头的魔术字随便段改一个,比如我们这里就是将0x7f改成了0,最后看一下结果
//libnative-lib.so
void anti7(){
char line[1024];
int *start;
int *end;
int n=1;
FILE *fp=fopen("/proc/self/maps","r");
while (fgets(line, sizeof(line), fp)) {
if (strstr(line, "linker64") ) {
__android_log_print(6,"r0ysue","%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++;
}
}
long* sr= reinterpret_cast<long *>(start);
mprotect(start,PAGE_SIZE,PROT_WRITE|PROT_READ|PROT_EXEC);
*sr=*sr^0x7f;
__android_log_print(6, "r0ysue", "i find frida %p",*sr);
void* tt=dlopen("libc.so",RTLD_NOW);
void* ts=dlsym(tt,"strstr");
__android_log_print(6,"r0ysue","%p",ts);
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_roysue_anti_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
anti7();
return env->NewStringUTF(hello.c_str());
}
可以看到以attach的方式启动,可以成功的使我们的frida崩溃掉,我们来一起看一下他为什么会有这种结果。
可以直接从frida-core/src/linux/frida-helper-backend-glue.c目录下的_frida_linux_helper_backend_do_inject函数入手(还是c代码比较易读其他语言不太易读),发现这里面调用了frida_resolve_linker_address函数来获得dlclose函数,我们跟进去看一下
void
_frida_linux_helper_backend_do_inject (FridaLinuxHelperBackend * self, guint pid, const gchar * path, const gchar * entrypoint, const gchar * data, const gchar * temp_path, guint id, GError ** error)
{
.....
#elif defined (HAVE_ANDROID)
params.dlopen_impl = frida_resolve_android_dlopen (pid);
params.dlclose_impl = frida_resolve_linker_address (pid, dlclose);//从linker里面搜索dlclose
params.dlsym_impl = frida_resolve_linker_address (pid, dlsym);//从linker里面搜索dlsym
....
}
有两个frida_resolve_linker_address函数我们只需要看这个ANDROID就好了,它之中调用了gum_android_get_linker_module_details函数来获得linker地址
#ifdef HAVE_ANDROID
static GumAddress
frida_resolve_linker_address (pid_t pid, gpointer func)
{
......
else
local_base = gum_android_get_linker_module_details ()->range->base_address;//使用获得linker的地址
.....
return remote_address;
}
接着就跳到了gum_android_get_linker_module_details函数,又回到了上篇文章的那里
// frida-gum/gum/backend-linux/gumandroid.c
const GumModuleDetails *
gum_android_get_linker_module_details (void)
{
static GOnce once = G_ONCE_INIT;
g_once (&once, (GThreadFunc) gum_try_init_linker_details, NULL);
if (once.retval == NULL)
{
g_critical ("Unable to locate the Android linker; please file a bug");
g_abort ();
}
return once.retval;
}
调用了gum_try_parse_linker_proc_maps_line,来寻找maps当中的linker
static const GumModuleDetails *
gum_try_init_linker_details (void)
{
no_vdso:
for (i = num_lines - 1; i >= 0; i--)
{
if (gum_try_parse_linker_proc_maps_line (lines[i], linker_path,
linker_path_pattern, &gum_dl_module, &gum_dl_range))
{
result = &gum_dl_module;
goto beach;
}
}
return result;
}
最终到了我们的判断函数gum_try_parse_linker_proc_maps_line,这里面最大的问题就是验证了elf头部信息这个根本不会被用到的东西,那么只要我们更改掉maps里面的elf头那么frida就找不到linker的地址了,那么frida就自然崩掉了,会抛出异常Unable to locate the Android linker; please file a bug
static gboolean
gum_try_parse_linker_proc_maps_line (const gchar * line,
const gchar * linker_path,
const GRegex * linker_path_pattern,
GumModuleDetails * module,
GumMemoryRange * range)
{
.....
const guint8 elf_magic[] = { 0x7f, 'E', 'L', 'F' };
if (memcmp (GSIZE_TO_POINTER (start), elf_magic, sizeof (elf_magic)) != 0)//判断开头魔术字段是否是elf的魔术字段
return FALSE;
....
return TRUE;
}
新方案之findsymbol流程中寻找anti点
承接上文的findsymbol
,这里其实存在一个巨大的bug
,就是somain
的获取他没有判断是否为空我们跟下去看一下,跟踪到最后发现它没有判断是否为空就直接取值了,就会造成地址不对这种情况,下面我们试一下。
static void
gum_enumerate_soinfo (GumFoundSoinfoFunc func,
gpointer user_data)
{
......
//得到主进程的soinfo指针
somain = api->solist_get_somain ();
gum_init_soinfo_details (&details, somain, api, &ranges);//将它初始化到detail中
.....
}
static void
gum_init_soinfo_details (GumSoinfoDetails * details,
GumSoinfo * si,
GumLinkerApi * api,
GHashTable ** ranges)
{
details->path = gum_resolve_soinfo_path (si, api, ranges);
details->si = si;
//跟入这里
details->body = gum_soinfo_get_body (si);
details->api = api;
}
static GumSoinfoBody *
gum_soinfo_get_body (GumSoinfo * self)
{
guint api_level = gum_android_get_api_level ();
if (api_level >= 26)
//这里没有做判断直接就取值了,十分的不科学,所以我们可以将somain改为空对普通使用也没啥影响,下面也一样
return &self->modern.body;
else if (api_level >= 23)
return &self->legacy23.body;
else
return &self->legacy.legacy23.body;
}
写一个简单的demo,搞到app里面,这里写在了init段中就是,让他人不管是spawn或者attch都不能寻找符号。
void anti7(){
char line[1024];
int *start;
int *end;
int n=1;
FILE *fp=fopen("/proc/self/maps","r");
while (fgets(line, sizeof(line), fp)) {
if (strstr(line, "linker64") ) {
__android_log_print(6,"r0ysue","%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++;
}
}
long* sr= reinterpret_cast<long *>(start);
mprotect(start,PAGE_SIZE,PROT_WRITE|PROT_READ|PROT_EXEC);
long off=findsym("/system/bin/linker64","__dl__ZL6somain");
__android_log_print(6,"r0ysue","xxxxxxxx %p",off);
long* somain= reinterpret_cast<long*>((char *) sr + off);
*somain=0;
void* sb=dlopen("libc.so",RTLD_NOW);
void* ddd=dlsym(sb,"strstr");
void* tt=dlopen("libc.so",RTLD_NOW);
void* ts=dlsym(tt,"strstr");
__android_log_print(6,"r0ysue","%p",ts);
}
extern "C" void _init(void){
anti7();
}
用下面的frida脚本试一下,最后果然崩溃了。
function main(){
var dlopen = Module.findExportByName(null, "android_dlopen_ext");
Interceptor.attach(dlopen, {
onEnter: function (arg) {
var name=ptr(arg[0]).readCString();
if(name.indexOf("libnative-lib.so")>=0){
console.log(name)
this.name=name;
}
}, onLeave: function (ret) {
if(this.name!=undefined){
var libcrackme=Module.findBaseAddress("libnative-lib.so");
console.log(libcrackme);
}
}
})
}
setImmediate(main);
0x03 总结
anti-frida
与绕过frida
的手段都在一直的进步,比如早期提出的so特征antifrida的方式就被hluda完美的绕过了,导致很长一段时间内frida畅通无阻;后来的从大致的原理出发的ptrace与hook特征,也有一定的局限性就是太容易被感知到了,比如双进程互相ptrace判断,这样ps就能知道手法。
最好的方式还是从源码出发,直接以找bug的心态阅读frida源码,当然前文介绍的这种方式也有一定的局限性,就是如果以spawn的方式启动frida,此时so代码是没法影响到frida-server
的,所以spwan去hook系统的so是确实anti不到,算是一个小的遗憾吧,但是只要我们的app启动frida就没法完成hook包括java hook,总之说了这么多,脚本小子总会被掣肘,想愉快的逆向,最好是能自己开发一个主动调用兼hook框架。
- 本文作者: 26号院
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/1389
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!