拿到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_ARRAY
在0x34cd0
处,直接丢到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_create
,mprotect
与__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
在这个函数中可以看到JNIEnv
的FindClass
与RegisterNatives
,所以可以拿到验证key
的native
函数地址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
点注册即可断下。
经过调试分析后总结的验证流程如下:
- 把输入的串第一位放到最后
- 根据串的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
- 把结果中的字节按高低位翻转
- 翻转后的结果与全局变量
dword_36098
中的每个字节异或,这里应该是有检测,如果检测到调试,这个值会被修改。正确的值为0x83 0xae 0x33 0x23
可使用frida
的Memory.readByteArray
通过基址+偏移来读取到这个值
- 异或后的结果每两个字节翻转
- 再次查表
- 查表结果分别高字节与低字节作为索引,在串
A3Cw6Gb0OZWPU52s
中搜索,得到两个串,高位串称作串1,低位为串2
例如: 0x81 0x23 分别用8 2 和 1 3在串A3Cw6Gb0OZWPU52s
中搜索,得到串1OC
与串23w
- 把串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
阿里云助力开发者!2核2G 3M带宽不限流量!6.18限时价,开
发者可享99元/年,续费同价!