吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 15491|回复: 35
收起左侧

[Android CTF] L-ctf 2016 两道安卓题浅析

  [复制链接]
qtfreet00 发表于 2016-10-17 12:10
这个比赛结束没多久,个人也就随便做了玩玩,两道题其实都不难,尤其是看上去比较难的第二道题,是需要靠投机取巧的

题目一:
QQ截图20161017111202.png

不得不说界面真渣,不过现在真正搞安卓的都是c大牛,都不玩java的

接下来就需要自己去分析了,第一步当然看看有没有壳,没有就直接反编译看吧,个人比较喜欢jadx,下面都用jadx作为java反编译工具

[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[available];
            open.read(obj, 0, available);
            Object obj2 = new byte[16];
            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方法做了什么
[Java] 纯文本查看 复制代码
 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方法,将两个值传入,跟入

[Java] 纯文本查看 复制代码
 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
[Java] 纯文本查看 复制代码
 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);
    }


最后拿到加密后的数据与
[Java] 纯文本查看 复制代码
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

题目二:
拿到后安装到模拟器上是无法打开的,判断是检测了模拟器
看了下也是没有壳的,直接反编译
入口处的主要代码如下:
[Java] 纯文本查看 复制代码
((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();
                }
            }
        });


[Java] 纯文本查看 复制代码
  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);
    }
//这里都是简单的对字符串进行截取,只是长度不一样


[Java] 纯文本查看 复制代码
    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

[Asm] 纯文本查看 复制代码
.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, [R0]
.text:0000507E                 LDR     R3, [R3,R1]
.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函数,不看的话坑还是比较多
[C] 纯文本查看 复制代码
    while ( 1 )
    {
      filename = v3 + 1;
      j_j_sprintf((char *)&v43, "/data/dalvik-cache/data@app@%s-%[email]d.apk@classes.dex[/email]", "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方法里看到了关键处
QQ截图20161017115039.png
实质这里将form和f0rm方法进行了对调,所以真正调用的截取字符串的方法是f0rm
接下来还调用了ptrace和检测模拟器的方法
[C] 纯文本查看 复制代码
  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
[C] 纯文本查看 复制代码
    if ( v9 )
    {
      do
      {
        v11 = v8[v10 - 1];
        v8[v10 - 1] = v8[v9];
        v8[v9--] = v11;
      }
      while ( v10++ < v9 );
    }

将字符串顺序颠倒

[C] 纯文本查看 复制代码
  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方法
[C] 纯文本查看 复制代码
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是什么了
QQ截图20161017120231.png
双击进入sub_4c54方法
[C] 纯文本查看 复制代码
int sub_4C54()
{
  int result; // r0@1
  int v1; // [sp+0h] [bp-18h]@1
  char v2; // [sp+4h] [bp-14h]@1
  int v3; // [sp+8h] [bp-10h]@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.

既然知道了加密后字符串,就需要去关注下加密算法了
QQ截图20161017120439.png
说实话,这一箩筐代码我真没看懂,不过如果在jni_Onload中过掉反调试或者直接修改sonop掉反调试代码的话,就知道了,这里其实是个base64
所以吗,这道题灵魂就是靠猜。。。。。

直接把dHR0dGlldmFodG5vZGllc3VhY2VibGxlaHNhdG5hd2k.解码下得到ttttievahtnodiesuacebllehsatnawi,颠倒顺序
QQ截图20161017120758.png
答案就已经出来了

附件:
apk.zip (3.49 MB, 下载次数: 460)

免费评分

参与人数 13威望 +2 热心值 +13 收起 理由
zhuzaiting + 1 谢谢@Thanks!
1372_7 + 1 谢谢@Thanks!
暮清 + 1 我很赞同!
lawlier + 1 谢谢@Thanks!
lakshmi + 1 谢谢@Thanks!
AWEIWEI + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
myouter + 1 验证总是错误。。。醉了。第一题思路分析结果一样,就是插桩输出没成,懒得.
wangsheng66 + 1 热心回复!
mengzhenhai + 1 围观大神
1789912406 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
某中二绅士 + 1 心疼沙发。。。
renfeng + 1 空姐一出手就知有没有
Hmily + 2 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

SGC沉默 发表于 2016-10-17 12:15
搞真正安卓的都是C大牛?你从哪里知道的,搞C有个卵用,安卓本身就是java虚拟机你真会逗

点评

下次装逼之前请了解一下基本知识,安卓和java虚拟机没有半毛钱关系  发表于 2016-10-17 16:04
菜鸡葫芦娃 发表于 2016-10-18 14:22 来自手机
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是个什么东西。。。(求大牛解释?)
以上,是我已知了解的知识,若有哪里有错误或缺漏,欢迎指出纠正。
FraMeQ 发表于 2016-10-17 12:32
我想问下为什么我没有调试运行的时候就会闪退,真机运行的时候
windwing1883 发表于 2016-10-17 15:26
SGC沉默 发表于 2016-10-17 12:15
搞真正安卓的都是C大牛?你从哪里知道的,搞C有个卵用,安卓本身就是java虚拟机你真会逗

呵呵~兄弟你没理解人家要表达的意思
某中二绅士 发表于 2016-10-17 19:18
感谢分享,学习一下~
Poison丶毒 发表于 2016-10-17 19:26
楼主好厉害。...
wanglaihuai 发表于 2016-10-17 19:38 来自手机
沙发真逗…
1789912406 发表于 2016-10-17 19:53
大神的世界我也不懂,只好默默的顶一下
mengzhenhai 发表于 2016-10-17 19:54
围观大神   
making 发表于 2016-10-18 06:48 来自手机
辛苦支持
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-24 22:52

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表