吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 7582|回复: 18
收起左侧

[Android 原创] 某CrackMe出题思路

  [复制链接]
qtfreet00 发表于 2016-12-13 10:17
原题:
写了个小cm,欢迎来玩http://www.52pojie.cn/thread-558208-1-1.html
(出处: 吾爱破解论坛)

已经发了快一个月时间了,但并没有收到答案,看来大神并不屑于去做下啊

0x0 源码
       本CM制作出后就是开源出来的,哈哈,关注我的github的人肯定就注意到了,https://github.com/Qrilee/crackme,期间我还更新了几次,当然这就跟题目无关了,纯属技术积累吧

0x1 正篇
       本CM玩的点比较杂,反调试,dalvik代码自篡改(这就是我为什么让大家在5.0机器下玩喽),算法(AES,RC4),算法其实非常的简单,使用的都是标准的openssl算法库,可能难就难在我把算法从api中提取了出来,在编译优化完符号表之后大家看到的都是subxxxx的方法名,会对大家解题造成一定的困惑
       本题没有使用静态注册jni函数的形式,所以需要先去分析JNI_OnLoad,so没有经任何的处理,所以可以很容易使用IDA打开并f5
[C++] 纯文本查看 复制代码
__int16 *__fastcall JNI_OnLoad(int a1)
{
  int v1; // r4@1
  __time_t v2; // r3@1
  int v3; // r0@6
  int v4; // r0@6
  int v5; // r4@6
  int v6; // r1@6
  __int16 *result; // r0@8
  int v8; // [sp+4h] [bp-1Ch]@1
  struct timeval tv; // [sp+8h] [bp-18h]@1

  v1 = a1;
  v8 = 0;
  gettimeofday(&tv, 0);  //获取当前时间
  v2 = tv.tv_sec % 2 + 10;  //利用当前时间去计算出一个数
  if ( v2 == 10 )
  {
    dword_214AC = 81;
    dword_214B0 = 6;
    byte_214A8 = 0;               //给全局变量赋值,这个地方还没有看出来全局变量有什么用
  }
  else if ( v2 == 11 )
  {
    byte_214A8 = 1;
    dword_214AC = 80;
    dword_214B0 = 7;
  }
  free(&tv);
  if ( (*(int (__fastcall **)(int, int *, __int16 *))(*(_DWORD *)v1 + 24))(v1, &v8, &word_10006)
    || (v3 = sub_8F64(),  
        v4 = sub_8CF8(v3),   //这边有三个函数
        sub_8BA4(v4),
        v5 = v8,
        (v6 = (*(int (__fastcall **)(int, const char *))(*(_DWORD *)v8 + 24))(v8, "com/qtfreet/crackme001/MainActivity")) == 0)   //动态注册off_21458所在位置的函数
    || (*(int (__fastcall **)(int, int, char **, signed int))(*(_DWORD *)v5 + 860))(v5, v6, off_21458, 1) < 0 )  
  {
    result = (__int16 *)-1;
  }
  else
  {
    result = &word_10006;
  }
  return result;
}


sub_8F64()函数

