【知识库】DDCTF 2018 writeup(三) 安卓篇

阅读量    43870 |

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

一.奈沙夜影与DDCTF

本篇文章依旧由DDCTF2018比赛 第一名奈沙夜影提供,关于此次比赛的安卓部分,夜影这样评价:安卓的题目只要抽丝剥茧调试找到关键部分,题目就能轻松解开。ps:除了ECC那道题,加密算法取值不同太恐怖了。TAT

想看前几个方向的writeup可点击:

【知识库】DDCTF 2018 writeup(一) WEB篇

【知识库】DDCTF 2018 writeup(二) 逆向篇

二. 安卓 writeup

0x01  RSA

JAVA层没什么东西,直接将输入送入了Native层的stringFromJNI函数  

这个函数垃圾代码极其的多  

建议动态调试,跟随输入值来观察计算过程

在主函数中首先用gpower生成了32个字节的i²Table,又GetTicks取了两次时间,然而都没有用上。

sub_3133C调用了input,从其中用到的字符串“basic_string::_S_construct null not valid”来看,应该是静态编译的basic_string类的构造函数。

结构体中存放了字符串的长度和其他信息,将指针送给了第一个参数

sub_309E0中没有改变字符串,只是把string的指针送给了返回值,因此就不多纠结了 。跟入那个长的很像库函数名字很奇葩但其实就是核心函数的prj函数

上来第一句

if ( *(_DWORD *)(v2 - 12) == 31 )

虽然一般都能猜出来这个31大概就是input的长度,但较真的话往前翻也能在sub_309E0中找到根据,或者动调可以更直观地看到这个数据  

继续往下,逐字节异或了byte_4DECB数组,很常规的操作

这里的操作看起来比较复杂,但理清了其实很简单  

关键的check其实只有中间那句v10[10]!=*v10  

问题在于判断条件何时满足  

分析一下,要j>=1,则v11>=10,即ii和v10都已+10  

而v10的初值是&d[-10],也就是说异或后的字符串需要从0-30皆满足a[i]==a[i+10]的关系  

也就是一个长为10字节的字符串循环3.1遍  

接着将d[10]赋0,也就是仅保留一遍该字符串  

用d构造了一个basic_string,将其通过atoll转成整数保存下来  

下面的操作比较有意思,将两个字符串构造成string  

那个名字超长的函数点进去可以发现是return j_std::map<char,int,std::less<char>,std::allocator<std::pair<char const,int>>>::operator[](a1, a2);  

就是STL的map对象,pair对是<char, int>  

即第一个循环构造了一个dic,遍历字符串a,将每个值作为key,下标整除2作为value

for i in range(len(a)):
   dic[a[i]]
= i//2

第二个循环则遍历字符串b,将每个值的value取出连接在nptr中,最后atoll转成一个大整数  

当然,比赛的时候没工夫慢慢逆23333直接动调看atoll的结果就是了

最后将两个整数相除,IDA反编译的结果比较乱,需要自己找准变量看  

目标是return 1,即要r=1  

那么v24必须为0,虽然没有给出v24的来源,不过在栈中可以看到

__int64 v24; // r2@24

v24指的是r2,x86和ARM中的除法函数都是会同时计算出商和余数的,并且余数通常会被放在备选寄存器中,商视操作数长度有时存在返回值寄存器中,有时被拆分成高低两段存在两个寄存器中    

而IDA反编译时通常仅关注调用约定中的返回值寄存器,导致这里的v24不知来由  

说了这么多,还是动调最方便啦~  

因此这里要求big_n整除input_n  

继续往下走  

v27=1 => v25=0/HIDWORD(input_n)<v26  

这里的v26是r1,即商的高32位  

v25=0则要求input_n<商的低32位  

综合考虑就是取该数的较小因子了  

将其进行大整数分解,得到两个因子  

1499419583<10> · 3927794789<10>

取较小的1499419583,重复3.1遍后异或数组即可得到flag

a = [73, 90, 75, 10, 67, 92, 65, 80, 65, 75, 85, 93, 67, 13, 70, 64, 65, 1, 92, 6, 1, 89, 91, 14, 90, 82, 65, 93, 8, 94, 6]
r = "1499419583"*4
for i in range(31):
   print(chr(ord(r[i])^a[i]), end='')

0x02  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类的作用等等。 

0x03  Differ-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

0x04  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库  

 

0x05  破解秘钥

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的一篇精华(https://www.52pojie.cn/thread-733981-1-1.html)中有师傅们的教程和讨论 。一般来说Hook也是可以解决这个反调试的,不过这个程序有自校验读取libc,所以Hook并不可行。

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

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

本周安卓篇的writeup到此结束啦,下周发杂项篇writeup哦~

比赛平台地址:http://ddctf.didichuxing.com

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多