180523 安卓-DDCTF(详细复现版)

Hello Baby Dex

jeb反编译发现不少第三方库,其中一个com.meituan.robust包搜索一下可以发现是美团开发的一个开源热更新框架
参照使用教程可以发现补丁的位置在PatchExecutor类调用的PatchManipulateImp类中的fetchPatchList方法中调用的setLocalPath方法处设置
于是跟着去找
cn.chaitin.geektan.crackme.PatchManipulateImp.fetchPatchList方法

这里可以发现读取了GeekTan.BMP的数据
setLocalPath在下面一点儿,同样也是将GeekTan设置为文件路径

于是去assets文件夹中把这个文件扒出来,查看发现是zip结构,解压得到DEX文件
(话是这么说,能塞私货的地方其实也只有assets文件夹了。所以作为题目而言看到热补丁就可以直接去这找,反正又不可能联网更新233)
处理dex文件,用jeb/dex2jar+jd-gui都可以

再往下分析补丁,大部分教程的方法都是借助插件直接生成Patch.jar,而不提及具体内部原理,因此要分析补丁还是要找原理解析的文章

PatchExecutor开启一个子线程,通过指定的路径去读patch文件的jar包,patch文件可以为多个,每个patch文件对应一个 DexClassLoader 去加载,每个patch文件中存在PatchInfoImp,通过遍历其中的类信息进而反射修改其中 ChangeQuickRedirect 对象的值。

在补丁中的PatchInfoImp中找到这样两句,说明了补丁的类分别是MainActivity和MainAcitivity$1

localArrayList.add(new PatchedClassInfo("cn.chaitin.geektan.crackme.MainActivity", "cn.chaitin.geektan.crackme.MainActivityPatchControl"));
localArrayList.add(new PatchedClassInfo("cn.chaitin.geektan.crackme.MainActivity$1", "cn.chaitin.geektan.crackme.MainActivity$1PatchControl"));

PatchControl类用来控制Patch,没有具体方法,可以忽略
两个Patch类中则是关键的更新方法
首先是MainActivity$1中的onClick方法

发现有很多EnhancedRobustUtils.invokeReflectMethod
搜索一下可以发现解释

EnhancedRobustUtils是一个对反射的封装类,可以反射指定对象的指定字段和方法。比如说((Integer)EnhancedRobustUtils.invokeReflectMethod(“b”, var5, var6, new Class[]{Integer.TYPE}, SampleClass.class)) 就是反射var5对象的b方法,方法的参数类型是Integer,参数的具体值是var6。

整理一下大量的反射方法,发现整个逻辑就是构造一个String,将”DDCTF{“、Joseph(3, 4)、Joseph(5, 6)、”}”四个字符串连接起来,最后通过equals与输入比较

由于flag明文出现在内存中,可以操作的方法非常多
Hook啊、Patchsmali代码打log啊、动态调试啊等等
这个题目有签名验证,所以Patch相对要麻烦一些
Hook也是常规操作了,不赘述
动态调试在没有反调的情况下最简单233,虚拟机跑起来,下个断就能看到

Joseph也被打了补丁,反射方法看起来太累,扫了一遍都是add,就不详细分析了

Robust的各个方法介绍和原理在https://juejin.im/post/58e4ce652f301e006227ab40有比较详细的说明,包括xxPatch类,xxPatchControl类的作用等等

Diffie-Hellman

跟第一题一样,JAVA层没有任何东西,直接调用StringFromJNI
不过这次没啥垃圾代码,开头一个跟第一题一样的basic_string构造
直接通过str2ll转成了整数

IDA的反编译对于这种r0和r1两个返回值的就不太友善
直接看汇编就很清晰,低32位R0放到R4中,高32位R1放到R5中


这里的>>31实际上应该是取高1-33位的意思,IDA会把两个32位寄存器合并成一个变量来考虑,包括i, v11, v14, v10等等
所以循环其实是当i==n时退出
另一方面,v11的实际寄存器是r2,也就是divmod的余数,或者从mod_residual的命名来看也可以猜出与之对比的v11应该是余数
然后v14=v11<<1(高低位复合起来看)

也就是说不断对v14*2,每次模p,余数再赋给v14,循环input次以后将余数与mod_residual比较,相等则通过

再整理一下,根据同余定理,可以直接导出

2^input % 0xB49487B06AA40 == 0x1d026744b3680

爆破input,得到208603

ECC

反编译发现使用的第三方包被混淆过,包名和方法名完全无法辨认

主函数很简单

根据题目和字符串”secp256k1”可以猜到是ECC椭圆曲线加密算法
按照题目的连接去学了一波ECC,大概了解了公私钥的生成方法
这里是在secp256k1曲线上把输入作为私钥生成公钥的两个数,然后拼接起来并hex_decode与this.m进行对比

ECC作为一种安全的加密算法显然不可能有从公钥反推私钥的攻击方法,因此只可能爆破了

问题在于怎么爆破
既然知道它是ECC,曲线也已知了,那么爆破用C++当然是最快的

