首页
论坛
课程
招聘
[原创]看雪.京东 2018CTF 第五题 APK-ExecuteTable ------ WriteUp
2018-6-26 11:20 2814

[原创]看雪.京东 2018CTF 第五题 APK-ExecuteTable ------ WriteUp

2018-6-26 11:20
2814

拿到apk直接丢Jeb查看,可以看到java层代码注册了几个native函数,验证也是交给native函数进行的,所以转移关注点到apk加载的so上。

 

解压apk文件,只有一个libexecute_table.so,先用readelf查看动态段

Dynamic section at offset 0x34be0 contains 28 entries:
  Tag        Type                         Name/Value
 0x00000003 (PLTGOT)                     0x35f00
 0x00000002 (PLTRELSZ)                   488 (bytes)
 0x00000017 (JMPREL)                     0x5d00
 0x00000014 (PLTREL)                     REL
 0x00000011 (REL)                        0x3a18
 0x00000012 (RELSZ)                      8936 (bytes)
 0x00000013 (RELENT)                     8 (bytes)
 0x6ffffffa (RELCOUNT)                   1109
 0x00000006 (SYMTAB)                     0x168
 0x0000000b (SYMENT)                     16 (bytes)
 0x00000005 (STRTAB)                     0x13c8
 0x0000000a (STRSZ)                      7571 (bytes)
 0x00000004 (HASH)                       0x315c
 0x00000001 (NEEDED)                     Shared library: [liblog.so]
 0x00000001 (NEEDED)                     Shared library: [libstdc++.so]
 0x00000001 (NEEDED)                     Shared library: [libm.so]
 0x00000001 (NEEDED)                     Shared library: [libc.so]
 0x00000001 (NEEDED)                     Shared library: [libdl.so]
 0x0000000e (SONAME)                     Library soname: [libexecute_table.so]
 0x0000000c (INIT)                       0x8c59
 0x0000001a (FINI_ARRAY)                 0x34cc8
 0x0000001c (FINI_ARRAYSZ)               8 (bytes)
 0x00000019 (INIT_ARRAY)                 0x34cd0
 0x0000001b (INIT_ARRAYSZ)               60 (bytes)
 0x00000010 (SYMBOLIC)                   0x0
 0x0000001e (FLAGS)                      SYMBOLIC BIND_NOW
 0x6ffffffb (FLAGS_1)                    Flags: NOW
 0x00000000 (NULL)                       0x0

可以看到INIT_ARRAY0x34cd0处,直接丢到IDA查看,可以看到相关的初始化函数列表,为了方便观察交叉引用,我给每个函数重命了名。

LOAD:00034CD0 ; ELF Initialization Function Table
LOAD:00034CD0                 DCD proc_1+1
LOAD:00034CD4                 DCD proc_2+1
LOAD:00034CD8                 DCD proc_3+1
LOAD:00034CDC                 DCD proc_4+1
LOAD:00034CE0                 DCD proc_5+1
LOAD:00034CE4                 DCD proc_6+1
LOAD:00034CE8                 DCD proc_7+1
LOAD:00034CEC                 DCD proc_8+1
LOAD:00034CF0                 DCD proc_9+1
LOAD:00034CF4                 DCD proc_10+1
LOAD:00034CF8                 DCD proc_11+1
LOAD:00034CFC                 DCD proc_12+1
LOAD:00034D00                 DCD proc_13+1
LOAD:00034D04                 DCD proc_14+1
LOAD:00034D08                 ALIGN 0x10
LOAD:00034D08 ; LOAD          ends

初看觉得每个函数都很复杂,只注意到了proc_4开头处new了一段空间并且存入了几个关键函数

//proc_4
  v8 = (struc_table *)operator new(0x20u);
  sub_C8A0(v8);

