KCTF2020秋季赛安卓逆向题解隐晦的修改java层代码带来的rabbithole 2020-12-02 09:48:45 Steven Xeldax [TOC] ## 逆向JAVA层 代码中首先判断system 版本信息,题目中也说过必须要使用android 7 以上版本  调用init函数进行初始化  我们跟进init函数,将assert中的b.txt利用dexloader载入到内存中。b.txt本身是一个dex文件。  再回到onCreate函数中,随后代码开始监听button事件,把用户输入的字符串传入MainActivity.check函数中进行检查,如果结果返回True则校验通过。  我们跟进到check函数中,主要逻辑去调用动态加载dex中的check方法。  跟进check方法  content的长度必须等于16  代码12-23行首先使用crypt在java层进行第一次加密,然后从MyCrack中取出第crypt变量。  MyCrack的变量为otVvmpP4ZI58pqB26OTaYw==  代码33-50行首先调用jni中的crackjni方法进行一次加密,然后再调用java层中的crypt方法进行一次加密。  接着我们来看下crypt来分析下究竟是什么加密 ``` public static byte[] crypt(byte[] b_data) { String mKkey = "kaokaonio"; if (b_data == null) { return null; } int x = 0; int y = 0; byte[] b_key = mKkey.getBytes(); byte[] key = new byte[256]; for (int i = 0; i < 256; i++) { key[i] = (byte) i; } int index1 = 0; int index2 = 0; if (b_key == null || b_key.length == 0) { return null; } for (int i2 = 0; i2 < 256; i2++) { index2 = ((b_key[index1] & 255) + (key[i2] & 255) + index2) & 255; byte tmp = key[i2]; key[i2] = key[index2]; key[index2] = tmp; index1 = (index1 + 1) % b_key.length; } byte[] result = new byte[b_data.length]; for (int i3 = 0; i3 < b_data.length; i3++) { x = (x + 1) & 255; y = ((key[x] & 255) + y) & 255; byte tmp2 = key[x]; key[x] = key[y]; key[y] = tmp2; result[i3] = (byte) (b_data[i3] ^ key[((key[x] & 255) + (key[y] & 255)) & 255]); } return result; } ``` 没有特征,这种应该不是自己构造的专门算法,肯定是某一个公开的加密算法,或者对公开加密算法的魔改算法。 一般来说去识别一个加密算法主要有两种方法,第一种是搜索关键的变量,比如MD5中的特定变量黄金分割函数等等,我们可以借助yara工具来进行扫描。第二种方法是根据算法特征操作一个个算法比对过去。 根据 ``` ((b_key[index1] & 255) + (key[i2] & 255) + index2) & 255; ) (b_data[i3] ^ key[((key[x] & 255) + (key[y] & 255)) & 255]); ``` 两个特征我们发现这个是一个rc4的算法 整理下流程 > 用户输入 -> rc4 密钥是: kaokaonio -> cracklib 加密 -> rc4加密 ## 逆向NATIVE层 粗看一下发现代码量还是相对比较多的,并且似乎有一些decode函数?  直接来到Java_com_kanxue_crackme_MyCrack_crackjni  我们观察发现大部分的字符串全是一些二进制数据  看下data段,应该是做过处理整个data段都是加密的  我们自顶向下进行分析一个native层的加载流程是先调用.init_array然后是JNI_Onload init_array  好多decode函数做了大量的异或操作,如果把代码抠出来进行静态还原就似乎太麻烦了估计还是得动态去跑一下。  JNI_OnLoader不知道做了啥,需要动态跑一下  ## 动态调试 对Java_com_kanxue_crackme_MyCrack_crackjni下断点,然后输入16位数据,触发breakpoint之后发现没有反调试,成功到达断点处,此时data段中的数据经过init_array已经是解密的了 继续分析Java_com_kanxue_crackme_MyCrack_crackjni  AES加密  密钥直接在data段中直接看到  然后在代码中对b.txt做了一些手脚  看下这处的代码 ``` v8 = base_address_of_btxt + 0x16D3A6; v9 = *(_BYTE *)(base_address_of_btxt + 0x16D3A6); v10 = bsscounter++; *(_BYTE *)(base_address_of_btxt + 0x16D3A6) = v9 + v10; ``` 对dex基址的某段逻辑做了一系列处理,直接跳转到这个内存上看看改了些什么。 看不到东西,我们直接将dex拖入ida,再去看看0x16D3A6里有啥  再回到动态调试  貌似把最后一个字符改成了D3,也就是001A3d3f  发现将String mKkey = "kaokaonio";改为keepGoing。  逐步动态调试,发现修改了MyCrack类中的crypt属性,从otVvmpP4ZI58pqB26OTaYw==改为l+x7fKd2FBaaEY4NV4309A==  ## 重新回到JAVA层 还有一种办法我们直接在调用cracklib函数的时候dump出dex内存然后再拖入查看代码 代码运行到这里  然后dumpdex ## 梳理流程 用户输入 -> RC4(kaokaonio) -> AES(kaokaonikaokaoni) -> RC4(keepGoing) =>l+x7fKd2FBaaEY4NV4309A== ## 解密脚本 ``` from Crypto.Cipher import AES from Crypto.Cipher import ARC4 import base64 def AESDecrypt(data, key): aes1 = AES.new(key.encode(),mode=AES.MODE_ECB) encrypted = aes1.decrypt(data) return encrypted def RC4Decrypt(data, key): rc4 = ARC4.new(key.encode()) decrypted = rc4.decrypt(data) return decrypted result = 'l+x7fKd2FBaaEY4NV4309A==' result = base64.b64decode(result) result = RC4Decrypt(result, 'keepGoing') result = AESDecrypt(result, 'kaokaonikaokaoni') print(RC4Decrypt(result, 'kaokaonio')) ``` ## 总结 这道题首先考点: 1. 识别RC4 2. 通过init_array解密data段 3. 识别AES 4. 隐晦的修改了java层代码,通过地址修改dex属性