首页
论坛
课程
招聘
[原创] KCTF 2020 Win. 第四题 路在何方
2020-11-24 12:18 1718

[原创] KCTF 2020 Win. 第四题 路在何方

HHHso 活跃值
21
2020-11-24 12:18
1718

庚子年十月初八,小雪,贞曰:宜坏垣(拆除围墙)。

 

摘要:这是一条几乎全静态的分析之路,部分片段使用了Unicorn模拟执行。最后有上机的操作,是在最后一步自陷后,投石问路之举。目的是捕捉下特定时态的内存空间,拍个照,以印证静态分析走过的路是否正确,能否继续走下去。分析都是以第一个加固版本为样本【App_Crackme.apk】。

 

两个版本样本【App_Crackme.apk】和【disanti.apk】参考文后附件。

 

一、初次见面
(1)7z x App_Crackme.apk -oApp_Crackme
(2)App_Crackme.apk 直接上JEB,如图。

 

我们通过(1)的7z.exe命令行压缩工具,将apk当zip解压到App_Crackme目录,以备用。通过(2)直接上了手头的JEB。题外,按以前的套路,(1)过后会直接将App_Crackme目录的class.dex上IDA分析;或者通过ApkToolkit.exe将apk转换为jar后上jdgui.exe查看,或者apk直接拖进jadxgui.exe也行(应注意到与前面的jdgui.exe不是同一个东西)。

 

大概浏览下apk的目录树,这时候注意到目录树中一些so库,这时候,
(3)可以将感兴趣的放IDA分析,如libcrack.so和libjaigu_x86.so,如图。
so在IDA自动分析完后,首先关注的肯定是敏感的到处函数,如JIN_OnLoad和java_com*这些,我们也注意到一些decode**类的函数。
概览后做了一些防护,一时间没法得到太多有用信息,先回去看看java或smali层。
图片描述

 

图片描述

 

二、绳头
从Mainifest中我们找到绳头"com.stub.StubApp"启动类,如图。
最开始JEB并不会对反编译对象做任何改动,所以类或成员以及方法都是一些不可见字节内容。这样会非常影响阅读和交叉引用查阅,我们可以右键选择rename all自动全部做初步人性化命名。让后根据StupApp类的构造函数和一些关键函数,随类方法和成员做基本重命名。

 

题外,IDA自身并不带自动重命名,这点比JEB弱很多,估计得找或自行写脚本重命名。

 

图片描述

 

图片描述

 

图片描述

 

AppStub的OnCreate没什么重要信息,如图。

 

图片描述

 

在AppStub相关构造函数中,我们知晓了AppStup的基本结构。

 

图片描述

 

我们在AppStub非混淆的方法中,注意到attachBaseContext(Context)方法,如图。
图片描述

 

其中clsUtils->decrypt_str是逆向后根据起意义重新在jeb自动命名后修正的伪类名和方法。通过逆向可以看到"decrypt_str"类方法是对一类加密字符解密操作,所以做出此命名,如
"q~tb\u007Fyt>s\u007F~du~d>`}>@qs{qwu@qbcub4@qs{qwu"
算法也简单,只是对字符异或0x10,python的解密实现如下。

1
2
3
4
5
6
7
8
import re
def decrypt_str(bs=''):
  cbs = re.findall('\\u00(..)',bs)
  for cb in cbs:
    bs = bs.replace('\\u00'+cb,chr(int(cb,0x10)))
  return ''.join([chr(ord(b)^0x10) for b in bs])
 
decrypt_str("q~tb\u007Fyt>s\u007F~du~d>`}>@qs{qwu@qbcub4@qs{qwu")

我们也对clsUtils的其他方法功能做了初步分析,都是写功能性的方法,
其也在util包中,所以如此命名,其他方法如:
(1)make_p0_close(Closeable);传入文件等一类具有.close()的可关闭对象,关闭对象。
(2)IsSupported_x86();是否支持x86。
(3)cmp_bis(BufferedInputStream, BufferedInputStream)Z;对比文件流,用于比对文件。
(4)cmp_copy__assetsFilename_to_dir_filename(Context, String, String, String);迁移文件等操作,用来搬动所需so等文件。
图片描述

 

图片描述

 

attachBaseContext加载加固相关的so后,转入了so地方方法中
.method public static native interface5(Application)V
而加固相关的so又是自藏式的,如果还有一条路走到黑,就只能网jiagu.so的JNI_OnLoad等方法硬干了。否则,至此,java或smali层已经没太多信息,静态之路变得不好走了,如何是好?

 