//sub_C8A0
struc_table *__fastcall sub_C8A0(struc_table *result)
{
  result->dlopen = (int)off_35D08;
  result->sscanf = (int)off_35D0C;
  result->__NR_read = (int)off_35D10[0];
  result->__NR_close = (int)off_35D14[0];
  result->__NR_mprotect = (int)off_35D18;
  result->__NR_openat = (int)sub_C874;
  result->sleep = (int)off_35D1C;
  result->pthread_create = (int)off_35D20;
  return result;
}

接下来就动态调试了,修改proc_1开头两字节为FE E7,使so在此处死循环,然后IDA附加之后再修改回原指令,别的先不管,对上面的几个关键函数下断点,特别关注pthread_createmprotect__NR_openat

 

分析后发现__NR_openat打开过/proc/self/maps/proc/self/status, 而mprotect有修改JNI_OnLoad偏移与代码解密操作

 

推测打开/proc/self/maps是为了拿到基址,/proc/self/status/是反调试, 后面实际解key时感觉应该是检测到调试,就把解密中需要用到的一个全局变量修改掉。导致得到错误的结果。

 

mprotect调用了两次,第一次修改JNI_OnLoad的偏移,由0x8205修改为0xA260 在这个函数中可以看到JNIEnvFindClassRegisterNatives,所以可以拿到验证keynative函数地址0xAC98

v14 = ((int (__fastcall *)(JNIEnv *, int))(*v20)->FindClass)(v20, v40);
    sub_19058(&v40);
    sub_19058(&v39);
    sub_19058(&v38);
    sub_19058(&v37);
    if ( !v14 || ((int (*)(void))(*v29)->RegisterNatives)() )
    {
      v15 = -1;
    }

直接去到0xAC98发现代码无法解析,应该是加密了。

 

所以第二次mprotect调用就是解密0xAC98中被加密的代码。

//sub_833C

 v9 = dword_36090;  //全局变量,保存0xAC98的实际地址
  if ( *(unsigned __int8 *)dword_36090 == 0x2E - v1
    && *(unsigned __int8 *)(dword_36090 + 1) == 0xEA - v1
    && (char *)*(unsigned __int8 *)(dword_36090 + 2) == v2 + 0xF0
    && *(_BYTE *)(dword_36090 + 3) == 0x4F )     //if判断函数开头4字节是否为2D E9 F0 4F
  {
    v3 = v10;
    v11 = dword_36090 & 0xFFFFF000;
    v13 = v3 + 101;
    v4 = 0;
    ((void (__fastcall *)(unsigned int, signed int, signed int))v12->__NR_mprotect)(dword_36090 & 0xFFFFF000, 0x1000, 7); //修改段属性
    while ( 1 )
    {
      sub_19850(&v14, (int)"A782E192B81NICAIsan38Qz", (int)&v20);
      sub_E24C((int)&v17, 24);
      sub_18504((int)&v18, 32123);
      sub_DD44(&v15, &v19);
      v5 = (_BYTE *)(dword_7D38 + 16);
      if ( !std::operator==<char>(&v14, &v15) )
        v5 = (_BYTE *)(&loc_7D7A + 1);
      if ( std::operator!=<char,std::char_traits<char>,std::allocator<char>>(&v15, &v14) )
      {
        sub_E24C((int)&v20, 24);
        sub_18504((int)&v21, 33687);
        sub_DD44(&v16, &v22);
        v5 = (char *)std::operator==<char,std::char_traits<char>,std::allocator<char>>(&v16, "0d87a");
        sub_19058(&v16);
        sub_DBAC(&v20);
        if ( v5 )
          v5 = (char *)off_35CFC + 303183;
      }
      sub_19058(&v15);
      sub_DBAC(&v17);
      sub_19058(&v14);
      //解密代码,用前一个字节xor后一个字节来解密,第一个字节与0x66异或,总长度400,起始为0xAC98+150
      v6 = (int)&v5[v4++ + 150];
      v7 = *(_BYTE *)(v9 + v6);
      *(_BYTE *)(v9 + v6) = v7 ^ v13;
      if ( v4 == 400 )
        break;
      v13 = v7;
    }
    ((void (__fastcall *)(unsigned int, signed int, signed int))v12->__NR_mprotect)(v11, 4096, 5);  //还原段属性
  }
  return 1;

