浅谈2015AliCTF-MSC前三题解题思路
2015-06-15 11:11
330 查看
//weibo: @少仲
题目下载地址: http://bbs.pediy.com/showthread.php?t=197462
0x1 第一题
验证密码,显示错误,直接连上DDMS看log,看到了一堆乱码.直接反编译apk,看看加解密流程.
先看34行所说,找到对话框的提示,然后具体看看传递值的定义
protected void onCreate(Bundle paramBundle) { super.onCreate(paramBundle); requestWindowFeature(1); setContentView(2130903064); final EditText localEditText = (EditText)findViewById(2131034173); ((Button)findViewById(2131034174)).setOnClickListener(new View.OnClickListener() { public void onClick(View paramAnonymousView) { String str1 = localEditText.getText().toString(); String str2 = MainActivity.this.getTableFromPic(); String str3 = MainActivity.this.getPwdFromPic(); Log.i("lil", "table:" + str2); Log.i("lil", "pw:" + str3); String str4 = ""; try { str4 = MainActivity.bytesToAliSmsCode(str2, str1.getBytes("utf-8")); Log.i("lil", "enPassword:" + str4); if ((str3 != null) && (!str3.equals("")) && (str3.equals(str4))) { //猜测这里是正确的提示框,具体看showDialog函数 MainActivity.this.showDialog(); return; } } catch (UnsupportedEncodingException localUnsupportedEncodingException) { for (;;) { localUnsupportedEncodingException.printStackTrace(); } //对话框提示 AlertDialog.Builder localBuilder = new AlertDialog.Builder(MainActivity.this); //这个是具体传递的值,在反编译的R.java中搜索一下可以知道是错误信息 localBuilder.setMessage(2131361809); localBuilder.setTitle(2131361808); localBuilder.setPositiveButton(2131361811, new DialogInterface.OnClickListener() { public void onClick(DialogInterface paramAnonymous2DialogInterface, int paramAnonymous2Int) { paramAnonymous2DialogInterface.dismiss(); } }); localBuilder.show(); } } } } }
再看24行的猜测是否正确.
private void showDialog() { AlertDialog.Builder localBuilder = new AlertDialog.Builder(this); //传递的值就是正确的提示框 localBuilder.setMessage(2131361810); localBuilder.setTitle(2131361808); localBuilder.setPositiveButton(2131361811, new DialogInterface.OnClickListener() { public void onClick(DialogInterface paramAnonymousDialogInterface, int paramAnonymousInt) { paramAnonymousDialogInterface.dismiss(); } }); localBuilder.show(); }
现在已经明确具体的位置,可以看看生成正确对话框提示的条件是什么,看第21行的注释,最重要的就是str3和str4要相等,所以要理清str1.str2.str3.str4.
String str1 = localEditText.getText().toString(); String str2 = MainActivity.this.getTableFromPic(); String str3 = MainActivity.this.getPwdFromPic(); str4 = MainActivity.bytesToAliSmsCode(str2, str1.getBytes("utf-8"));
str1是输入的字符串密码,str2和str3都是读取图片资源里预设的密码.str4就要具体分析bytesToAliSmsCode函数.
private static String bytesToAliSmsCode(String paramString, byte[] paramArrayOfByte) { StringBuilder localStringBuilder = new StringBuilder(); for (int i = 0;; i++) { if (i >= paramArrayOfByte.length) { return localStringBuilder.toString(); } localStringBuilder.append(paramString.charAt(0xFF & paramArrayOfByte[i])); } }
通过分析可以看出,用户输入的str1转换成数组传递,并用i作为下标获取paramArrayOfByte数据,再把取出来的数据0xFF & paramArrayOfByte[i]做运算.运算的结果累加返回.说白了就是从图片资源中获取密码表,然后输入密码找到索引号,然后给出ASCII值,aliCodeToBytes就是解密函数.直接写个java代码解密就好.
(1).方法一
package calc; public class calc { private static byte[] aliCodeToBytes(String paramString1, String paramString2) { byte[] arrayOfByte = new byte[paramString2.length()]; for (int i = 0;; i++) { if (i >= paramString2.length()) { return arrayOfByte; } arrayOfByte[i] = ((byte)paramString1.indexOf(paramString2.charAt(i))); } } public static void main(String args[]) { String table = "一乙二十丁厂七卜人入八九几儿了力乃刀又三于干亏士工土才寸下大丈与万上小口巾山千乞川亿个勺久凡及夕丸么广亡门义之尸弓己已子卫也女飞刃习叉马乡丰王井开夫天无元专云扎艺木五支厅不太犬区历尤友匹车巨牙屯比互切瓦止少日中冈贝内水见午牛手毛气升长仁什片仆化仇币仍仅斤爪反介父从今凶分乏公仓月氏勿欠风丹匀乌凤勾文六方火为斗忆订计户认心尺引丑巴孔队办以允予劝双书幻玉刊示末未击打巧正扑扒功扔去甘世古节本术可丙左厉右石布龙平灭轧东卡北占业旧帅归且旦目叶甲申叮电号田由史只央兄叼叫另叨叹四生失禾丘付仗代仙们仪白仔他斥瓜乎丛令用甩印乐"; String password = "义弓么丸广之"; String strResult = new String(aliCodeToBytes(table, password)); System.out.println(strResult); } }
密码就是581026.
(2).方法二
python版.
# -*- coding: UTF-8 -*- table = u'一乙二十丁厂七卜人入八九几儿了力乃刀又三于干亏士工土才寸下大丈与万上小口巾山千乞川亿个勺久凡及夕丸么广亡门义之尸弓己已子卫也女飞刃习叉马乡丰王井开夫天无元专云扎艺木五支厅不太犬区历尤友匹车巨牙屯比互切瓦止少日中冈贝内水见午牛手毛气升长仁什片仆化仇币仍仅斤爪反介父从今凶分乏公仓月氏勿欠风丹匀乌凤勾文六方火为斗忆订计户认心尺引丑巴孔队办以允予劝双书幻玉刊示末未击打巧正扑扒功扔去甘世古节本术可丙左厉右石布龙平灭轧东卡北占业旧帅归且旦目叶甲申叮电号田由史只央兄叼叫另叨叹四生失禾丘付仗代仙们仪白仔他斥瓜乎丛令用甩印乐' password = u'义弓么丸广之' for result in password: print chr(table.find(result))
(3).方法三
方法三并非正规解法,而是在我已经知道答案的情况下,巧妙地通过判断数字1-9在表中的位置得到的.通过app特有的lil标签,用adb logcat过滤信息.
输入数字123456789,得到对应的字符.
· 1 -么
· 2 -广
· 3 -亡
· 4 -门
· 5 -义
· 6 -之
· 7 -尸
· 8 -弓
· 9 -己
再根据”义弓么丸广之”,可得581026
0x2 第二题
(1).方法一:上来直接反编译看看
关键函数securityCheck在so里.IDA载入后,习惯性的先看字符串信息
难道答案如此简单?测试后发现不对,估计是运行后修改了.所以再看看代码.
我现在的思路就是调用print函数,打印修改后真正的密码,所以要在关键比较的地方patch.
LDRB R3,[R2] -> MOV R1,R0 LDRB R1,[R0] -> BL LOC_1280
使用010editor修改opcode,修改后so的F5的C代码
重打包签名后运行,崩溃前打印了结果.
(2).方法二:
方法一是一个比较取巧的方法,方法二则就真正的动态调试解决问题.直接在securitycheck函数里下断点,然后调试的时候apk会自动关闭,此时猜测到程序会有反调试.所以只能从JNI_Onload入手.
(1).执行android_server
(2).端口转发adb forward tcp:23946 tcp:23946
(3).调试模式启动程序adb shell am start -D -n com.yaotong.crackme/.MainActivity
(4).IDA附加到crackme进程
(5).Debugger->Debugger Option,选择suspendLibrary Load/Unload
(6).在DDMS中查看调试端口
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8600
(7).在IDA中运行
IDA成功中断
(8).ctrl+s查找libcrackme.so的基地址
我这里是AB825000
(9).函数地址=基地址+偏移地址
基地址已经找到,偏移地址就是静态分析中可以看到,JNI_Onload的偏移是0x1B9C,所以地址=0x1B9C+ 0xAB825000=0xAB826B9C,跳转到该地址下断
(10).单步执行,找到反调试的地方
多次测试,在这里断开,我们F7跟进看看
(11).跟进后发现是一个创建线程的函数,猜测是创建线程监控ida
这里猜测方法就是TracerPid,如果把BEQ改成B,就能跳过检测,或者直接把创建线程的函数给nop掉,不创建监控函数就可以绕过.为了方便调试,我这里把标志位Z修改为1,让它跳转.然后线程无法创建,成功过掉反调试.
(12).在securityCheck处下断点,动态调试可以获取密码
SecurityCheck地址=0x11A8+0xAB825000=0xAB8261A8
关于反调试.
值得一提的是它的方法是检测TracerPid的值.在非调试状态下,TracerPid的值为0.调试状态下它的值就是调试器的pid.它具体实现的步骤应该是创建一个线程,在新线程里循环读取/proc/xxx/status文件里的TracerPid字段,当它不为0的时候,kill掉调试进程.这样达到反调试目的.附上一个反调试代码.
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <string.h> #include <errno.h> inline void _check(void) { char status[0x64] = { '\0' }; FILE *fd = 0; char buf[0x200] = { '\0' }; char *offset = NULL; char tracer[0x200] = { '\0' }; pid_t tracer_pid = 0; printf("main\n"); memset(status, 0, 0x64); snprintf(status, 0x64, "/proc/%d/status", getpid()); fd = fopen(status, "r"); if (fd == 0) { printf("open status failed\n"); return; } while (1) { offset = NULL; memset(buf, 0, 0x200); if (fgets(buf, 0x200, fd) == NULL) break; offset = strstr(buf, "TracerPid"); if (offset != NULL) break; } fclose(fd); if (offset == NULL) return; memset(tracer, 0, 0x200); sscanf(offset, "%s %d", tracer, &tracer_pid); printf("tracer_pid=%d\n", tracer_pid); if (tracer_pid != 0) { printf("kill %d\n", tracer_pid); kill(tracer_pid, 9); printf("%s\n", strerror(errno)); } } void *check_main(void *argv) { while (1) { _check(); sleep(3); printf("No Tracer\n"); } } int main(int argc, char const *argv[]) { printf("main\n"); pthread_t thread; pthread_create(&thread, 0, check_main, 0); // TODO .. // while (1) { printf("main sleep\n"); sleep(1); } return 0; }
0x3 第三题
apk被加壳了.由于对dex脱壳之类的完全不了解,只好拿到了别人已经dump完整的dex文件,进行学习分析…看了一把so文件应该是有ptrace反调试,nop掉应该不难.经过分析,核心部分在于b类和e类.e类是一个类莫斯电码转换.b类的核心函数是run方法.其流程为:1. 获取用户输入的内容
2. 调用e.a()解码
3. 使用AES key解密
4. 要求电码前两个字符hashcode等于3618且两个字节相加等于168
5. 从类e,a中获得f标签为”7e”,”lp”.可得后四位”7elp”.
6. 其余aes,sha1,base64计算均为混淆视听的运算
for ( size_t i = 33; i < 127; ++i ) { for ( size_t j = 33; j < 127; ++j ) { String x =String.valueOf((char)j)+String.valueOf((char)i); if (x.hashCode()==3618 && (i+j) == 168) { System.out.println(x); System.out.println(j+i); } } }
可以枚举出前两位为”s5”或者”qs”.经过测试结果为”s57e1p”.通过莫斯电码转换可得答案” ... ____ ___. . ..___ .__.”
0x4 总结
第一题是apk逆向的入门题目,考了一把反编译的基本知识,只要大概理清楚关系,都能搞定.第二题是动态调试的入门题目,以及反调试入门.
第三题难度和前两道题比难度陡增,反调试+动态调试+壳.
看来还是要继续加强逆向方面的学习,以及加固方面的研究.
相关文章推荐
- jqGrid API(中英文)
- JdbcTemplate 、NamedParameterJdbcTemplate、SimpleJdbcTemplate的区别
- 窥探Swift之基本数据类型
- 常见文件格式信息
- JQuery工具函数汇总
- 应用 Valgrind 发现 Linux 程序的内存问题
- Linux-Buffer和Cache的区别
- 查看Linux操作系统版本
- GeoStru.CVSoil.v2014.7.2.47 1CD
- 每天进步一点点——linux——rm
- w3cschool.cc SQLite 教程
- c++空类实例大小不是0原因
- 第十五周 课后实践:阅读程序1
- mac+windows双分区
- UML系列图--用例图
- linux 命令——10 cat (转)
- unity free asset
- Win8删除文件时不弹出提示窗口的详细解决办法
- 统计字符数
- 【黑马程序员】------java基础----String类、StringBuffer(StringBuilder)、基本数据类型包装类