图片描述

 

三、遇事不决,量子力学
量子力学?开个玩笑。我们还是继续看看其他什么角落又什么发现。翻箱倒柜,Certificate,.appkey,Resource下的一堆有瞄过,发现有个奇怪的【b.txt】,不得不说,JEB在自动识别上还是很人性化。直接双击就自动识别打开了,惊不惊喜,意不意外?
图片描述
(1)直接点开了check,如图。
当然,正常还是得按套路,从MainActivity开始。
题外,有试过在手上的Mate 30 Pro安装运行,结果如图。
虽然运行不起来,但还是注意到了CHECK这些。
图片描述

 

check的在JEB的smali视图中也还算清晰
(1)key长度为16
(2)ek = crypt(key)
(3)ck = com.kanxue.crackme.MyCrack.crackjni(ek)
(4)eck = crypt(ck)
(5)rk = base64.b64encode(eck)
(6) crk = com.kanxue.crackme.MyCrack.crypt
bOK = (rk== crk? crk:"test")

 

图片描述

 

至此,基本任务需要分别拿下crypt和crackjni方法还有MyCrack.crypt成员。JEB的crypt方法smalic基本看一下有点像RC41。用ApkToolkit将b.txt (重命名为b.dex)转换为jar用jdgui打开,如图
图片描述

 

题外:到此,实际上如果一开始直接将【App_Crackme.apk】仍经jadx-gui.exe;完全没有那么什么量子力学什么事,jadx-gui.exe直接一杆到底,自动识别了b.txt,并合并到类代码中,且可以对比看到,jdgui对0x100还不大友好,jadx-gui.exe比较完美还原,如图。
图片描述

 

题外:关于crypt方法,只看到samli的时候,直接推定了为RC41(也的确是真的RC41),因为没有动态调试,暂时也没想到直接测试反编译的java代码。所以放一边了,只知道这就算是改版的RC41,至少问题有解。就先看crackjni方法。实际上,眼见不一定为实,这时候一只脚已经踩坑里,尤其当你觉得一个真的RC41是假的时候,而真假似乎都对不上时,"你可能开始怀疑人生"。

 

(2)crackjni
在IDA中,我们直接定位libcrack.so的导出函Java_com_kanxue_crackme_MyCrack_crackjni,如图
图片描述

 

密密麻麻一堆,且IDA的字符串信息几乎没有有价值的信息。
事实上前期的探查中,我们注意到decode一类的导出函数的业务逻辑相对简单,就是对全局静态内容的异或操作,可知是加解密操作,通过观察,我们发现其都是用字节串最后一个字节作为异或因子,因此,我们可以直接解密字节内容看看。通过采样解密,确定是字符串一类。全部解密如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
s_ea = 0x1F004
e_ea = 0x1F2FB
cea = s_ea
ea_ss = {}
while cea <= e_ea:
  n = idc.Name(cea)
  if n:
    nea = cea
    bs = [idc.GetOriginalByte(cea)]
    cea+=1
    while idc.Name(cea)=='' and cea <= e_ea:
      bs.append(idc.GetOriginalByte(cea))
      cea+=1
    while len(bs)>0 and bs[-1]==0:
      bs.pop(-1)
    x = bs[-1]
    if nea in [0x1F0B8]:
      pass
    else:
      ea_ss[nea]= ''.join([chr(b^x) for b in bs])
      print("{:X}".format(nea),ea_ss[nea][:-1].__repr__())
      for i in range(len(ea_ss[nea])):
         idc.PatchByte(nea+i,ord(ea_ss[nea][i]))
      idc.MakeStr(nea,nea+len(ea_ss[nea]))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