接下来调试验证函数的代码。直接在0xAC98下断,然后随便输入一个key点注册即可断下。
经过调试分析后总结的验证流程如下:

  1. 把输入的串第一位放到最后
  2. 根据串的ASCII码值进行查表,表如下
     0x40, 0x50, 0x78, 0x7A, 0x29, 0x88, 0xF7, 6, 0x21, 9, 0xF3, 0x5C, 0x95, 0xAE, 0x66, 0x12
     0x8F, 0x85, 0xC8, 0x5A, 0xBF, 0x33, 0x3D, 0x86, 0x90, 0x8C, 0xED, 0xD5, 0x8B, 0xA4, 0xC5, 0xC7
     0xEA, 0xF6, 0x79, 0x1E, 0x3C, 0xBA, 0x97, 0x4E, 0x38, 0x60, 8, 0xDD, 0xFA, 0xB3, 0xDE, 0x77
     0x81, 0x41, 0x19, 0xF4, 0x52, 0x6B, 0xFF, 0xD8, 0x2A, 0xC2, 0xBC, 0xB9, 0xE7, 0x91, 0xE9, 0x54
     0x82, 0xAD, 0x7E, 0x11, 0x35, 0x93, 0xB0, 0xA1, 0x18, 0xC4, 0x53, 0xA, 0x74, 0x2F, 0xE2, 0x17
     0x98, 0xC, 0x70, 0x92, 0x47, 0x64, 0x16, 0xFE, 0x75, 0x83, 0x37, 0x8D, 7, 0x72, 0x25, 4
     0xB7, 0xC9, 0xCE, 0xE, 0x9E, 0xEB, 0xCF, 0xB1, 0xDB, 0x71, 0x56, 0xAF, 0x39, 0xF0, 0xBB, 0xBD
     0x46, 0x32, 0xE6, 0x9F, 0x4F, 0x1B, 0x4D, 0x68, 0xF2, 0x4B, 0x2E, 0xCB, 0x20, 0xD2, 0xB, 0xA5
     0xEE, 0xE1, 0xA9, 0x2B, 0x84, 0x14, 0x67, 0x63, 0x6F, 0x3E, 0x7F, 0xFD, 0xB6, 0xFC, 0x55, 0x7C
     0x5F, 0xF8, 0x4C, 0x65, 0x2C, 0x30, 0xEF, 0x48, 0xD7, 0xD, 0xF, 0x1A, 0x5E, 0xC0, 0x3A, 0x57
     0x6A, 0x31, 0, 0xF1, 0x59, 0x10, 0xB8, 0x9A, 0x43, 0x73, 0xA3, 0x6E, 0x26, 0x1D, 0x13, 0x15
     0x89, 0x5D, 0xDA, 0x61, 0xD1, 0x6C, 0xD3, 0xE0, 0xD9, 0x1F, 0xD4, 0x49, 0xEC, 0xE3, 0xD0, 0x34
     0x36, 0xC6, 0x24, 0xE4, 0xF5, 0xAA, 0x9B, 0xB2, 0x4A, 0xDF, 0xAC, 0x96, 0xDC, 0xE8, 0xA0, 0xF9
     0xC1, 0x9C, 0xCA, 0x9D, 0x27, 0xC3, 0xBE, 0x87, 0x28, 0xCC, 0x99, 0xE5, 0x45, 0x58, 0x94, 0x23
     0x22, 0xFB, 2, 1, 3, 0x8A, 0x7B, 0xB5, 0x1C, 0xA7, 0x44, 0xCD, 0xA2, 0x51, 0x8E, 0x3F
     0x42, 0xD6, 0x69, 0xAB, 0x62, 0x3B, 0x7D, 0xA6, 5, 0x2D, 0xA8, 0x80, 0x6D, 0xB4, 0x76, 0x5B
    
  3. 把结果中的字节按高低位翻转
  4. 翻转后的结果与全局变量dword_36098中的每个字节异或,这里应该是有检测,如果检测到调试,这个值会被修改。正确的值为0x83 0xae 0x33 0x23 可使用fridaMemory.readByteArray 通过基址+偏移来读取到这个值
  5. 异或后的结果每两个字节翻转
  6. 再次查表
  7. 查表结果分别高字节与低字节作为索引,在串A3Cw6Gb0OZWPU52s中搜索,得到两个串,高位串称作串1,低位为串2
    例如: 0x81 0x23 分别用8 2 和 1 3在串A3Cw6Gb0OZWPU52s中搜索,得到串1OC与串23w
  8. 把串1的首字节放到最后,把串2的首字节放到最后并翻转整个串,然后将串1与串2拼接,并与3ww3U53wOAWG333wwPZ56GGw0PO02OUW进行比较,相同则验证通过。