[C++] 纯文本查看 复制代码
int sub_8F64()
{
  int v1; // [sp+0h] [bp-28h]@1
  char v2; // [sp+Ch] [bp-1Ch]@1
  char v3; // [sp+Dh] [bp-1Bh]@1

  v3 = 97;
  v2 = 48;
  sub_8ECE(&v1, &byte_21490, &v2);
  v2 = 49;
  v3 = 54;
  sub_8ECE(&v1, &byte_21490, &v2);
  v2 = 50;
  v3 = 102;
  sub_8ECE(&v1, &byte_21490, &v2);
  v2 = 51;
  v3 = 99;
  sub_8ECE(&v1, &byte_21490, &v2);
  v2 = 52;
  v3 = 57;
  sub_8ECE(&v1, &byte_21490, &v2);
  v2 = 53;
  v3 = 48;
  sub_8ECE(&v1, &byte_21490, &v2);
  v2 = 54;
  v3 = 49;
  sub_8ECE(&v1, &byte_21490, &v2);
  v2 = 55;
  v3 = 101;
  sub_8ECE(&v1, &byte_21490, &v2);
  v2 = 56;
  v3 = 50;
  sub_8ECE(&v1, &byte_21490, &v2);
  v2 = 57;
  v3 = 51;
  sub_8ECE(&v1, &byte_21490, &v2);
  v2 = 97;
  v3 = 52;
  sub_8ECE(&v1, &byte_21490, &v2);
  v2 = 98;
  v3 = 53;
  sub_8ECE(&v1, &byte_21490, &v2);
  v2 = 99;
  v3 = 55;
  sub_8ECE(&v1, &byte_21490, &v2);
  v2 = 100;
  v3 = 100;
  sub_8ECE(&v1, &byte_21490, &v2);
  v2 = 101;
  v3 = 56;
  sub_8ECE(&v1, &byte_21490, &v2);
  v2 = 102;
  v3 = 98;
  sub_8ECE(&v1, &byte_21490, &v2);
  v2 = 108;
  v3 = 116;
  sub_8ECE(&v1, &byte_21490, &v2);
  v2 = 105;
  v3 = 108;
  sub_8ECE(&v1, &byte_21490, &v2);
  v3 = 105;
  v2 = 113;
  sub_8ECE(&v1, &byte_21490, &v2);
  v2 = 116;
  v3 = 113;
  return sub_8ECE(&v1, &byte_21490, &v2);
}


直接对照源码看吧


[C++] 纯文本查看 复制代码
void initMap() {
    opcodemap.insert(std::make_pair(48, 97));//
    opcodemap.insert(std::make_pair(49, 54));//
    opcodemap.insert(std::make_pair(50, 102));//
    opcodemap.insert(std::make_pair(51, 99));//
    opcodemap.insert(std::make_pair(52, 57));//
    opcodemap.insert(std::make_pair(53, 48));//
    opcodemap.insert(std::make_pair(54, 49));//
    opcodemap.insert(std::make_pair(55, 101));//
    opcodemap.insert(std::make_pair(56, 50));//
    opcodemap.insert(std::make_pair(57, 51));//
    opcodemap.insert(std::make_pair(97, 52));//
    opcodemap.insert(std::make_pair(98, 53));//
    opcodemap.insert(std::make_pair(99, 55));//
    opcodemap.insert(std::make_pair(100, 100));//
    opcodemap.insert(std::make_pair(101, 56));//
    opcodemap.insert(std::make_pair(102, 98));//

    opcodemap.insert(std::make_pair(108, 116));//
    opcodemap.insert(std::make_pair(105, 108));//
    opcodemap.insert(std::make_pair(113, 105));//
    opcodemap.insert(std::make_pair(116, 113));//

}

这边就是初始化了一张表,类似于java里的hashmap,后面就会知道这张表的用途了,

sub_8CF8()函数,这个函数的功能就是反调试了
[C++] 纯文本查看 复制代码
int sub_8CF8()
{
  if ( pthread_create((pthread_t *)&dword_214B4, 0, (void *(*)(void *))sub_899E, 0) )
    exit(-1);
  return pthread_detach(dword_214B4);
}


跟进sub_899E看看,最终调用的方法在sub_E3E0()里
[C++] 纯文本查看 复制代码
__pid_t sub_E3E0()
{
  __int32 v0; // r4@1
  __pid_t result; // r0@3
  int v2; // r5@3
  FILE *v3; // r5@9
  int v4; // r7@10
  char s; // [sp+Ch] [bp-11Ch]@3
  char v6; // [sp+8Ch] [bp-9Ch]@6
  __int16 v7; // [sp+96h] [bp-92h]@10
  int v8; // [sp+10Ch] [bp-1Ch]@1

  v8 = _stack_chk_guard;
  v0 = syscall(20);
  if ( sub_E258() == 2 )
    kill(v0, 9);
  sprintf(&s, "/proc/%d/status", v0);
  result = fork();
  v2 = result;
  if ( !result )
  {
    if ( ptrace(0, 0, 0, 0) == -1 )
      exit(v2);
LABEL_9:
    v3 = fopen(&s, "r");
    do
    {
      if ( !fgets(&v6, 128, v3) )
      {
LABEL_8:
        sleep(0xAu);
        goto LABEL_9;
      }
    }
    while ( strncmp(&v6, "TracerPid", 9u) );
    v4 = atoi((const char *)&v7);
    fclose(v3);
    syscall(6, v3);
    if ( !v4 )
      goto LABEL_8;
    result = kill(v0, 9);
  }
  if ( v8 != _stack_chk_guard )
    _stack_chk_fail(result);
  return result;
}