结果如下
('1F004', "'/proc/self/task'")
('1F014', "'/proc/%d/task'")
('1F022', "'/proc/self/maps'")
('1F032', "'r'")
('1F034', "'r-xp'")
('1F039', "'%lx'")
('1F03D', "'/system/lib/'")
('1F04A', "'/odm/lib/'")
('1F054', "'/vendor/lib/'")
('1F061', "'openmemory'")
('1F06C', "'DexFile'")
('1F074', "'art'")
('1F078', "'OpenMemory'")
('1F083', "'OatDexFile'")
('1F090', "'LoadClassMembers'")
('1F0A1', "'ClassLinker'")
('1F0AD', "'LoadMethod'")
('1F0C0', "'ro.build.version.sdk'")
('1F0D5', "'\\x7fELF'")
('1F0E0', "'read /proc/self/maps'")
('1F0F5', "'/proc/self/maps'")
('1F105', "'frida'")
('1F10B', "'fdafd'")
('1F120', "'%x-%lx %4s %lx %*s %*s %s'")
('1F13A', "'.oat'")
('1F140', "'ro.product.cpu.abi'")
('1F160', "'ro.build.version.sdk'")
('1F175', "'libc.so'")
('1F17D', "'execve'")
('1F190', "'/system/lib/libart.so'")
('1F1A6', "'openmemory'")
('1F1C0', "'LoadClassMembers'")
('1F1E0', "'kaokaonikaokaoni'")
('1F200', "'com/kanxue/crackme/MyCrack'")
('1F21B', "'crypt'")
('1F230', "'Ljava/lang/String;'")
('1F250', "'l+x7fKd2FBaaEY4NV4309A==\\n'")
('1F270', "'ZmxhZ3RyeWFnYWluP30='")
('1F285', "'warn'")
('1F290', "'only support Android 7,Android 7.1,Android 8,Android 8.1,Android 9'")
('1F2D3', "'x86'")
('1F2E0', "'only support arm and arm64'")

我们注意到 ('1F21B', "'crypt'"),也注意到一些base64字符串如,
('1F250', "'l+x7fKd2FBaaEY4NV4309A==\n'")
('1F270', "'ZmxhZ3RyeWFnYWluP30='")
当然还有我们在不支持运行手机上执行的提示
('1F290', "'only support Android 7,Android 7.1,Android 8,Android 8.1,Android 9'")
注意到('1F105', "'frida'")大概猜测到要反这类插桩的功能组件了。我们静态分析,可以先忽略。其中('1F21B', "'crypt'")的交叉引用到Java_com_kanxue_crackme_MyCrack_crackjni,我们跟进查看。逻辑相对也清晰 FindClass、GetStaticFieldID、NewStringUTF的逆向原理来自JNIEnv*结构,其是接口虚函数,根据偏移量可以确定函数名称。
图片描述
图片描述

 

(A)图中下半部分与crypt相关的就是com.kanxue.crackme.MyCrack.crypt="l+x7fKd2FBaaEY4NV4309A==\n"
本想解决crackjni,反而先解决了.MyCrack.crypt的值。
(B)图中上半部分,由于一开始没有逆向确定Hi_dex_for_mprotect的含义,所以无法确定其意义。实际这是挽救前面掉进坑的那只脚的关键。到这里先放一放,本来也没太在意这一部分,毕竟防护加固,涉及写内存修改难免。

 

在crackjni大概的浏览分析中,基本确定了只有部分有效代码,如下图方框所示,而几个间隔的循环材猜测是反调试一类操作。crackjni入口处,通过定位ek的交叉引用,我们得到了crackjni对ek运输的核心处理函数Hi_core_func_ek_ck_rk,如图
图片描述
图片描述
图片描述
Hi_core_func_ek_ck_rk函数业务逻辑比较清楚
其中ek是crackjni输入,ck是内置的"kaokaonikaokaoni",rk为处理结果。
(1)rk=ek;k初始为ek值,
(2)Hi_stepA_gen_xtbl_from_pck;通过ck,生产xtbl表
(3)Hi_stepB;通过xtbl表和内置的sbox表等对rk进行变换。
图片描述
图片描述
其中(2)中的Hi_stepA_gen_xtbl_from_pck没用到ek,而只用到内置固定的ck和固定的sbox表和Hi_Noekeon表。
如图, Hi_sbox_at只是对sbox表的索引[idx]。
图片描述

 

其中(3)StepB对rk进行主要的变换,我们需要根据变换业务逻辑得到逆变换
StepB如图,业务逻辑也比较清晰,其中关键的几个变换函数如下
(3.1)Hi_prk_xor_xtbl_R0_A10h_X,使用xtbl中第X行矩阵与rk异或,xtbl有11行,每行16个字节,rk也是16个字节矩阵,变换可逆。
(3.2)Hi_sbox_map,使用sbox表对rk进行索引变换,即rk[i]=sbox[rk[i]],i=0...15,变换可逆。
(3.3)Hi_rxbhgd对rk内部16字节顺序进行交换,变换可逆。
(3.4)Hi_cbx对rk分段(没四个字节一段)进行变换,半可逆。
可见,要完成逆变换,关键是(2)中的xtbl,sbox表以及(3)中的各个变换的逆变换
图片描述

 