所以只用根据上面的计算过程,反向计算3ww3U53wOAWG333wwPZ56GGw0PO02OUW的原始输入即可。

 

写了个按照每一步操作反向计算的脚本,输入最终串直接打印出key

def calc1(input):
    l = input[:-1]
    l.insert(0, input[-1])
    r = [chr(i) for i in l]
    print ('[+] calc1: ', r)

def calc2(table, input):
    r1 = []
    for i in input:
        for j in range(0, len(table)):
            if i == table[j]:
                r1.append(j)
    print ('[+] calc2: ', r1)
    return r1

def calc3(input):
    r1 = []
    for i in input:
        r1.append(((i >> 4) & 0xF) + ((i & 0xF) << 4))
    print ('[+] calc3: ', r1)
    return r1

def calc4(table, input):
    r1 = []
    n = 0
    for i in input:
        if n == 4:
            n = 0
        r1.append(i ^ table[n])
        n += 1
    print ('[+] calc4: ', r1)
    return r1


def calc5(input):
    for i in range(0, len(input), 2):
        if i % 2 == 0:
            tmp = input[i+1]
            input[i+1] = input[i]
            input[i] = tmp
    print ('[+] calc5: ', input)
    return input

def calc6(table, input):
    r1 = []
    for i in input:
        for j in range(0, len(table)):
            if i == table[j]:
                r1.append(j)
    print ('[+] calc6: ', r1)
    return r1


def calc7(table, s1, s2):
    r1 = []
    for i in s1:
        for j in range(0, len(table)):
            if table[j] == i:
                r1.append(j << 4)

    n = 0
    for i in s2:
        for j in range(0, len(table)):
            if table[j] == i:
                r1[n] = r1[n] + j
                n += 1
    print ('[+] calc7: ', r1)
    return r1



def calc8(input):
    s1 = input[:15]
    s1 = input[16] + s1
    tmp = input[:15:-1]
    s2 = input[16] + tmp[:15]

    print ('[+] calc8:', s1, s2)
    return s1, s2

