吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 901|回复: 13
上一主题 下一主题
收起左侧

[Android CTF] 2025春节红包之Android中级题Writeup(Frida Hook法)

  [复制链接]
跳转到指定楼层
楼主
Doratmon 发表于 2025-3-4 17:47 回帖奖励

分析思路和上一篇帖子一样,只是「动态调试分析秘钥」变成了「frida hook分析秘钥」

定位Check()方法--->JNI动态注册识别--->frida hook分析秘钥--->计算最终flag值

两种方法本质都是动态调试,第一篇帖子是IDA本身的动态调试,而这里用的frida去动态调试。

本篇帖子只对第三步「frida hook分析秘钥」进行分析,其余内容可参考帖子2025春节红包之Android中级题Writeup - 吾爱破解 - 52pojie.cn

回顾之前分析

Check()方法检查输入秘钥是否正确,对应到so层的native函数为sub_E8C54()。

对关键变量的数据流分析后,sub_E8C54()的关键代码简化后如下:

  v10 = getenv(byte_7767FEA58A);
  v11 = jgbjkb();

  ...

  if ( v10 )
    v13 = v12 >= 3;
  else
    v13 = 0;
  v14 = !v13;

  ...

  v22[1] = *(_OWORD *)off_7767FE1638;
  v22[0] = *(_OWORD *)off_7767FE1628;

  v15 = (void (__fastcall *)(__int64 *, __int64, __int64, _QWORD *))nullsub_2(*(_QWORD *)(
(unsigned __int64)v22 & 0xFFFFFFFFFFFFFFF7LL | //第四步:v22 & 0xFFFFFFFFFFFFFFF7LL |8*(v11|14) ^ 0
(8LL * (((unsigned __int8)(v11 | v14) //第三步:8*(v11|14) ^ 0 
^ (((unsigned int)ao ^ (unsigned int)a) >> 24)) //第一步:(0x7764532F60^0x77645328A0)>>24 ==> 0
& 1))));//第二步:0&1 ==> 0

数组v22的内存布局如下:

所以要让v15执行函数a的逻辑,需要第三步的结果为0,也就是(v11|v14)的值为0,最终第四步的结果为v22,这样v15就指向了v22[0],然后(_QWORD*)进行8字节转换,v15也就指向了a函数。

上一篇帖子分析错了,写的让(v11|v14)值为1,正确的应该是让(v11|v14)值为0。因为v22的地址是16字节对齐的,所以v22内存地址低4位一定是0。对于v22 & 0xFFFFFFFFFFFFFFF7LL|8*(v11|14) ^ 0,如果(v11|v14)的值为0,由于v22内存地址低4位为0,所以与运算结果不发生变化,v22还是指向v22[0],转换为8字节地址就是a()函数的地址;但如果(v11|v14)的值为1,运算结果的低4位变为了8,v22就指向v22[0]的第二个元素,即ao()函数,那么就会走错误的代码逻辑了。

所以要走a()函数的逻辑,(v11|v14)的值应该为0,数据流回溯一下就是需要控制下面两行代码:

v10 = getenv(byte_7767FEA58A); //v10返回非0,这样v14就等于0
v11 = jgbjkb()//让v11等于0

//最终v11|v14 ==> 0|0  ==> 0

编写hook代码

知道了要控制的变量值,接着就是hook对应的函数getenv()和jgbjkb(),借用了@ngiokweng大佬的代码。

帖子链接:2025吾愛解題領紅包活動(Android題解) - 吾爱破解 - 52pojie.cn

// hook动态库加载函数,用于在指定so库加载时执行hook操作
function hook_dlopen(soName) {
    // hook标准的dlopen函数
    var dlopen_func = Module.findExportByName(null,"dlopen");
    if(dlopen_func != null){
        // console.log("found dlopen address")
        Interceptor.attach(dlopen_func, {
            // 在函数执行前调用
            onEnter: function (args) {
                // 获取dlopen的第一个参数,即动态库文件路径
                var pathptr = args[0];
                if (pathptr !== undefined && pathptr != null) {
                    // 将指针转换为字符串
                    var path = ptr(pathptr).readCString();
                    console.log("dlopen path: ", path);
                    // 检查是否是目标so库
                    if (path.indexOf(soName) >= 0) {
                        // 设置标记,表示找到了目标so库
                        this.is_can_hook = true;
                    }
                }
            },
            // 在函数执行后调用
            onLeave: function (retval) {
                // 如果是目标so库
                if (this.is_can_hook) {
                    console.log("hook start...");
                    // 调用hook_func开始执行具体的hook操作
                    hook_func(soName)
                }
            }
        }
        );
    }else{
        console.log("Not found dlopen address")
    }

    var android_dlopen_ext_func = Module.findExportByName(null, "android_dlopen_ext");
    if (android_dlopen_ext_func != null) {
        // console.log("found android_dlopen_ext address")
        // hook Android特有的dlopen_ext函数,逻辑与上面类似
        Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
            {
                onEnter: function (args) {
                    var pathptr = args[0];
                    if (pathptr !== undefined && pathptr != null) {
                        var path = ptr(pathptr).readCString();
                        console.log("android_dlopen_ext_func path: ", path);
                        if (path.indexOf(soName) >= 0) {
                            this.is_can_hook = true;
                        }
                    }
                },
                onLeave: function (retval) {
                    if (this.is_can_hook) {
                        console.log("hook start...");
                        hook_func(soName)
                    }
                }
            }
        );
    }else{
        console.log("Not found android_dlopen_ext address")
    }
}