四、正负变换
(4.1)获取xtbl,sbox
(4.2)各正逆变换

 

IDA分析中发现,Hi_core_func_ek_ck_rk函数层级调用全是纯内调的shellcode(内有对系统api调用)。所以用unicorn简单配置即可模拟执行,不需要任何额外劫持解析。如图,初始化ek,ck,rk的指针,然后从0xC2B8运行到Step停止,然后我们读取xtbl的值(也读取sbox)备用。
图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from unicorn import *
from unicorn.arm_const import *
import sark
import idc
import struct
 
segs = list(sark.segments())
elf_base = segs[0].ea
elf_size = segs[-1].ea+segs[-1].size
elf_size = 0x1000*((elf_size+0x0FFF)/0x1000)
stack_size = 4*1024*1024
mem_size = 4*1024*1024
mem_ptr = elf_base + elf_size + stack_size
all_size = elf_size + stack_size + mem_size
stack_init = elf_base + elf_size + stack_size/2
mu = Uc(UC_ARCH_ARM, UC_MODE_ARM)
mu.mem_map(elf_base, all_size)
print("Init Module: base:= {:06X} size:= {:04X}".format(elf_base,all_size))
for seg in segs:
  segdata = idc.get_bytes(seg.ea,seg.size)
  mu.mem_write(seg.ea, segdata)
  print("Init Seg: base:= {:06X} size:= {:04X}".format(seg.ea,seg.size))
 
ek_ptr = mem_ptr+0
ck_ptr = mem_ptr+0x100
rk_ptr = mem_ptr+0x200
mu.mem_write(ek_ptr,'T\xa7\xd0I;D\xaf\xe2d\xb2\x1b\x1cO%\x1d\x1e')#crypt('1234567890123456') kaokaonio
mu.mem_write(ck_ptr,"kaokaonikaokaoni")
mu.mem_write(rk_ptr,"\x00"*16)
SP=stack_init-0x448
mu.reg_write(UC_ARM_REG_SP,SP)
mu.mem_write(SP+0xBC,struct.pack("L",ek_ptr))
mu.mem_write(SP+0xB0,struct.pack("L",ck_ptr))
mu.mem_write(SP+0xAC,struct.pack("L",rk_ptr))
mu.emu_start(0xC2B8+1, 0x7F98) #实际由于StepB不对xtbl变换,可以模拟执行完整个正变换(0xC2B8+1, 0xC2C2)踩读取xtbl
xtbl_ea = 0x1F318
sbox_ea = 0x18F5B
xtbl = mu.mem_read(xtbl_ea,0x10*11)
sbox = mu.mem_read(sbox_ea,0x256)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
def encrypt_StepB(ek):#Step正变换
  rk = ek
  if isinstance(ek,str):
    rk = [ord(c) for c in ek]
  rk = x16(rk,xtbl_xm(0))
  for i in range(1,10):
    rk = sbox_map(rk)
    rk = rxchg(rk)
    rk = cbx16(rk)
    rk = x16(rk,xtbl_xm(i))
  rk = sbox_map(rk)
  rk = rxchg(rk)
  rk = x16(rk,xtbl_xm(10))
  return ''.join([chr(v) for v in rk])
 
def decrypt_AntiStepB(rk):#Step逆变换
  rk = rk
  if isinstance(rk,str):
    rk = [ord(c) for c in rk]
  rk = x16(rk,xtbl_xm(10))
  rk = anti_rxchg(rk)
  rk = anti_sbox_map(rk)
  for i in range(9,0,-1):
    rk = x16(rk,xtbl_xm(i))
    rk = anti_cbx16(rk)
    rk = anti_rxchg(rk)
    rk = anti_sbox_map(rk)
  rk = x16(rk,xtbl_xm(0))
  return ''.join([chr(v) for v in rk])
 
def xtbl_xm(x):#选取xtbl第x行矩阵
  return xtbl[x*16:x*16+16]
 
def sbox_map(m):#sbox_map正变换
  r = [0]*len(m)
  for i,v in enumerate(m):
    r[i] = sbox[v]
  return r
 
def anti_sbox_map(m):#sbox_map逆变换
  r = [0]*len(m)
  for i,v in enumerate(m):
    r[i] = sbox.index(chr(v))
  return r
 