可以看到这里就是比较简单的去获取TracerPid的值,并且该方法不带任何的返回值,所以在调试之前直接nop掉这个方法就行了

第三个函数sub_8BA4()就是难点之一了,因为里面的字符串我全部进行了加密,加密算法就是题目中用到的rc4,如果仔细去看的话还是比较恶心的
[C++] 纯文本查看 复制代码
  v0 = sub_82F4("Zvmmq56ICjFmg0doGjPySSQxMpk+mEJr+onBi14r6O1J1wKFCRT1IZVRccvJ9Sq8BVY=");
  s2 = (char *)sub_82F4((const char *)&unk_1E190);
  v1 = (const char *)sub_82F4("Bf6ostDWGjZ4kEt3GDPhSy8/cJhv3BIE1pjYpXwn8apOyhCOVw==");

算法就没必要分析了,key都是硬编码在so里的,关键点就是此处了
[C++] 纯文本查看 复制代码
 v13 = (const char *)sub_10574(v4, *(_DWORD *)(v12 + 4));
          if ( !strcmp(v13, s2) )
          {
            v14 = sub_5334(v4, v11);
            gettimeofday(&tv, 0);
            v15 = (void *)((unsigned int)(v14 + 16) >> 12 << 12);
            v22 = tv.tv_sec;
            if ( !mprotect(v15, 0x1000u, 3) )
            {
              v16 = (_BYTE *)(v14 + 64);
              v17 = (_BYTE *)(v14 + 72);
              if ( byte_214A8 )
              {
                v18 = dword_214B0;
                *v16 = dword_214AC;
              }
              else
              {
                v19 = dword_214B0;
                *v16 = dword_214AC - 1;
                v18 = v19 + 1;
              }
              *v17 = v18;
              mprotect(v15, 0x1000u, 1);
              gettimeofday(&tv, 0);
              sub_E3C8(v22, tv.tv_sec);
            }
          }
          v9 = v20 + 1;
        }
        free((void *)v10);
      }


看到mprotect可能就需要注意是否有自篡改了,此处就用上了之前初始的三个全局变量,并且此处是有反调试的,反调试点就是壳常用的gettimeofday去判断单步调试的时间差,对照源码看
[C++] 纯文本查看 复制代码
 if (!strcmp(dexStringById(pDexFile, pMethodId->nameIdx), findMethod)) {
                        const DexCode *pCode = dexGetCode(pDexFile, pMethod);
                        struct timeval tv;
                        gettimeofday(&tv, NULL);
                        int resTime = tv.tv_sec;
                        if (mprotect(PAGE_START((int) (pCode->insns)), PAGE_SIZE,
                                     PROT_READ | PROT_WRITE) == 0) {
                            if (flag) {
                                *((u1 *) (pCode->insns) + 48) = (u1) opCodeReverse;
                                *((u1 *) (pCode->insns) + 56) = (u1) opCodeToString;
                            } else {
                                *((u1 *) (pCode->insns) + 48) = (u1) (opCodeReverse - 1);
                                *((u1 *) (pCode->insns) + 56) = (u1) (opCodeToString + 1);
                            }

                            mprotect(PAGE_START((int) (pCode->insns)), PAGE_SIZE, PROT_READ) == 0;
                            gettimeofday(&tv, NULL);
                            int destTime = tv.tv_sec;
                            CalcTime(resTime, destTime);

                        }
                        pCode->insns;
                    }
                }
                free(pClassData);

