8

[原创]CTF2017 第六题 Ericky-apk 解题报告

kkHAIKE 2017-6-12 23:24 1379

CTF2017 第六题 Ericky-apk

初步分析

  1. 拿到 apk 直接丢到 jeb,发现 java 部分直接调用了 so 提供的 check 函数
  2. ida 打开 so,发现花指令:成对指令+跳转,多跟了几个后发现形式是固定的(不是随机产生)
  3. 这是开始手动过花指令的思路,原谅我放这么大的图


图1

脱花指令

手动过了十几个后,发现的确有挺多,还是写个 IDA 脚本过吧

import idaapi
import idautils
import keystone
text_seg = idaapi.get_segm_by_name('.text')
next_addr = text_seg.startEA
asm = Ks(KS_ARCH_ARM, KS_MODE_THUMB)
while True:
    # 找 PUSH.W {R4-R10,LR}
    #    POP.W  {R4-R10,LR}
    curr = idaapi.find_binary(next_addr, text_seg.endEA,
                "2D E9 F0 47 BD E8 F0 47", 0, SEARCH_DOWN)
    if curr == idaapi.BADADDR:
        break
    b1 = idautils.DecodeInstruction(curr + 8)
        # 下一条是 B跳转
    if (b1.get_canon_mnem() == "B" and
        # 花指令块最后一定是 SUB.W R1, R1, #1
        idaapi.get_many_bytes(curr + 0x62, 4).encode("hex") == "a1f10101"):
        # 用 ks 汇编跳转指令,ida 内置的不支持该平台
        op, cnt = asm.asm("B " + hex(curr + 0x66), curr)
        assert cnt == 1
        idaapi.patch_byte(curr, op[0])
        idaapi.patch_byte(curr + 1, op[1])
        # 中间 NOP 调
        idaapi.patch_many_bytes(curr + 2, "\x00\xbf" * 50)
        next_addr = curr + 0x66
    else:
        next_addr = curr + 8

后续修复

  1. check 开头有个无限循环要 NOP,要不 IDA 分析不出


图2

  1. 有堆栈保护的函数找最近 BLX __stack_chk_fail 设置为函数尾
  2. 没有堆栈保护的函数找最近的 POP {xxx-xxx, PC} (xxx-xxx 和函数开头对应,堆栈平衡) 设置为函数尾

分析代码

其实可以不导出 check 函数的,在 JNI_OnLoad 内就一个 RegisterNatives 少了很多看头