function hook_func(soName) {
    function hook_xorkey(base) {
        Interceptor.attach(base.add(0xE9954), { //0xE9954是函数sub_E9954,result = (__int128 *)sub_E9954(&var40);
            onLeave: function(retval) {
                console.log("[xor_key] ", hexdump(retval))
            }
        })
    }

    function hook_test2(base) {
        Interceptor.attach(base.add(0xE98A0), {//a()函数
            onEnter: function(args) {
                console.log("[call a()] ")
            }
        })

        // do_something1
        Interceptor.attach(base.add(0xE74E8), {//jgbjkb(void)函数
            onEnter: function(args) {
                console.log("[call jgbjkb] ")
            },
            onLeave: function(retval) {
                console.log("[jgbjkb] retval: ", retval)
                retval.replace(0);
                console.log("[jgbjkb] retval: ", retval)
            }
        })

        Interceptor.attach(Module.findExportByName(null, "getenv"), {
            onEnter: function(args) {
                let a0 = args[0].readCString();
                if (a0.indexOf("name") != -1) {//避免hook到app运行必需的环境变量
                    //Memory.writeUtf8String(args[0], "name");
                    this.flag = true
                    console.log("[getenv] a0: ", args[0].readCString()) 
                }
            },
            onLeave: function(retval) {
                if (this.flag) {
                    console.log("[getenv] retval: ", retval.readCString())
                    retval.replace(1);
                }
            }
        })
    }

    var base = Module.findBaseAddress(soName);

    hook_xorkey(base);
    hook_test2(base);
}

function main() {
    Java.perform(function(){
        hook_dlopen("libwuaipojie2025_game.so")

    });
}

setImmediate(main)

要理解这段代码需要清楚so的加载流程:

来自正己大佬:《安卓逆向这档事》十二、大佬帮我分析一下 - 吾爱破解 - 52pojie.cn

System.loadLibrary()/System.load() 位于Java层,用于加载so文件;android_dlopen_extdlopen() 位于so层,主要用于在运行时动态加载共享库。与标准的dlopen() 函数相比,android_dlopen_ext 提供了更多的参数选项和扩展功能,例如支持命名空间、符号版本等特性。

调用Module.findExportByName()是为了找到dlopen()android_dlopen_ext()的函数地址,然后确定是否加载的so库为libwuaipojie2025_game.so,最后调用hook_func()方法完成hook操作。其中hook_xorkey()用于打印加密密钥,hook_test2()对于a()、jgbjkb()、getenv()均进行了hook。hook a()的目的是为了确认是否执行了a()方法,而getenv()是libc的库函数,所以要调用Module.findExportByName()重新寻找函数地址。

运行frida的一个关键点是需要在app启动时进行hook,因此使用Spawn注入模式。

frida -U -f com.zj.wuaipojie2025_game -l 2025android_middle_hook.js

frida控制台输入%resume,随便输入一个秘钥123,最终输出结果如下:

可以看到确实调用了a()函数,表明成功绕过了反调试。

把两次加密秘钥进行拼接:2e 4b ee c8 e0 95 88 47 b0 72 1b 68 40 d0 0a 84 ca 9e 82

加密后的结果:48 27 8F AF 9B F8 EC 72 98 07 72 0C 6B E2 3A B6 42 59 F7 (分析加密结果的过程也参考上一篇帖子)。

两者进行异或计算:

嗯???(⊙_⊙)?为啥最后三个字节还是乱码,这不和走ao()函数逻辑得到的结果一样吗?一定是哪里出了问题。

第二次hook

首先一定是执行a()函数,不然不会输出[call a()],那为什么秘钥还是错的?