此处其实篡改的代码就是java层里的如下这个函数
[Java] 纯文本查看 复制代码
  public void checkCode() {
        String s = this.ed.getText().toString().trim();
        StringBuilder sb = new StringBuilder();
        sb.append(s);
        if (check(sb.toString().toLowerCase().trim())) {
            Toast.makeText(this, "Congratulations", 0).show();
        } else {
            Toast.makeText(this, "U are wrong~", 0).show();
        }
    }

在sb.toString()处改为了reverse(),将StringBuilder里的字符串全部进行了倒序,这个点其实也很好过,在动态调试时多看几次就知道名堂了,所以自篡改这个点也是比较容易去分析的,

接下来就是关键函数了,为了分析方便我们导入jni.h头文件
[C++] 纯文本查看 复制代码
 v28 = _stack_chk_guard;
  s = (char *)a1->functions->GetStringUTFChars(&a1->functions, a3, 0);
  memset(&v27, 0, 0x400u);
  sub_84FC(&v27);
  v3 = sub_9198(&v27);
  dest = (char *)&v25;
  memset(&v25, 0, 0x10u);
  v4 = (const char *)(v3 + 3);
  v5 = 0;
  strncpy((char *)&v25, v4, 0x10u);
  v26 = 0;
  v6 = strlen(s);
  v23 = (int *)&(&v21)[-2 * ((unsigned int)(v6 + 7) >> 3)];
  memset(&v21, 0, v6);
  v21 = &_stack_chk_guard;
  while ( v5 < v6 )
  {
    v7 = &byte_21490;
    v8 = (unsigned __int8)s[v5];
    for ( i = dword_21494; i; i = v10 )
    {
      if ( *(_BYTE *)(i + 16) < v8 )
      {
        v10 = *(_DWORD *)(i + 12);
        i = (int)v7;
      }
      else
      {
        v10 = *(_DWORD *)(i + 8);
      }
      v7 = (char *)i;
    }
    if ( v7 != &byte_21490 && (unsigned __int8)v7[16] > v8 )
      v7 = &byte_21490;
    *((_BYTE *)v23 + v5++) = v7[17];
  }
  v11 = v23;
  v12 = dest;
  *((_BYTE *)v23 + v6) = 0;
  v13 = (const char *)sub_8A18(v11, v12);
  v14 = v13;
  v15 = strlen(v13);
  v16 = sub_EAE4(v14, v15);
  v17 = (const char *)sub_835C(v16);
  v18 = v17;
  v19 = strlen(v17);
  result = (unsigned int)memcmp("KO+257fsDx9esUUzWD7Uc39tRa84ix4W", v18, v19) <= 0;
  if ( v28 != *v21 )
    _stack_chk_fail(result);
  return result;


既然传入的参数赋值给了s,我们就留意下s这个参数是怎么被使用的就行了,sub_84FC函数点进去发现这个就是获取当前的包名的,然后将获取到包名再去调用sub_9198函数就进行加密,这个是个md5,因为包名固定,所以md5也是固定的,这个动态一看就知道了

这个md5的作用是给aes加密当作密钥的,当然了我不会这么轻易直接用这个原md5去干,肯定要做些手脚,
[C++] 纯文本查看 复制代码
  v4 = v3 + 3;
  v5 = 0;
  strncpy((char *)&v25, v4, 0x10u);