def x16(a,b):#行矩阵异或
  c = [0]*16
  for i in range(16):
    c[i]=a[i]^b[i]
  return c
 
def rxchg(r):#行元素位置正交换
  v = r[1]
  r[1]=r[5]
  r[5]=r[9]
  r[9]=r[13]
  r[13]=v
  #
  v=r[2]
  r[2]=r[10]
  r[10]=v
  #
  v=r[6]
  r[6]=r[14]
  r[14]=v
  #
  v=r[3]
  r[3]=r[15]
  r[15]=r[11]
  r[11]=r[7]
  r[7]=v
  return r
 
def anti_rxchg(r):#行元素位置逆交换
  v=r[7]
  r[7]=r[11]
  r[11]=r[15]
  r[15]=r[3]
  r[3]=v
  #
  v=r[14]
  r[14]=r[6]
  r[6]=v
  #
  v = r[10]
  r[10]=r[2]
  r[2]=v
  #
  v=r[13]
  r[13]=r[9]
  r[9]=r[5]
  r[5]=r[1]
  r[1]=v
  return r
 
def bxchg(b):#字节Noekeon正变换,姑且起名Noekeon
  b = c_ubyte(b)
  bm = b.value&0x80
  b.value = b.value << 1
  if bm:
    b.value ^= 0b00011011
  else:
    b.value ^= 0b00000000
  return b.value
 
def anti_bxchg(b):#字节Noekeon负变换
  b = c_ubyte(b)
  bm = b.value&1
  if bm:
    b.value ^= 0b00011011
  else:
    b.value ^= 0b00000000
  b.value = b.value >> 1
  b.value = b.value|0x80 if bm else b.value
  return b.value
 
def cbx4(m):#4字节Noekeon正变换
  c0,c1,c2,c3=m
  cx = c0
  cy = c0^c1^c2^c3
  t0 = c0 ^ cy ^ bxchg(c0^c1)
  t1 = c1 ^ cy ^ bxchg(c2^c1)
  t2 = c2 ^ cy ^ bxchg(c3^c2)
  t3 = c3 ^ cy ^ bxchg(c3^c0)
  bm = [t0,t1,t2,t3]
  return bm
 
def cbx16(m):#16字节Noekeon正变换
  bm = []
  for i in range(0,16,4):
    bm+=cbx4(m[i:i+4])
  return bm
 
def anti_cbx4(m):#4字节Noekeon负变换
  t0,t1,t2,t3 = m
  cy = t0^t1^t2^t3
  for rx in range(0,0x100):
    r0 = rx
    r1 = anti_bxchg(t0^cy^r0)^r0
    r2 = anti_bxchg(t1^cy^r1)^r1
    r3 = anti_bxchg(t2^cy^r2)^r2
    br0 = anti_bxchg(t3^cy^r3)^r3
    if r0==br0:
      return [r0,r1,r2,r3]
      #print(r0,r1,r2,r3)
 
def anti_cbx16(m):#16字节Noekeon负变换
  bm = []
  for i in range(0,16,4):
    bm+=anti_cbx4(m[i:i+4])
  return bm
 
from Crypto.Cipher import ARC4
def myRC4(data,key='kaokaonio'):
    rc41 = ARC4.new(key)
    encrypted = rc41.encrypt(data)
 
def crypt(p,key='keepGoing'):#crypt,实际就是RC41,可用 myRC4替换
  pm = p
  if isinstance(p,str):
    pm = [ord(c) for c in p]
  #bk = [ord(c) for c in 'kaokaonio']
  bk = [ord(c) for c in key]
  bk_len = len(bk)
  bm = [i for i in range(0x100)]
  k = 0
  m = 0
  for i in range(0x100):
    m = (m + bk[k] + bm[i])&0xFF
    t = bm[i]
    bm[i] = bm[m]
    bm[m] = t
    k = (k+1)%bk_len
  #
  pm_len = len(pm)
  rm = [0]*pm_len
  x = 0
  y = 0
  for i in range(pm_len):
    x = (x+1)&0xFF
    y = (y+bm[x])&0xFF
    t = bm[x]
    bm[x]=bm[y]
    bm[y]=t
    n = (bm[x]+bm[y])&0xFF
    rm[i]=pm[i]^bm[n]
  return rm
 
def ts(m):#矩阵到字符串
  return ''.join([chr(c_ubyte(i).value) for i in m])
 