再审计一下a()函数看出问题了。在i=16时,会第二次调用sub_7767F70954()重新计算v10,得到一个新的加密密钥。基于第二次的加密密钥,会决定最后三字节(a[16] = v10[0]^a2[16]、a[17] = v10[0]^a2[17]、a[18] = v10[0]^a2[18])的加密结果。而sub_7767F70954()的入参v10受到*(_BYTE *)((unsigned __int64)&v10 | v9) = v8; 的影响,所以需要保证用户输入的秘钥前16字节是正确的,因为在i=0~15时,v10的前16字节受到用户输入的影响,也就是输入秘钥以flag{md5(uid+2025 开头。

/*
a方法实现
a2是Check()方法的入参,a3=19,a4是一个大小为19的QWORD数组
*/
__int128 *__fastcall a(__int128 *result, __int64 a2, __int64 a3, __int64 a4)
{
  __int64 i; // x23
  char v8; // w9
  __int64 v9; // x25
  __int128 v10; // [xsp+0h] [xbp-20h] BYREF
  __int64 v11; // [xsp+18h] [xbp-8h]

  v11 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
  v10 = *result;
  if ( a3 )
  {
    for ( i = 0LL; i != a3; ++i )//循环19次
    {
      v9 = i & 0xF;
      if ( (i & 0xF) == 0 )//i=0或i=16时满足
        result = (__int128 *)sub_7767F70954(&v10);//会修改v10的值

      //下面两行代码等价于a4[i] = v10[v9]^a2[i]
      v8 = *(_BYTE *)((unsigned __int64)&v10 | v9) ^ *(_BYTE *)(a2 + i);//a2每个字节与(v10|v9)异或计算
      *(_BYTE *)(a4 + i) = v8;//异或计算结果赋值给a4数组

      /*
      v10是__int128类型,128位,占用16字节,在内存追中16字节对齐
      因为要保证16字节对齐,在内存中v10的地址是类似于0x1000、0x1010这种形式,即低4位为0
      而v9的取值范围为0~15
      所以下面代码的效果为修改以v10为起始地址的16个字节的值,等价于v10[v9] = v8
      */
      *(_BYTE *)((unsigned __int64)&v10 | v9) = v8;
    }
  }
  return result;
}

所以要输入形如flag{md5(uid+2025 开头的秘钥,进行第二次hook得到加密加密的后三字节,最终结果如下:

最终的输入秘钥

所以正确的加密密钥为:2e 4b ee c8 e0 95 88 47 b0 72 1b 68 40 d0 0a 84 77 70 8a

加密结果不变:48 27 8F AF 9B F8 EC 72 98 07 72 0C 6B E2 3A B6 42 59 F7

两者异或得到最终正确的输入秘钥:flag{md5(uid+2025)}

免费评分

参与人数 4威望 +1 吾爱币 +21 热心值 +4 收起 理由
正己 + 1 + 20 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
冷若冰淡如水 + 1 我很赞同!
XMQ + 1 热心回复!
Sorronia + 1 + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
geesehoward 发表于 2025-3-5 09:15
其实getenv()与反调试没有什么关系,app启动的时候已经设置了这个环境变量,之所以会是0,是因为解密后的字符串没有终止符,导致参数不符合预期,包括之前的两个检测进程的函数也一样,文件名不对,导致根本没检查,jgbjkb()太长,没全看完。感觉正己大神出这个题的本身就没想让大家在不修改程序的情况下找到秘钥,毕竟,直接运行app,输入flag{md5(uid+2025)}会提示是错的。
推荐
 楼主| Doratmon 发表于 2025-3-5 09:58 |楼主
geesehoward 发表于 2025-3-5 09:15
其实getenv()与反调试没有什么关系,app启动的时候已经设置了这个环境变量,之所以会是0,是因为解密后的字 ...

感谢大佬,getenv应该是获取name环境变量,最后获取了/name/proc/cpuinfo(好像是这个,记不清了)。反调试这块我没仔细看,我直接借用的其他大佬的分析。
沙发
dengbin 发表于 2025-3-4 19:39
3#
Sorronia 发表于 2025-3-4 20:08
学习一下
4#
zhou820815 发表于 2025-3-4 20:28
看着好厉害哦
5#
Acting307 发表于 2025-3-5 00:49
强,插个眼,学习学习
6#
rd843i2 发表于 2025-3-5 07:52
感谢分享,学习一下思路
8#
haitunhv 发表于 2025-3-5 09:35
感谢大佬分享,学习一下思路
9#
q4310 发表于 2025-3-5 09:47
古德古德、学习一下
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-3-18 23:02, Updated at 2025-03-18 23:02:33.

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表