if __name__ == '__main__':
    mainTable = [0x40, 0x50, 0x78, 0x7A, 0x29, 0x88, 0xF7, 0x06, 0x21, 0x09, 0xF3, 0x5C, 0x95, 0xAE, 0x66, 0x12,
0x8F, 0x85, 0xC8, 0x5A, 0xBF, 0x33, 0x3D, 0x86, 0x90, 0x8C, 0xED, 0xD5, 0x8B, 0xA4, 0xC5, 0xC7,
0xEA, 0xF6, 0x79, 0x1E, 0x3C, 0xBA, 0x97, 0x4E, 0x38, 0x60, 0x08, 0xDD, 0xFA, 0xB3, 0xDE, 0x77,
0x81, 0x41, 0x19, 0xF4, 0x52, 0x6B, 0xFF, 0xD8, 0x2A, 0xC2, 0xBC, 0xB9, 0xE7, 0x91, 0xE9, 0x54,
0x82, 0xAD, 0x7E, 0x11, 0x35, 0x93, 0xB0, 0xA1, 0x18, 0xC4, 0x53, 0x0A, 0x74, 0x2F, 0xE2, 0x17,
0x98, 0x0C, 0x70, 0x92, 0x47, 0x64, 0x16, 0xFE, 0x75, 0x83, 0x37, 0x8D, 0x07, 0x72, 0x25, 0x04,
0xB7, 0xC9, 0xCE, 0x0E, 0x9E, 0xEB, 0xCF, 0xB1, 0xDB, 0x71, 0x56, 0xAF, 0x39, 0xF0, 0xBB, 0xBD,
0x46, 0x32, 0xE6, 0x9F, 0x4F, 0x1B, 0x4D, 0x68, 0xF2, 0x4B, 0x2E, 0xCB, 0x20, 0xD2, 0x0B, 0xA5,
0xEE, 0xE1, 0xA9, 0x2B, 0x84, 0x14, 0x67, 0x63, 0x6F, 0x3E, 0x7F, 0xFD, 0xB6, 0xFC, 0x55, 0x7C,
0x5F, 0xF8, 0x4C, 0x65, 0x2C, 0x30, 0xEF, 0x48, 0xD7, 0x0D, 0x0F, 0x1A, 0x5E, 0xC0, 0x3A, 0x57,
0x6A, 0x31, 0x00, 0xF1, 0x59, 0x10, 0xB8, 0x9A, 0x43, 0x73, 0xA3, 0x6E, 0x26, 0x1D, 0x13, 0x15,
0x89, 0x5D, 0xDA, 0x61, 0xD1, 0x6C, 0xD3, 0xE0, 0xD9, 0x1F, 0xD4, 0x49, 0xEC, 0xE3, 0xD0, 0x34,
0x36, 0xC6, 0x24, 0xE4, 0xF5, 0xAA, 0x9B, 0xB2, 0x4A, 0xDF, 0xAC, 0x96, 0xDC, 0xE8, 0xA0, 0xF9,
0xC1, 0x9C, 0xCA, 0x9D, 0x27, 0xC3, 0xBE, 0x87, 0x28, 0xCC, 0x99, 0xE5, 0x45, 0x58, 0x94, 0x23,
0x22, 0xFB, 0x02, 0x01, 0x03, 0x8A, 0x7B, 0xB5, 0x1C, 0xA7, 0x44, 0xCD, 0xA2, 0x51, 0x8E, 0x3F,
0x42, 0xD6, 0x69, 0xAB, 0x62, 0x3B, 0x7D, 0xA6, 0x05, 0x2D, 0xA8, 0x80, 0x6D, 0xB4, 0x76, 0x5B]

    table = ['A', '3', 'C', 'w', '6', 'G', 'b', '0', 'O', 'Z', 'W', 'P', 'U', '5', '2', 's']
    txor = [0x83, 0xAE, 0x33, 0x23]

    s1, s2 = calc8('3ww3U53wOAWG333wwPZ56GGw0PO02OUW')
    key1 = calc7(table, s1, s2)
    key2 = calc6(mainTable, key1)
    key3 = calc5(key2)
    key4 = calc4(txor, key3)
    key5 = calc3(key4)
    key6 = calc2(mainTable, key5)
    calc1(key6)

最终key为:C0ngRa7U1AtIoN2U


[公告] 2021 KCTF 春季赛 防守方征题火热进行中!

收藏
点赞0
打赏
分享
最新回复 (2)
雪    币: 85
活跃值: 活跃值 (22)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
RorschachL 活跃值 2018-6-26 14:18
2
0
雪    币: 6982
活跃值: 活跃值 (217)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
点中你的心 活跃值 2018-6-27 21:04
3
0
厉害了,,,
游客
登录 | 注册 方可回帖
返回