def tm(s):#字符串到矩阵
  return [ord(i) for i in s]
 
def prk(m):#辅助打印矩阵或字符串
  if isinstance(m,str):
    print(' '.join(["{:02X}".format(ord(c)) for c in m]))
  else:
    print(' '.join(["{:02X}".format(c) for c in m]))

其中关键还是(3.4)的cbx半可逆变换。这里称cbx为16字节Noekeon变换,其只是是分段完成,关键是4字节Noekeon变换;之所以称为Noekeon,是在xtbl生成中用到Noekeon表,为啥又称Noekeon表,因为在用表中的常量google时,其结果展示Noekeon算法,而4字节Noekeon变换与Noekeon算法又相似之处,虽然未证明其关系如何,姑且成为Noekeon变换。这里的之所以说半可逆,可能时能力有限,未找到高效的完全可逆算法,这里四个字节,相互限制,一个确定,则可以推定另一个,是一个闭环证明。及4字节只有256中情形,
所以可以遍历的方式进行逆变换。

1
2
3
4
5
6
7
8
9
10
11
12
def anti_cbx4(m):#4字节Noekeon负变换
  t0,t1,t2,t3 = m
  cy = t0^t1^t2^t3
  for rx in range(0,0x100):
    r0 = rx
    r1 = anti_bxchg(t0^cy^r0)^r0
    r2 = anti_bxchg(t1^cy^r1)^r1
    r3 = anti_bxchg(t2^cy^r2)^r2
    br0 = anti_bxchg(t3^cy^r3)^r3
    if r0==br0:
      return [r0,r1,r2,r3]
      #print(r0,r1,r2,r3)

五、出路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
根据验证正向变换:
key == '1234567890123456'
ek == crypt(key,'kaokaonio')
rk == encrypt_StepB(ek)
erk == crypt(key,'kaokaonio')
rk == base64.b64encode(erk)
#rk = "l+x7fKd2FBaaEY4NV4309A=="
逆向变换得到key
rk = "l+x7fKd2FBaaEY4NV4309A=="
erk = base64.b64decode(rk)
rk = crypt(erk,'kaokaonio')
ek = decrypt_AntiStepB(rk)
key = crypt(ek,'kaokaonio')
ts(key)
',\x08v{u\xae\xe7\xdc 3GWE\x8fG\xdd'
这样的结果,明显对不上号,哪里出了问题?

各正逆变换都是测试过多,大体逻辑应该也不会有问题。在排查各正逆变换没问题后,最终还是回到了前述crackjni对crypt属性设置的代码前面部分片段
图片描述
如图,这一部分意思可以看出 Hi_dex_for_mprotect 放的是一个指针,
其对指针Hi_dex_for_mprotect偏移0x16D3A6位置修改为 0xD3,0x3D,那么Hi_dex_for_mprotect是啥?静态分析并不能得到快速确定,我们看下Hi_dex_for_mprotect的交叉引用,尝试确定其赋值位置。
如图,有几处都是赋值位置,我们看下第一处。
图片描述
图片描述
在第一处赋值位置,我们看到其值来自于局部变量lv_30_src_ptr.04hww,
其也作为mprotect函数的地址addr参数,结果PROT_WIRTE,我们就更确定是为后面的内存修改铺垫了。同时主要到lv_30_src_ptr.04hww也即Hi_dex_for_mprotect的长度是lv_30_src_ptr.08hww。
在mprotect修改前,lv_30_src_ptr.08hww长度len与0x281CB8比较,必须小于它,可见目标内存长度原则上不超过0x281CB8,突破口源自对整个数字的敏感。因为前面用7z解压出的b.txt大小就是2MB左右,会不会就是它?这只是猜测,我们用python的相关函数获取了b.txt的字节大小,

1
2
import os
hex(os.path.getsize(".\b.txt"))

发现就是0x281CB8,我们有理由相信这不是巧合,应该是对b.txt的dex做了修改。我们用十六进制编辑器修改偏移0x16D3A6位置修改为 0xD3,0x3D,然后拖进JEB查看。发现crypt函数内置的crypt_key变了,由原来的"kaokaonio"变成了"keepGoing",可见这是修改了资源ID。这下就清楚了,在encrypt_StepB(即crackjni)对ek处理,crypt函数使用后是"kaokaonio",而调用encrypt_StepB后,变为了"keepGoing",所以crypt再次变换使用了不同的rc_key。即正向变换应该为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
key == '1234567890123456'
ek == crypt(key,'kaokaonio')
rk == encrypt_StepB(ek)
erk == crypt(key,'keepGoing')
rk == base64.b64encode(erk)
#rk = "l+x7fKd2FBaaEY4NV4309A=="
所以反向变换为
rk = "l+x7fKd2FBaaEY4NV4309A=="
erk = base64.b64decode(rk)
rk = crypt(erk,'keepGoing')
ek = decrypt_AntiStepB(rk)
key = crypt(ek,'kaokaonio')
 