找了一下午的实现,大多数都是随机生成的密钥对,最后好不容易找到一个给定私钥生成公钥的,结果跑了一下发现跟动调得到的生成结果不同,也就意味着算法不同……OTZ血崩

后来用了下python的ECC库,生成的公钥跟本程序也不一样

纠结了很久,尝试动调、逆整个程序,找到哪里不同,结果因为变量名混淆导致根本不清楚自己跟到哪里去了233333
后来想着直接导出反编译的代码和库去运行,结果因为包名和方法名混淆后相同,java编译器辨认不清而作罢

最后翻到某一个方法的时候偶然发现

抛出异常的字符串真是天使
拿着这个去谷歌,终于找到了第三方库bouncycastle
还好这库是开源的,在github一个一个类根据字符串去比对,最后完全还原整个函数调用过程,一运行发现公钥得到的两个数还是不同,心态爆炸
突然发现IDE给了提示,这个函数被废弃了

于是找到getXCoord,结果终于相同
开始爆破,安心睡觉

第二天起来发现结果43458080

package me.company;
import java.math.BigInteger;
import java.security.spec.ECParameterSpec;
//import java.security.spec.ECPoint;
import java.security.spec.ECPrivateKeySpec;
import java.security.spec.ECPublicKeySpec;

import org.bouncycastle.asn1.nist.NISTNamedCurves;
import org.bouncycastle.asn1.x9.X962NamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.ec.CustomNamedCurves;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import org.bouncycastle.asn1.x9.ECNamedCurveTable;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.asn1.sec.SECNamedCurves;
public class Main {

    public static void main(String[] args)
    {
        long n = 43450000;
        while (true) {
            if (c(n)) {
                break;
            }
            else{
                n++;
                if(n%10000==0)System.out.println(n);
            }
        }
        System.out.println("find it");
        System.out.println(n);
         // 43458080

    }

    public static boolean c(long i)
    {
        String m = "00AF576186553CC4B9224B738D89162F723BCFBF589CEF072A2C0ADA7B3443B5DC21D75144B89C87E3AC0BE030A1F5CE90E86F635D3E86271FB71375F5F581E9A2";
        //getParameterSpec("secp256k1").;
        String input = String.valueOf(i);
        BigInteger test = new BigInteger(input.getBytes());
        //BigInteger test = new BigInteger("1");
        //System.out.println(test);
        X9ECParameters ecP = SECNamedCurves.getByName("secp256k1");
        ECPoint g = ecP.getG();
        //System.out.println(g);
        ECPoint p = g .multiply(test);
        p.getX();
        BigInteger x = p.getXCoord().toBigInteger();
        BigInteger y = p.getYCoord().toBigInteger();
        //System.out.println(x);
        //System.out.println(y);
        byte[] v3 = x.toByteArray();
        byte[] v4 = y.toByteArray();
        byte[] v5 = new byte[v3.length + v4.length];
        int v0_3;
        for(v0_3 = 0; v0_3 < v5.length; ++v0_3) {
            byte v2_1 = v0_3 < v3.length ? v3[v0_3] : v4[v0_3 - v3.length];
            v5[v0_3] = v2_1;
        }

        StringBuilder v2_2 = new StringBuilder();
        int v3_1 = v5.length;
        for(v0_3 = 0; v0_3 < v3_1; ++v0_3) {
            v2_2.append(String.format("%02X", Byte.valueOf(v5[v0_3])));
        }
        return v2_2.toString().equals(m);
    }
}

参考BinCrack的时候发现他的方法要快很多很多
在apk文件中有一个org文件夹露出了端倪

搜索”spongycastle”同样可以找到bouncycastle库

破解密钥

JAVA层又啥都没有,直接调CtfLib类中的native函数validate
so的函数列表中没这玩意儿,显然是动态注册的
在JNI_OnLoad中找到
(*v3)->RegisterNatives)(v3, v4, off_5F358004, 1) < 0 )

即这个结构体

(方法名, 类, 函数地址)

进去反编译,整个结构看起来很简单


input接到以后直接拿下来到最后与某个数组异或比较
问题就是这个数组怎么生成的了233

静态分析实在搞不来,认输orz
sha256的表、读取了libc的几个函数头部还有各种乱七八糟的操作,太复杂了(:з」∠)

动态调试的时候注意有两处反调
sub_3c54

这里读取了本进程的status,利用了”TracerPid:\t0”这个字符串来取SHA256表的值来异或
当它读到的时候手动更改内存即可

还有一处sub_3a6c,一样是利用了status中的”TracerPid”字段

BinCrack师傅是通过自己魔改的内核直接使所有status中的TracerPid都显示0从而直接过反调,不过有一个弊端就是如果程序通过ptrace ME来检查
将会发现这点问题
在52的一篇精华中有师傅们的教程和讨论

一般来说Hook也是可以解决这个反调试的,不过这个程序有自校验读取libc,所以Hook并不可行

两处简单的反调修改内存通过以后,Dump出两个异或的数组即可得到flag

纯做题角度而言这题应该算是最简单的,虽然算法比较恐怖但是最终与输入交互的形式比较简单,存在一条很近的捷径

你可能感兴趣的:(CTF,Android)