L-ctf 2016 两道安卓题浅析
这个比赛结束没多久,个人也就随便做了玩玩,两道题其实都不难,尤其是看上去比较难的第二道题,是需要靠投机取巧的题目一:
不得不说界面真渣,不过现在真正搞安卓的都是c大牛,都不玩java的
接下来就需要自己去分析了,第一步当然看看有没有壳,没有就直接反编译看吧,个人比较喜欢jadx,下面都用jadx作为java反编译工具
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView((int) R.layout.activity_main);
ApplicationInfo applicationInfo = getApplicationInfo();
int i = applicationInfo.flags & 2;
applicationInfo.flags = i;
if (i != 0) {
p();
((Button) findViewById(R.id.sureButton)).setOnClickListener(new d(this)); //d方法为触发点击事件的函数
} else {
p();
((Button) findViewById(R.id.sureButton)).setOnClickListener(new d(this));
}
}
private void p() {
try {
InputStream open = getResources().getAssets().open("url.png"); //读取一张图片的值,从144位开始,读取16位数据,保存到全局变量v中
int available = open.available();
Object obj = new byte;
open.read(obj, 0, available);
Object obj2 = new byte;
System.arraycopy(obj, 144, obj2, 0, 16);
this.v = new String(obj2, "utf-8");
} catch (Exception e) {
e.printStackTrace();
}
}
private boolean a(String str, String str2) {
return new c().a(str, str2).equals(new String(new byte[]{(byte) 21, (byte) -93, (byte) -68, (byte) -94, (byte) 86, (byte) 117, (byte) -19, (byte) -68, (byte) -92, (byte) 33, (byte) 50, (byte) 118, (byte) 16, (byte) 13, (byte) 1, (byte) -15, (byte) -13, (byte) 3, (byte) 4, (byte) 103, (byte) -18, (byte) 81, (byte) 30, (byte) 68, (byte) 54, (byte) -93, (byte) 44, (byte) -23, (byte) 93, (byte) 98, (byte) 5, (byte) 59})); //这里暂时看不出来什么东西,等分析完了就知道了
}
看下d方法做了什么
public void onClick(View view) {
if (this.a.a(this.a.v, ((EditText) this.a.findViewById(R.id.passCode)).getText().toString())) {
//拿到了输入框的内容,和之前的v变量,传入到a方法中进行了处理,这里的a方法就是上面的boolean类型函数
TextView textView = (TextView) this.a.findViewById(R.id.textView);
Toast.makeText(this.a.getApplicationContext(), "Congratulations!", 1).show();
textView.setText(R.string.nice);
return;
}
Toast.makeText(this.a.getApplicationContext(), "Oh no.", 1).show();
}
返回到a函数中看到又调用了c类中的a方法,将两个值传入,跟入
public String a(String str, String str2) {
String a = a(str); //调用a方法
String str3 = "";
a aVar = new a();//实例化一个a类,跟入发现是个aes加密
aVar.a(a.getBytes());
try {
return new String(aVar.b(str2.getBytes()), "utf-8");
} catch (Exception e) {
e.printStackTrace();
return str3;
}
}
private String a(String str) {
//这里就是将所有的字符以两位隔开,并交换两个字符的位置,通过for循环可以看出
try {
str.getBytes("utf-8");
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < str.length(); i += 2) {
stringBuilder.append(str.charAt(i + 1));
stringBuilder.append(str.charAt(i));
}
return stringBuilder.toString();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return null;
}
}
aes加密代码:用用户输入数据作为加密数据,图片数据作为加密key
protected void a(byte[] bArr) {
if (bArr == null) {
try {
this.a = new SecretKeySpec(MessageDigest.getInstance("MD5").digest("".getBytes("utf-8")), "AES");
this.b = Cipher.getInstance("AES/ECB/PKCS5Padding");
return;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return;
} catch (NoSuchAlgorithmException e2) {
e2.printStackTrace();
return;
} catch (NoSuchPaddingException e3) {
e3.printStackTrace();
return;
}
}
this.a = new SecretKeySpec(bArr, "AES");
this.b = Cipher.getInstance("AES/ECB/PKCS5Padding");
}
protected byte[] b(byte[] bArr) {
this.b.init(1, this.a);
return this.b.doFinal(bArr);
}
最后拿到加密后的数据与
new String(new byte[]{(byte) 21, (byte) -93, (byte) -68, (byte) -94, (byte) 86, (byte) 117, (byte) -19, (byte) -68, (byte) -92, (byte) 33, (byte) 50, (byte) 118, (byte) 16, (byte) 13, (byte) 1, (byte) -15, (byte) -13, (byte) 3, (byte) 4, (byte) 103, (byte) -18, (byte) 81, (byte) 30, (byte) 68, (byte) 54, (byte) -93, (byte) 44, (byte) -23, (byte) 93, (byte) 98, (byte) 5, (byte) 59})
进行对比,知道了key和加密算法,这里已经理清思路就知道怎么办了
写个解密方法,反响解密下或者比较省事的方法就是在先插桩打印出c类中对v变量进行位置交换的方法的返回值,这样拿到了aes的key,最后直接将上面的bytes数据直接解密就ok
题目二:
拿到后安装到模拟器上是无法打开的,判断是检测了模拟器
看了下也是没有壳的,直接反编译
入口处的主要代码如下:
((Button) findViewById(R.id.button)).setOnClickListener(new OnClickListener() {
public void onClick(View v) {
TextView textView = (TextView) MainActivity.this.findViewById(R.id.editText);
MainActivity.this.this_is_your_flag = textView.getText().toString();
if (MainActivity.this.this_is_your_flag.length() < 35) { 判断用户数据是否低于35位
Process.killProcess(Process.myPid());
} else if (MainActivity.this.this_is_your_flag.length() > 39) {判断用户数据是否大于35位
Process.killProcess(Process.myPid());
}
//这里数据输入长度错误都会直接结束进程
Format format = new Format();
MainActivity.this.this_is_your_flag = format.form(MainActivity.this.this_is_your_flag); //实例化了Format类对字符串进行了截取,代码如下
if (MainActivity.this.this_is_your_flag.length() < 32) {//判断截取后的字符串是否低于32位
Toast.makeText(MainActivity.this.getApplicationContext(), "No,more.", 1).show();
} else if (new Check().check(MainActivity.this.this_is_your_flag)) { //这里就是关键点了,调用了check方法
Toast.makeText(MainActivity.this.getApplicationContext(), "Congratulations!You got it.", 1).show();
} else {
Toast.makeText(MainActivity.this.getApplicationContext(), "Oh no.Come on!", 1).show();
}
}
});
protected String form(String input) {
return input.substring(5, 38);
}
protected String fo1m(String input) {
return input.substring(5, 36);
}
protected String forn(String input) {
return input.substring(5, 39);
}
protected String f0rm(String input) {
return input.substring(5, 37);
}
//这里都是简单的对字符串进行截取,只是长度不一样
private static String[] known_pipes = new String[]{"/dev/socket/qemud", "/dev/qemu_pipe"};
String emulator = checkPipes();
private native boolean checkPasswd(String str);
protected native boolean checkEmulator(String str);
public static String checkPipes() {
for (String pipes : known_pipes) {
if (new File(pipes).exists()) {
return "true";
}
}
return "false";
}
boolean check(String pass) {
if (checkEmulator(this.emulator)) {
return false;
}
return checkPasswd(pass);
}
主要的check方法中第一步调用了checkEmulator,emulator在checkPipes中进行了初始化,这里简单的判断了下是否是模拟器,是的话直接返回false,所以做这类东西的时候还是在手机上做比较好,坑比较少
当然checkEmulator是个native方法,还需要看下so,如果过掉模拟器检测,最后就会真正的检测密码
ida打开下so
.text:00005074 EXPORT Java_com_example_ring_wantashell_Check_checkEmulator
.text:00005074 Java_com_example_ring_wantashell_Check_checkEmulator
.text:00005074 PUSH {R4,R6,R7,LR}
.text:00005076 ADD R7, SP, #8
.text:00005078 MOVS R1, #0x2A4
.text:0000507C LDR R3,
.text:0000507E LDR R3,
.text:00005080 MOVS R4, #0
.text:00005082 PUSH {R2}
.text:00005084 POP {R1}
.text:00005086 PUSH {R4}
.text:00005088 POP {R2}
.text:0000508A BLX R3
.text:0000508C LDR R1, =(aTrue - 0x5092)
.text:0000508E ADD R1, PC ; "true"
.text:00005090 BL j_j_strcmp//这里就简单的判断下传入值和true是否相等,相等等于0,否则等于1
.text:00005094 PUSH {R0}
.text:00005096 POP {R1}
.text:00005098 MOVS R0, #1
.text:0000509A CMP R1, #0
.text:0000509C BEQ locret_50A2
.text:0000509E PUSH {R4}
.text:000050A0 POP {R0}
不过这个so还有jni_Onload函数,不看的话坑还是比较多
while ( 1 )
{
filename = v3 + 1;
j_j_sprintf((char *)&v43, "/data/dalvik-cache/data@app@%s-%d.apk@classes.dex", "com.example.ring.wantashell");
v39 = 1869770799;
v40 = 1702047587;
v41 = 1831822956;
v42 = 7565409;
v4 = j_j_fopen((const char *)&v39, "r");//读取内存中dex数据
if ( v4 )
{
while ( 1 )
{
v5 = j_j_fgets(&s, 1024, v4);
v6 = 0;
if ( !v5 )
break;
if ( j_j_strstr(&s, (const char *)&v43) )
{
v7 = j_j_strtok(&s, "-");
v30 = 0;
v8 = j_j_strtoul(v7, 0, 16);
if ( v8 != 0x8000 )
v30 = v8;
v6 = v30;
break;
}
}
v9 = v6;
j_j_fclose(v4);
if ( v9 )
break;
}
v3 = filename;
if ( (signed int)filename >= 2 )
goto LABEL_27;
}
v31 = v9;
v39 = 1869770799;
v40 = 1702047587;
v41 = 1831822956;
v42 = 7565409;
v10 = j_j_fopen((const char *)&v39, "r");
v11 = 0;
if ( v10 )
{
while ( 1 )
{
v11 = 0;
if ( !j_j_fgets(&s, 1024, v10) )
break;
if ( j_j_strstr(&s, (const char *)&v43) )
{
v12 = j_j_strtok(&s, " ");
v13 = j_j_strtok(v12, "-");
v14 = j_j_strtok(0, "-");
v15 = j_j_strtoul(v14, 0, 16);
v11 = v15 - j_j_strtoul(v13, 0, 16);
break;
}
}
j_j_fclose(v10);
}
v16 = *(_DWORD *)(v31 + 8) + v31;
if ( !j_j_strcmp((const char *)v16, "dex\n035") )//拿到dex
{
dword_1E0CC = v16;
dword_1E0A4 = v16;
dword_1E0A8 = *(_DWORD *)(v16 + 60) + v16;
dword_1E0AC = *(_DWORD *)(v16 + 68) + v16;
dword_1E0B4 = *(_DWORD *)(v16 + 92) + v16;
dword_1E0B0 = *(_DWORD *)(v16 + 84) + v16;
dword_1E0BC = *(_DWORD *)(v16 + 100) + v16;
dword_1E0B8 = *(_DWORD *)(v16 + 76) + v16;
filenamea = (char *)sub_5570((int)"f0rm");
if ( filenamea )
{
v17 = (_WORD *)sub_5570((int)"form");
if ( v17 )
{
if ( sub_5570((int)"fo1m") && sub_5570((int)"forn") && !j_j_mprotect((void *)v31, v11, 7) )//看到这些玩意大概判断这里可能用到了动态修改字节码
{
*v17 = *(_WORD *)filenamea;
v18 = *((_DWORD *)filenamea + 3);
if ( v18 )
{
v19 = filenamea + 16;
v20 = v17 + 8;
do
{
*v20 = *v19;
++v19;
++v20;
--v18;
}
while ( v18 );
}
j_j_mprotect((void *)v31, v11, 5);
}
}
}
}
跟进sub_5570方法里看到了关键处
实质这里将form和f0rm方法进行了对调,所以真正调用的截取字符串的方法是f0rm
接下来还调用了ptrace和检测模拟器的方法
v31 = 0;
j_j_ptrace(0, 0, 0, 0);
sub_91C0(&v75, "/system/lib/libc_malloc_debug_qemu.so", &v62);
sub_91C0(&v76, "/sys/qemu_trace", &v61);
sub_91C0(&v77, "/system/bin/qemu-props", &v60);
do
{
v32 = j_j_fopen((const char *)*(&v75 + v31++), "r");
if ( v32 )
goto LABEL_44;
}
while ( v31 < 3 );
v71 = 1869770799;
v72 = 1885548387;
v73 = 1718511989;
LOWORD(v74) = *(_WORD *)"o";
v33 = j_j_fopen((const char *)&v71, "r");
if ( !v33 || !j_j_fgets(&s, 1024, v33) )
{
这里就是为了加大动态调试的难度,不过这道题太投机取巧,完全不需要调试
看下真正的主体方法checkpassword
if ( v9 )
{
do
{
v11 = v8;
v8 = v8;
v8 = v11;
}
while ( v10++ < v9 );
}
将字符串顺序颠倒
v13 = j_j_strlen(v8);
sub_7118((int)&v22, (int)v8, v13);
j_j_j__Z7encryptPKcj(v20, v22, *(_DWORD *)(v22 - 12)); //猜测这里就是对字符串进行了加密
v14 = *(_DWORD *)v20;
sub_8740(*(_DWORD *)v20 - 12, &v23);
sub_8740(v22 - 12, &v23);
(*v18)->ReleaseStringUTFChars(v18, v19, v17);
v15 = sub_7B10(&secret, v14);将加密后的值与secret这个引用里的值进行处理,跟进下这个方法
v5 = 1;
if ( v15 )
v5 = 0;
}
if ( _stack_chk_guard != v24 )
j_j___stack_chk_fail(_stack_chk_guard - v24);
return v5;
sub_7b10方法
int __fastcall sub_7B10(const void **a1, const char *a2)
{
const void *v2; // r7@1
const char *v3; // r6@1
size_t v4; // r5@1
size_t v5; // r4@1
size_t v6; // r2@1
int result; // r0@3
v2 = *a1;
v3 = a2;
v4 = *((_DWORD *)*a1 - 3);
v5 = j_j_strlen(a2);
v6 = v5;
if ( v5 > v4 )
v6 = v4;
result = j_j_memcmp(v2, v3, v6);
if ( !result )
result = v4 - v5;
return result;
}
这里没有用strcmp,而是用的memcmp,v6是字符串的长度,还是相当于字符串的比较
那就可以关注下secret是什么了
双击进入sub_4c54方法
int sub_4C54()
{
int result; // r0@1
int v1; // @1
char v2; // @1
int v3; // @1
v3 = _stack_chk_guard;
sub_91C0(&secret, "dHR0dGlldmFodG5vZGllc3VhY2VibGxlaHNhdG5hd2k.", (int)&v1);
j_j___cxa_atexit(sub_6BE4, &secret, &unk_1E000);
sub_91C0(&dword_1E09C, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", (int)&v2);
j_j___cxa_atexit(sub_6BE4, &dword_1E09C, &unk_1E000);
result = _stack_chk_guard - v3;
if ( _stack_chk_guard != v3 )
j_j___stack_chk_fail(result);
return result;
}
那这里大概可以猜测真正的加密后字符串就是dHR0dGlldmFodG5vZGllc3VhY2VibGxlaHNhdG5hd2k.
既然知道了加密后字符串,就需要去关注下加密算法了
说实话,这一箩筐代码我真没看懂,不过如果在jni_Onload中过掉反调试或者直接修改sonop掉反调试代码的话,就知道了,这里其实是个base64
所以吗,这道题灵魂就是靠猜。。。。。
直接把dHR0dGlldmFodG5vZGllc3VhY2VibGxlaHNhdG5hd2k.解码下得到ttttievahtnodiesuacebllehsatnawi,颠倒顺序
答案就已经出来了
附件:
搞真正安卓的都是C大牛?你从哪里知道的,搞C有个卵用,安卓本身就是java虚拟机你真会逗 SGC沉默 发表于 2016-10-18 12:12
说的好像你很牛逼一样,你指的安卓为什么每次GC的时候都会增加一个线程么,你知道死锁的时候都会netiy么 ...
我是菜鸟。
首先,我看得出来你是一个搞Java的,不管在java领域是不是有很深的造诣。
但是在Android中并不运行着Java虚拟机,Android采用自家的Dalvik虚拟机来执行程序,即便大部分应用都是在Java层编码,但最后都是经过dx.exe将class文件(Java虚拟机可执行文件)转换为dex文件(Dalvik虚拟机可执行文件)。
而繁华大大所说的C,是指Native层(即.SO)。即比Java层更加接近底层的开发,安全性及效率都较高,而NDK开发最后也是通过Dalvik虚拟机来调用执行,与Java虚拟机并没有半毛钱关系。
死锁: 据我很久之前的知识了解...这应该是代码逻辑的问题吧???跟执行环境似乎没啥关系,然后我这小学生也不懂netiy是个什么东西。。。(求大牛解释?)
以上,是我已知了解的知识,若有哪里有错误或缺漏,欢迎指出纠正。 我想问下为什么我没有调试运行的时候就会闪退,真机运行的时候 SGC沉默 发表于 2016-10-17 12:15
搞真正安卓的都是C大牛?你从哪里知道的,搞C有个卵用,安卓本身就是java虚拟机你真会逗
呵呵~兄弟你没理解人家要表达的意思 感谢分享,学习一下~ 楼主好厉害。... 沙发真逗… 大神的世界我也不懂,只好默默的顶一下 围观大神 {:17_1089:} 辛苦支持