一.奈沙夜影与DDCTF
本篇文章依旧由DDCTF2018比赛 第一名奈沙夜影提供,关于此次比赛的安卓部分,夜影这样评价:安卓的题目只要抽丝剥茧调试找到关键部分,题目就能轻松解开。ps:除了ECC那道题,加密算法取值不同太恐怖了。TAT
想看前几个方向的writeup可点击:
【知识库】DDCTF 2018 writeup(一) WEB篇
【知识库】DDCTF 2018 writeup(二) 逆向篇
二. 安卓 writeup
0x01 RSA
JAVA层没什么东西,直接将输入送入了Native层的stringFromJNI函数
这个函数垃圾代码极其的多
建议动态调试,跟随输入值来观察计算过程
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中找到根据,或者动调可以更直观地看到这个数据
这里的操作看起来比较复杂,但理清了其实很简单
关键的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
发表评论
您还未登录,请先登录。
登录