这个地方可以看到从第四位开始取数值,总共取16位
[C++] 纯文本查看 复制代码
v6 = strlen(s);
  v23 = (int *)&(&v21)[-2 * ((unsigned int)(v6 + 7) >> 3)];
  memset(&v21, 0, v6);
  v21 = &_stack_chk_guard;
  while ( v5 < v6 )
  {
    v7 = &byte_21490;
    v8 = (unsigned __int8)s[v5];
    for ( i = dword_21494; i; i = v10 )
    {
      if ( *(_BYTE *)(i + 16) < v8 )
      {
        v10 = *(_DWORD *)(i + 12);
        i = (int)v7;
      }
      else
      {
        v10 = *(_DWORD *)(i + 8);
      }
      v7 = (char *)i;
    }
    if ( v7 != &byte_21490 && (unsigned __int8)v7[16] > v8 )
      v7 = &byte_21490;
    *((_BYTE *)v23 + v5++) = v7[17];
  }
  v11 = v23;

直接对照源码看
[C++] 纯文本查看 复制代码
   int size = strlen(flag);
    unsigned char content[size];
    memset(content, 0, size);
    for (int i = 0; i < size; i++) {
        int temp = (int) flag[i];
        content[i] = opcodemap.find(temp)->second;

    }
    content[size] = 0;

这个地方就是进行查表操作,将输入进来的字符串的每一位在表中进行查找并替换,后面就没啥好分析的了,使用的就是两个标准算法,在进行两次加密后与固定字符串进行对比
[C++] 纯文本查看 复制代码
const char final_flag[] = {
        75, 79, 43, 50, 53, 55, 102, 115, 68, 120, 57, 101, 115, 85, 85, 122,
        87, 68, 55, 85, 99, 51, 57, 116, 82, 97, 56, 52, 105, 120, 52, 87,
        0, 99, 111, 109, 47, 113, 116, 102, 114, 101, 101, 116, 47, 99, 114, 97,
};

因为算法都是可逆的,并且key都已知,就不分析了

0x2 踩坑
1.在编译器优化掉符号表之后给大家解题会造成一定的困惑
2.算法,不常接触算法的话可能在看到算法时会比较头疼
3.置换表,在编译好的so里直接去看map函数显得很乱
4.反调试,该cm中使用了三种反调试检测手段,TracerPid,gettimeofday,/proc/%d/wchan,不过都比较明显,还是好处理的
5.自篡改,这个坑不大,多试几次就知道了

0x3 答案
fla3i5qtf1eet00



免费评分

参与人数 11威望 +1 吾爱币 +1 热心值 +11 收起 理由
zhuzaiting + 1 + 1 谢谢@Thanks!
siuhoapdou + 1 谢谢@Thanks!
610100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
Peace + 1 用心讨论,共获提升!
清风一缕 + 1 热心回复!
huangn2008 + 1 热心回复!
本物天下霸唱 + 1 you为何这么吊
XhyEax + 1 热心回复!
Hmily + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
wangsheng66 + 1 热心回复!
4everlove + 1 用心讨论,共获提升!

查看全部评分

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

 楼主| qtfreet00 发表于 2016-12-13 19:28
z529943198 发表于 2016-12-13 18:23
想问下(*v5)->RegisterNatives(v5, v6, (const JNINativeMethod *)off_21458, 1) < 0 )   这个off_21458函 ...

这只是个结构体,需要点进去之后找到对应函数的偏移地址才可以f5的
z529943198 发表于 2016-12-13 18:23
本帖最后由 z529943198 于 2016-12-13 18:39 编辑

想问下(*v5)->RegisterNatives(v5, v6, (const JNINativeMethod *)off_21458, 1) < 0 )   这个off_21458函数不能F5啊 才发现在IDA 6.5下这个函数不能F5
4everlove 发表于 2016-12-13 10:33
wangsheng66 发表于 2016-12-13 10:39
因为功力达不到,无法分析。
keeslient 发表于 2016-12-13 15:37

因为功力达不到,无法分析。
KaQqi 发表于 2016-12-13 20:47
膜拜安卓逆向牛
Loopher 发表于 2016-12-14 09:41
厉害啊。算法真的头痛-_-||,大牛指点下怎么研究算法。感谢啊~~
夏侯爷 发表于 2016-12-14 11:26
因为功力达不到,无法分析。
okmummum 发表于 2016-12-14 23:41
额1你够破坏米西米西
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

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

GMT+8, 2024-4-26 04:42

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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