空余时间刷了下ddctf,ak 了 android,不难,整理发一下 writeup。
文件链接:https://github.com/LeadroyaL/attachment_repo/tree/master/didictf_2018
一、LeveL1 Java 层什么都没有,直接看 native;native 里包含了一些数学计算。 有 init_array ,但里面主要是一些线程相关操作的初始化,没有JNI_OnLoad。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int __fastcall Java_com_didictf_guesskey2018one_MainActivity_stringFromJNI (JNIEnv *a1, jobject a2, jstring a3) { i = 0 ; bInput = (*a1)->GetStringUTFChars(a1, a3, 0 ); j_j_GetTicks(); do v10 = j_j_gpower(i++); while ( i != 32 ); j_j_GetTicks(); fromBytes((String *)&p_string, bInput); v5 = (String *)fromString((String *)&cp_string, (String *)&p_string); ret = j_j_j__Z20__aeabi_wind_cpp_prjSs((int )v5); finiString((int *)(cp_string - 12 )); finiString((int *)(p_string - 12 )); return ret; }
上来先算了32次平方,不知道想干嘛,调用2次 GetTicks ,不知道想干嘛。之后把输入转为std::string 类型,进入check 函数。 首先检测长度是否为36,以及与 const-data 进行 xor。
1 2 3 4 5 6 7 8 9 10 while ( 1 ){ if ( v13 >= 1 && currentOff < input_len ) { v3 = 0 ; if ( v10[10 ] != *v10 ) break ; } ++currentOff; ++v10;
这个地方校验第010、第1120、第2130、第3040是否一模一样。 最后的检测是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 if ( v24 ) goto LABEL_40; v26 = j_j_j___aeabi_uldivmod(divisor, dividend); v3 = 1 ; v25 = (unsigned int )dividend >= (unsigned int )v26; LODWORD(v26) = 1 ; if ( v25 ) LODWORD(v26) = 0 ; v27 = 1 ; if ( HIDWORD(dividend) >= HIDWORD(v26) ) v27 = 0 ; if ( HIDWORD(dividend) != HIDWORD(v26) ) LODWORD(v26) = v27; if ( !(_DWORD)v26 ) LABEL_40: v3 = 0 ; finiString((int *)v30 - 3 );
这里v3最后被返回了,要求是前者能够整除后者,而且会有除数和商的大小比较,只有除数大于上时候才有可能返回1。
1 dividend = j_j_atoll((const char *)a1->ptr);
往上翻,发现输入仅与除数有关。 被除数是由两个字符串算出来的,怎么算出来的我也看不大懂,好像是重新组合成一个字符串,拼接字符什么样的,应该可以直接 dump。
【后来看某位老哥写的 writeup,发现是通过2个字符串取 index 得到的】 https://blog.csdn.net/dydxdz/article/details/80037937
1 2 3 4 5 6 7 8 map1 = {} str1 = 'deknmgqipbjthfasolrc' for i in range (len (str1)): map1[str1[i]] = i/2 str2 = 'jlocpnmbmbhikcjgrla' k = [] for i in range (len (str2)): print map1[str2[i]],
先创建map<char,int>
,第i 个 char对应的数字是i/2 ,刚好得到每个 char 对应[0,10) 的数字;再查询 str2里每个 char 所对应的下标,将这个下标加上'0' ,拼起来,得到新的十进制的字符串。 综上,拿到数字5889412424631952987 ,将它分解了,5889412424631952987=1499419583*3927794789 ,输入就是偏大的数字,1499419583 ,再 xor 一下常量就行了。 最后 flag 是d5axivcw6ggfswpxg80estgc58h7yghqogbm 。
二、LeveL2 看起来使用的是 Robust 的热更新框架,没有做太特殊的处理,在 assets 里存放了GeekTan.BMP ,其实是个 zip 包,里面放着 Robust 的 patch 文件。 有简单的方法,也有复杂的方法,复杂的就是肉眼去看,把代码运行一遍即可,是个约瑟夫问题,也可以直接求解,跟我以前出的用栈写约瑟夫很像。 简单的方法嘛,直接上 xposed
input text DDCTF{2517299225169920}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 XposedHelpers.findAndHookMethod("cn.chaitin.geektan.crackme.MainActivity" , loader, "Joseph" , int .class, int .class, new XC_MethodHook() { @Override protected void beforeHookedMethod (MethodHookParam param) throws Throwable { super .beforeHookedMethod(param); new Exception().printStackTrace(); Log.d(TAG, "======== before hook =======" ); Log.d(TAG, "with " + (int ) param.args[0 ] + " and" + (int ) param.args[1 ]); } @Override protected void afterHookedMethod (MethodHookParam param) throws Throwable { super .afterHookedMethod(param); Log.d(TAG, "======== after hook =======" ); Log.d(TAG, "result is " + param.getResult()); } });
三、LeveL3 Java 层什么都没有,直接看 native。 init_array 应该是初始化一些东西,没有过多操作。 没有JNI_OnLoad。 直接看 JNI 的方法,进入之后先将输入转化为std::string ,再使用str2ll 转为int64。 长得比较丑,看起来是做divmod(int64, int64) ,循环终止的条件是i==int64(input) ,最后检测余数是否和预期相等。 debug 一下,大概就是左移1bit,mod 一下,左移1bit,mod 一下这样,写段 python 爆破即可。
DDCTF{ddctf-android2-KEY}
p = 0x17A904F1B91290 mod = 0xDBDEE7AE5A90
1 2 3 4 5 6 7 8 9 10 In [23]: i = 1 ...: remain = 1 ...: while True: ...: remain = ((remain << 1) & 0xFFFFFFFFFFFFFFFF) % 0x17A904F1B91290 ...: if remain == 0xDBDEE7AE5A90: ...: print i, remain, hex(remain >> 32), hex(remain & 0xFFFFFFFF) ...: break ...: i += 1 ...: 595887 241750416186000 0xdbdeL 0xe7ae5a90L
不知道这题想干嘛。。。
四、LeveL4 这次只有 java 层,没有 native 层,看起来使用了公开的第三方库 spongycastle,所以丢到网站上 deguard 一下,得到一个非常优美的结果~ 官方说是10位以内的数字,所以是暗示爆破,而且 ECC 么,除了爆破也没有办法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public MainActivity () { super (); this .editText = "00C3632B69D3FC1DD8D80C288C44281B67F4828DC77E37EE338E830E66DC71972A008835BA3156353815DFEDEB4330B48B454F35A88D83DA6260C206E4A619753F97" ; } public void onClickTest (View arg24) { this .outputView.setText("Empty Input" ); TextView v1 = this .preview; this = this ; String v4 = v1.getText().toString(); String v5 = v4; if (v4.length() == 0 ) { v5 = "1" ; } new R$id().init(); ECPoint v11 = SECNamedCurves.getByName("secp256k1" ).getG().multiply(new BigInteger(v5.getBytes())); BigInteger v8 = v11.getXCoord().toBigInteger(); BigInteger v13 = v11.getYCoord().toBigInteger(); byte [] v14 = v8.toByteArray(); byte [] v15 = v13.toByteArray(); byte [] v9 = new byte [v14.length + v15.length]; int v6; for (v6 = 0 ; v6 < v9.length; ++v6) { byte v17 = v6 < v14.length ? v14[v6] : v15[v6 - v14.length]; v9[v6] = v17; } StringBuilder v18 = new StringBuilder(); v6 = v9.length; int v16; for (v16 = 0 ; v16 < v6; ++v16) { v18.append(String.format("%02X" , Byte.valueOf(v9[v16]))); } if (v18.toString().equals(this .editText)) { this .outputView.setText("Correct" ); return ; } this .outputView.setText("Wrong" ); }
使用的是 ECC 加密算法,使用secp256k1曲线,先拿到 G 点,与输入进行椭圆域上的相乘,得到新的点,去校验计算出来的点是否是预先规定好的那个点,是的话就return true 。
这个没什么操作,就是按照描述去爆破,一开始懒得写 java 代码,直接在手机上爆破的(原谅我脑残),发现速度简直慢到炸,手机烫了一晚上也没跑多少数据。
然后想着优化,但发现这个 API 似乎很不好用,G+G+G 和 G*3 不相等,以及各种神奇的表现,可能是我不大会用 API 吧,按理说加法比乘法好做很多,每次加一比每次乘法应该要快,但优化时候老是算出来的不一样,就懒得优化了。
最后在 PC 上写个爆破脚本,早上起来就看到了 flag,DDCTF{54135710}。
五、LeveL5 这题就是反调试的大集合,乱七八糟的方式什么都有,Java 层没有东西,直接看 native。 init_array 没有特殊操作,是 C++的初始化。 JNI_OnLoad里动态注册了 JNI 函数,没有额外操作。 直接看了哈,最原始的长这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 int __fastcall Java_check(const char *b_input) { void *v2; // r0@1 void *v3; // r5@1 int i; // r2@4 char v6[32]; // [sp+4h] [bp+0h]@1 memset(v6, 0, 0x20u); v2 = dlopen("libc.so", 0); v3 = v2; if ( v2 ) { open = (int (__fastcall *)(_DWORD, _DWORD, _DWORD))dlsym(v2, "open"); close = (int (__fastcall *)(int))dlsym(v3, "close"); read = (int (__fastcall *)(_DWORD, _DWORD, _DWORD))dlsym(v3, "read"); strncmp = (int (__fastcall *)(_DWORD, _DWORD, _DWORD))dlsym(v3, "strncmp"); strstr = (int)dlsym(v3, "strstr"); } isTraced = 0; setValue(dword_EF2B5024); maybe_antidebug_1(); some_encrypt_2(dword_EF2B5024, v6); if ( strlen(b_input) == 32 ) { i = 0; do { v6[i] ^= b_input[i]; ++i; } while ( i != 32 ); memcpy(&unk_11100, &v7, 0x20u); // return strncmp(xx, xx, 32); // patch by LeadroyaL } return -1; }
将输入操作一下,xor 一下,返回的是strncmp 的结果,这不是送分题么?
直接上去调试,断下来,发现答案并不对。。。有几个反调试的函数,把 xor_key 给修改了。
sub_3c54是第一个函数,先做一些不知道什么的操作,再检测 tracerPid那行的 strlen ,可以绕过,然后去从sha256_table里取一些值,不知道想干嘛。内层还有一堆不知道在干嘛的函数,估计藏了一些反调试,而且会对 global 的值进行一些操作,乱七八糟的。
反正每次都会被测到反调试,于是懒得搞了,我认输,ok? patch 一下 binary 文件,因为是简单的 xor,所以只要能拿到xor_key 即可,在最后一句他是strncmp,如果把它 patch 为memcpy的话,在正常运行过程中,就可以将算出来的密文保存下来。之后想办法 dump 内存,就能拿到密文,与输入进行 xor ,就拿到了 key。
经过一番努力,终于 patch 成功了。。。如上图的最后一个 memcpy。 先运行,让它算一遍,再 attach,断在最开始,就能拿到明密文对了。 最后算出来是DDCTF{GoodJob,Congratulations!!}。