2025VNCTF Re-wp+复现
hook_fish
运行应用提示输入一串字符并要求联网。
jadx分析,发现在main中,在点击按钮后会调用encrypt()方法对我们输入的数据加密,然后调用fish()方法下载了一个“hook_fish.dex”文件。通过下面的registerReceiver方法创注册了一个广播接收器,只要收到系统下载完成的广播时,就会调用onReceive()方法。
1 | registerReceiver(this.downloadCompleteReceiver, new IntentFilter("android.intent.action.DOWNLOAD_COMPLETE")); |
在onReceive()方法中把加密后的的输入传入了MainActivity.this.loadClass();,加载下载来的.dex文件并反射调用了encode(input0);对输入再次加密,最后调用check()方法检查结果。然后删除本地的“hook_fish.dex”文件。
我们先hook file.delete()方法,在删除前转移出dex文件(脚本1);
在savedDexFile.dex中我们可以发现是一个自定义的加密方法,并且还附带了解密函数,和密文,我们直接拿下来用,用java写解题脚本就行(脚本2)。(在官方的脚本中通过hook调用decode方法先解密密文,再逆向写出decrypt解密)。
脚本1
1 | Java.perform(function(){ |
脚本2(不太会处理java的数据,最后用python来进行最后的异或)
1 | import java.util.HashMap; |
1 |
|
kotlindroid
在SearchActivityKt初始化加密并验证flag。密文是”MTE0NTE0HMuJKLOW1BqCAi2MxpHYjGjpPq82XXQ/jgx5WYrZ2MV53a9xjQVbRaVdRiXFrSn6EcQPzA==”,iv是”114514”,加密方式是”AES/GCM/NoPadding”。密钥在Button$lambda$7$lambda$6方法内由两部分异或加密后拼接而成,后面传入了check函数中,hook一下就能拿到(也可以自己解)。但是没看到加密的位置,hook一下check的参数,发现输入的数据在经过sec()方法后就被加密了,查看sec方法后发现,数据被传入了SearchActivityKt$sec$1类。
加密逻辑在SearchActivityKt$sec$1类里面,在SearchActivityKt$sec$1方法下我们可以看到,被传入的函数被作为结果函数在最后执行,并接受明文和密钥。加密逻辑在invokeSuspend()方法里。
这里还用JNI.INSTANCE.getAt()方法获取了一个从so层传来的字符串并在updateAAD(bytes)中作为加密的ADD传入。之后进行AES-gcm的加密,最后再转为base64。hook一下JNI.INSTANCE.getAt()获得ADD。从题目提示得知tag也是密文的一部分。AES-GCM的密文结果一般是”iv | encrypt | tag(16bit)”。我们把密文base64解码后获取中间的密文部分。再提取tag,输入数据解密就行。
1 | from base64 import b64decode |
hook脚本
1 | Java.perform(function(){ |
幸运转盘
鸿蒙逆向。用abc编译器反编译.hap解压后的etc\modules.abc文件。被混淆了,不过不影响分析,用搜索寻找函数的声明与引用就行。
我们可以看到一个Index页面,这是第一个界面,里面有一个#~@0>@1*^d*#()方法。
这里先加载了一个lib库,里面传入了一些数据,我们ctrl+f搜索一下名字就会发现_lexenv_0_1_.numX是输入数据的长度,lexenv_0_1.pw是我们输入的数据,还有一个常量24也被传入。然后把返回的结果传参传给了pages/MyPage。
我们来看MyPage,往下看会看到一串密文。
在lexenv_0_0_ = Uint8Array(buffer.from(lexenv_0_5.result).buffer);中我们发现_lexenv_0_5就是MyPage这个对象,从中我们获取了result这个属性并转为字符串。这就是从Index传来的参数。
往下看ldlexvar.forEach()这个方法对我们的输入进行了一些操作,第一个是(result+1)^7,第二个就是在和_lexenv_0_1也就是密文进行比较。
现在我们看c层对我们的输入进行的操作,ida打开libhello.so搜索MyCry,这是在调用lib库的时候传入参数的的函数。其实在你第一步解密密文时就会发现这是一个base64加密编码,也可以去字符串表找base64的表用交叉引用定位函数。
有一个+3还有两个RC4魔改加密,试两次就行。估计是等于50的那个。
1 | import base64 |
AndroidLux
程序在java层用socket与linux虚拟环境进行通信,把输入传给linux进行判断。在Init类下发现解压了assert中的env文件进行初始化,我们解压下来就可以看到linux的rootfs文件。在Service类下可以看到启动了一个服务,就是启动了一个linux虚拟环境并设置执行目录为/root并执行了/root下的env文件。我们在rootfs文件中找到env文件,ida打开分析。里面有些用B和B.IM构成的花指令,用E0 03 00 AA字节码修改掉。发现main函数和一个魔改的base64的加密逻辑。main函数实现了对数据的接受,进行base64编码和最后的比较。直接写出解密脚本发现解密不出。感觉是什么地方被修改了。于是我提取env文件在手机上自启linux虚拟环境运行,同时运行frida进行hook。发现输入的数据并没有被修改。这就纳闷了。那么数据的修改不在env文件里,也不再java层里,那么就在虚拟linux系统上了。
最后看官方的文档,原来是修改了/etc/ld.so.preload配置文件加载了另一个so文件(参考),对read()和strcmp()做了手脚。至于怎么找到的,官方文档说用文件的修改时间对env内所有文件排序就可以看到了。
1 | find . -type f -printf "%T@ %p\n" | sort -nr | head -20 | cut -d' ' -f2- |
/etc/ld.so.preload,里面指向的是”\usr\libexec\libexec.so”,ida打开就可以看到逻辑。
1 |
|
这题的逻辑是proot通过ptrace拦截系统调用,并修改调用指向虚拟环境的位置,从而在虚拟机中执行root命令。同时也限制了调试。参考
Fuko’s_starfish
有些多余的东西可以pacth掉,最后有个反调试,在汇编里把判断改成jz就行。前面的游戏都可以patch一下,比如把猜数字的值改成0,贪吃蛇的分数改成0就通过。
搜索字符串可以找到最终的逻辑,里面有两个aes加密。开头加载密钥,交叉引用一下可以看到密钥初始的位置。
下面还有一个CheckRemoteDebuggerPresent函数,如果不处于调试,密钥异或0x17。写脚本解拿到密钥解AES即可。密文在比较的位置。
查找srand的引用可以找到种子114514
1 |
|
密文(3D011C190BA090815F672731A89AA47497362167AB2EB4A09418D37D93E646E7)