signed int __fastcall check(JNIEnv *env, int a2, int src)
{
  JNIEnv *env_; // r9@1
  void *src_; // r8@1
  signed int v5; // r4@3
  int v6; // r1@3
  int v7; // r5@4
  int v8; // r6@4
  __int16 *v9; // r4@5
  int v10; // r3@5
  __int16 v11; // r0@6
  int i; // r4@12
  const char *src_str; // r0@12
  _BYTE *data; // r1@12
  signed int result; // r0@15
  __int16 v16[32]; // [sp+0h] [bp-54h]@1
  int v17; // [sp+44h] [bp-10h]@1
  env_ = env;
  src_ = (void *)src;
  v17 = _stack_chk_guard;
  *(_DWORD *)v16 = 0x11000F;
  *(_DWORD *)&v16[2] = 0x11FFFF;
  *(_DWORD *)&v16[4] = 0xFFFFFFFF;
  *(_DWORD *)&v16[6] = 0x10010;
  *(_DWORD *)&v16[8] = 0x10FFFF;
  *(_DWORD *)&v16[10] = 0xFFFF0003;
  *(_DWORD *)&v16[12] = 0xF000F;
  *(_DWORD *)&v16[14] = 0;
  *(_DWORD *)&v16[16] = 0xFFFFF;
  *(_DWORD *)&v16[18] = -0xFFEFu;
  *(_DWORD *)&v16[20] = -0xFFEFu;
  *(_DWORD *)&v16[22] = 0x10FFFF;
  *(_DWORD *)&v16[24] = 0xFFFF0001;
  *(_DWORD *)&v16[26] = 0x30010;
  *(_DWORD *)&v16[28] = 0x21FFFF;
  *(_DWORD *)&v16[30] = 0xA;
  if ( try_count >= 6 )
  {
    while ( 1 )
      ;
  }
  v5 = 0;
  ++try_count;
  v16[17] = 'J';                                // 其实可能加密的时候参数设置错误才导致密文是明文的
                                                // 就是 v16[15] == 0 导致的
  v16[18] = 'y';
  v16[19] = 'u';
  v16[20] = '3';
  v16[21] = 'C';
  v6 = 0;
  v16[22] = 'J';
  v16[23] = 'l';
  v16[24] = 'V';
  v16[25] = 'D';
  v16[26] = 'S';
  v16[27] = 'G';
  v16[28] = 'Q';
  do
  {
    v7 = v16[v5];
    v8 = v5 + 3;
    if ( v7 == -1 )
      goto LABEL_10;
    v9 = &v16[v5];
    v10 = v9[1];
    if ( (unsigned __int16)v10 == 0xFFFF )
    {
      dest_buf[2 * v6++] = v16[v7];
LABEL_10:
      v5 = v8;
      continue;
    }
    v5 = v9[2];
    v11 = v16[v10] - v16[v7];
    v16[v10] = v11;
    if ( v11 > 0 )
      goto LABEL_10;
  }
  while ( v5 > -1 );                            // 前面解码后 dest_buf = J_y_u_3_C_J_l_V_D_S_G_Q_
  dest_part2();                                 // 这里面和上面的算法一样
                                                // 最后 dest_buf = JPyjup3eCyJjlkV6DmSmGHQ=
  i = 0;
  src_str = (*env_)->GetStringUTFChars(env_, src_, 0);
  data = string_encode(src_str);
  while ( dest_buf[i] == data[i] )
  {
    if ( ++i == 24 )
    {
      result = 1;
      goto LABEL_17;
    }
  }
  sub_27C8(dest_buf, data);                     // 干扰的比较
  result = 0;
LABEL_17:
  if ( _stack_chk_guard != v17 )
    _stack_chk_fail(result, _stack_chk_guard - v17);
  return result;
}
_BYTE *__cdecl string_encode(_BYTE *src)
{
  _BYTE *src_; // r9@1
  _BYTE *key; // r8@1
  _BYTE *v3; // r0@1
  int v4; // t1@2
  unsigned int srclen; // r6@3
  _BYTE *dst; // r5@3
  _BYTE *dst_; // r0@3
  int v8; // t1@4
  _BYTE *result; // r0@5
  int rc4; // [sp+0h] [bp-418h]@3
  int v11; // [sp+408h] [bp-10h]@1
  src_ = src;
  v11 = _stack_chk_guard;
  key = get_key();                              // 还是之前的算法返回 199310124853,但只要前面 8 个字节
  v3 = src_;
  do
    v4 = *v3++;
  while ( v4 );
  srclen = (unsigned int)&v3[~(unsigned int)src_];
  dst = malloc((size_t)&v3[~(unsigned int)src_ + 1]);
  _aeabi_memclr();
  rc4_init(&rc4, 8, key);                       // 这个点进去看到 256 就知道是 rc4 了(考试要考)
  rc4_crypt(&rc4, srclen, src_, dst);
  dst_ = dst;
  do
    v8 = *dst_++;
  while ( v8 );
  result = base64_encode((int)dst, (int)&dst_[-(signed int)dst - 1]);
  if ( _stack_chk_guard != v11 )
    _stack_chk_fail(result, _stack_chk_guard - v11);
  return result;
}

结果

从代码看出,整个流程就是将输入的字符串 rc4 加密后 base64,与内置的结果对比,python 一句出结果

ARC4.new("19931012").decrypt(base64.b64decode("JPyjup3eCyJjlkV6DmSmGHQ="))




快讯:[看雪招聘]十八年来,看雪平台输出了大量安全人才,影响三代安全人才!

最新回复 (4)
kkHAIKE 2017-6-14 16:29
2
我怎么感觉有去花指令的干货能加精。。
gttiankai 2017-6-15 13:10
3
能详细讲一下,去花之后的指令修复吗?
kiyaa 2017-7-3 23:02
4

膜拜大佬

kiyaa 2017-7-3 23:02
5

膜拜大佬 

返回