ts(key)

结果如图,即flag{thisiskey!}为我们所求。
图片描述

 

题外,实际上在rc_key上走过的路并没那么顺畅就跳出坑了。一开始并没有反应过来两次使用的rc_key不一样,测试中都使用相同的rc_key=kaokaonio或keepGoing,结果自然失配。最后怀疑到crackjni使用的内置"kaokaonikaokaoni"会不会也修改成了其他值。但静态分析并没有发现由直接的修改,所以只能上测试机看下。实测中发现反调试还是如预料中那么”强大“。

 

六、上机
测试机是以前已经刷了官方开发版和unlock的红米,主要是对adb reboot命令和root权限无障碍。
图片描述
cmd命令行窗口1:

1
2
3
4
5
6
7
8
9
(A)先完成apk安装和ida调试服务的安装,
adb install App_Crackme.apk
adb push "C:\IDA 7.2\dbgsrv\android_server"  /data/local/tmp
(B)我们在apk启动后再启动IDA调试服务,否则反调试应该检测到调试端口打开了,apk无法正常启动
adb shell
su
cd /data/local/tmp
chmod 777 ./android_server
./android_server

cmd命令行窗口2:

1
2
(C)手机调试端口映射到计算机端口
adb forward tcp:23946 tcp:23946

实际上我们目的只是想采集某个时态的内存情况,并不一定需要可以跑起来。
因为校验失败后,退出前有一定的停留,且这时候crackjni已经执行过,xtbl等都可能保留,这是我们捕捉的时机。
具体步骤如下:

1
2
3
4
5
6
7
(1)启动apk,输出"1234567890123456",不要点【CHEKCK】校验;输入需要16位,以便可以执行到crackjni。
(2)执行上述(B)启动IDA调试服务,并执行(C)完成端口映射
(3)打开IDA7.2,
  (3.1)选择Debugger|Attach|Remote ARM Linux/Android debuger
  (3.2)配置localhost和上述映射调试端口23946,点确定。
  (3.3)在出现的进程列表上排序选中com.kanxue.crackme,先不要点【确定】
(4)点击手机界面的【CHEKCK】校验,在出现退出提示的瞬间点击(3)的【确定】。这时候目标app会被调试器挂起,等处理完,IDA就捕捉到了目标进程空间,不需要运行起来。这时候我们就可以对我们感兴趣的地址空间内容进行查看了。

图片描述
图片描述
图片描述
图片描述
如,可直接通过模块导出函数定位crackjni,也可以结合基址和偏移就定位到 ck="kaokaonikaokaoni"如图
Jump(0xCC4A7000+0xC2BE)
图片描述
图片描述
这里可以发现ck并没有改变(也可能改回去也不一定,只是假设)
图片描述
图片描述
图片描述
进一步跟进,通过prk和pck指针,我们可以得到 prk = crackjni(crypt(key="1234567890123456"))的运行结果
可以用于验证我们静态分析得到的正变换是否正确,如果逆向的正变换正确,则逆变换也正确;
题外:逆向的正逆变换本身可以自证,因为 anti_tranf(tranf(input))==input,正确性与正变换一致。
经过这个简单的拍照印证,可以确定下述正变换逻辑正确

1
2
3
4
5
6
7
key == '1234567890123456'
ek == crypt(key,'kaokaonio')
rk == encrypt_StepB(ek)
因为前面已经注意到crypt更改keepGoing的问题,但这里发现用的还是kaokaonio,
这是后才突然开悟,后面用的不是kaokaonio,而是keepGoing,所以到这里才算真正跳出坑。
erk == crypt(key,'keepGoing')
rk == base64.b64encode(erk)

[注意] 欢迎加入看雪团队!base上海,招聘安全工程师、逆向工程师多个坑位等你投递!

最后于 2020-11-24 12:47 被HHHso编辑 ,原因:
上传的附件:
收藏
点赞1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回