Android 阿拉伯加密型锁机软件分析
作者:云在天(Harry)
发布平台:吾爱破解论坛
转载请说明出处
基本信息
文件名:com.tianlan.qq1247319341
MD5:688ea7a90e005a31ab41d79653961818
病毒行为
- 由任意宿主程序释放该病毒(实机测试并无Root权限申请)
- 释放后运行该病毒并重启
- 创建一个类悬浮窗的VIew来达到锁机的目的
逆向分析
难点
- 因为宿主程序加壳,在不好脱壳的情况下可以模拟器或实机运行后在ADB命令的辅助下结束该病毒进程&得到病毒文件这也是比较通用的解除锁机病毒的操作。
- 在得到病毒样本的前提下,载入任意安卓逆向分析软件(JEB,JD-gui,Android-killer等),发现类名方法名均为类似阿拉伯语的文字,我们知道像阿拉伯这种语言的阅读顺序都是从右向左读,所以给分析带来了一定困难。
- 可能会存在网络读取或MD5解密,在之前的帖子中有提到。
- Last but not least, 你没有足够的耐心。
PS:本文完成于1万米的高空,所以无图,分析均为文字,希望论坛支持阿拉伯文的显示
0x1 获得病毒样本
首先运行模拟器或实机,实机一定要打开USB调试或按照第三方Recover(TWRP等)。
连接电脑,打开ADB,输入adb shell pm list packages -f
并复制到文本文档
然后运行宿主程序,再次输入命令与之前的文本对比,得到释放的病毒文件及APK路径
输入adb shell am force-stop 包名
结束病毒进程
输入adb pull APK路径 本地路径
把病毒下载到电脑中,这样就完成了获取病毒样本。
0x2 获取关键smali
将病毒样本载入Android-Killer中查看入口
搜索关键字removeview定位关键smali
在此次的样本中关键smali为 Lݕݔݤݔݑݑݧ
找到关键字所在,是几个连续的if语句
以下代码均为JEB2中生成的伪代码
if(!TL.ݐݥݫݑݤݟݖ(this.ݐݥݫݑݤݟݖ).getText().toString().isEmpty()) {
if(TL.ݐݥݫݑݤݟݖ(this.ݐݥݫݑݤݟݖ).getText().toString().equals(String.valueOf(ݑݛݚݑݫݢݞ.ݐݥݫݑݤݟݖ(new byte[]{21, 80, 22, 81, 21, 65, 22, 68, 21, 80, 89, 97, 21, 66, 73, 102, 42})))) {
TL.ݐݥݫݑݤݟݖ(this.ݐݥݫݑݤݟݖ).setText(ݖݓݒݖݥݞݣ.ݐݥݫݑݤݟݖ(new StringBuffer().append("").append(this.ݐݥݫݑݤݟݖ.ݓݖݧݠݡݬݛ).toString()));
TL.ݑݪݚݨݣݬݞ(this.ݐݥݫݑݤݟݖ).setText(ݑݛݚݑݫݢݞ.ݐݥݫݑݤݟݖ(new byte[]{21, 66, 18, 116, 21, 121, 77, 110, 21, 66, 103, 99, 21, 80, 119, 87, 111, 85, 69, 83, 82, 111, 115, 22, 74, 111, 119, 88, 71, 71, 29, 29, 42}));
}
else if(TL.ݐݥݫݑݤݟݖ(this.ݐݥݫݑݤݟݖ).getText().toString().equals(new StringBuffer().append("").append(this.ݐݥݫݑݤݟݖ.ݒݨݓݓݭݠݑ).toString())) {
TL.ݐݥݫݑݤݟݖ(this.ݐݥݫݑݤݟݖ).setText(ݖݓݒݖݥݞݣ.ݐݥݫݑݤݟݖ(new StringBuffer().append("").append(this.ݐݥݫݑݤݟݖ.ݔݨݛݟݫݪݧ).toString()));
TL.ݑݪݚݨݣݬݞ(this.ݐݥݫݑݤݟݖ).setText(ݑݛݚݑݫݢݞ.ݐݥݫݑݤݟݖ(new byte[]{21, 66, 18, 116, 21, 121, 77, 110, 21, 66, 103, 99, 21, 80, 119, 87, 111, 85, 69, 83, 82, 111, 115, 20, 73, 69, 119, 88, 71, 71, 29, 29, 42}));
}
else if(TL.ݐݥݫݑݤݟݖ(this.ݐݥݫݑݤݟݖ).getText().toString().equals(new StringBuffer().append("").append(this.ݐݥݫݑݤݟݖ.ݓݖݧݠݡݬݛ).toString())) {
TL.ݐݥݫݑݤݟݖ(this.ݐݥݫݑݤݟݖ).setText(ݖݓݒݖݥݞݣ.ݐݥݫݑݤݟݖ(new StringBuffer().append("").append(this.ݐݥݫݑݤݟݖ.ݕݣݤݣݤݬݦ).toString()));
TL.ݑݪݚݨݣݬݞ(this.ݐݥݫݑݤݟݖ).setText(ݑݛݚݑݫݢݞ.ݐݥݫݑݤݟݖ(new byte[]{21, 66, 18, 116, 21, 121, 77, 110, 21, 66, 103, 99, 21, 80, 119, 87, 111, 85, 69, 83, 82, 111, 119, 66, 77, 11, 119, 88, 71, 71, 29, 29, 42}));
}
else if(TL.ݐݥݫݑݤݟݖ(this.ݐݥݫݑݤݟݖ).getText().toString().equals(new StringBuffer().append("").append(this.ݐݥݫݑݤݟݖ.ݑݪݚݨݣݬݞ).toString())) {
TL.ݐݥݫݑݤݟݖ(this.ݐݥݫݑݤݟݖ).setText(ݖݓݒݖݥݞݣ.ݐݥݫݑݤݟݖ(new StringBuffer().append("").append(this.ݐݥݫݑݤݟݖ.ݖݡݫݒݚݓݠ).toString()));
TL.ݑݪݚݨݣݬݞ(this.ݐݥݫݑݤݟݖ).setText(ݑݛݚݑݫݢݞ.ݐݥݫݑݤݟݖ(new byte[]{21, 66, 18, 116, 21, 121, 77, 110, 21, 66, 103, 99, 21, 80, 119, 87, 111, 85, 69, 83, 82, 111, 115, 22, 76, 111, 119, 88, 71, 71, 29, 29, 42}));
}
else if(TL.ݐݥݫݑݤݟݖ(this.ݐݥݫݑݤݟݖ).getText().toString().equals(new StringBuffer().append("").append(this.ݐݥݫݑݤݟݖ.ݐݥݫݑݤݟݖ).toString())) {
TL.ݐݥݫݑݤݟݖ(this.ݐݥݫݑݤݟݖ).setText(ݖݓݒݖݥݞݣ.ݐݥݫݑݤݟݖ(new StringBuffer().append("").append(this.ݐݥݫݑݤݟݖ.ݗݣݔݒݛݤݪ).toString()));
TL.ݑݪݚݨݣݬݞ(this.ݐݥݫݑݤݟݖ).setText(ݑݛݚݑݫݢݞ.ݐݥݫݑݤݟݖ(new byte[]{21, 66, 18, 116, 21, 121, 77, 110, 21, 66, 103, 99, 21, 80, 119, 87, 111, 73, 100, 77, 78, 105, 100, 76, 75, 105, 23, 75, 85, 105, 100, 76, 83, 121, 105, 29, 42}));
}
else if(TL.ݐݥݫݑݤݟݖ(this.ݐݥݫݑݤݟݖ).getText().toString().equals(ݗݬݐݣݜݝݠ.ݑݪݚݨݣݬݞ(new StringBuffer().append("").append(this.ݐݥݫݑݤݟݖ.ݔݨݛݟݫݪݧ).toString()))) {
TL.ݐݥݫݑݤݟݖ(this.ݐݥݫݑݤݟݖ).removeView(TL.ݐݥݫݑݤݟݖ(this.ݐݥݫݑݤݟݖ));
}
}
乍一看就想放弃了,这里我们可以利用JEB的特性把类名方法名重命名即可,双击进入每一个调用的方法名,右键Rename即可。这里可以根据命令和控件类型进行重命名。结果如下
if(!TL.Input(this.TL_public).getText().toString().isEmpty()) {
if(TL.Input(this.TL_public).getText().toString().equals(String.valueOf(Base64.decode(new byte[]{21, 80, 22, 81, 21, 65, 22, 68, 21, 80, 89, 97, 21, 66, 73, 102, 42})))) {
TL.Lable(this.TL_public).setText(Public.text(new StringBuffer().append("").append(this.TL_public.a).toString()));
TL.lable_TL(this.TL_public).setText(Base64.decode(new byte[]{21, 66, 18, 116, 21, 121, 77, 110, 21, 66, 103, 99, 21, 80, 119, 87, 111, 85, 69, 83, 82, 111, 115, 22, 74, 111, 119, 88, 71, 71, 29, 29, 42}));
}
else if(TL.Input(this.TL_public).getText().toString().equals(new StringBuffer().append("").append(this.TL_public.a1).toString())) {
TL.Lable(this.TL_public).setText(Public.text(new StringBuffer().append("").append(this.TL_public.b).toString()));
TL.lable_TL(this.TL_public).setText(Base64.decode(new byte[]{21, 66, 18, 116, 21, 121, 77, 110, 21, 66, 103, 99, 21, 80, 119, 87, 111, 85, 69, 83, 82, 111, 115, 20, 73, 69, 119, 88, 71, 71, 29, 29, 42}));
}
else if(TL.Input(this.TL_public).getText().toString().equals(new StringBuffer().append("").append(this.TL_public.b1).toString())) {
TL.Lable(this.TL_public).setText(Public.text(new StringBuffer().append("").append(this.TL_public.c).toString()));
TL.lable_TL(this.TL_public).setText(Base64.decode(new byte[]{21, 66, 18, 116, 21, 121, 77, 110, 21, 66, 103, 99, 21, 80, 119, 87, 111, 85, 69, 83, 82, 111, 119, 66, 77, 11, 119, 88, 71, 71, 29, 29, 42}));
}
else if(TL.Input(this.TL_public).getText().toString().equals(new StringBuffer().append("").append(this.TL_public.c1).toString())) {
TL.Lable(this.TL_public).setText(Public.text(new StringBuffer().append("").append(this.TL_public.d).toString()));
TL.lable_TL(this.TL_public).setText(Base64.decode(new byte[]{21, 66, 18, 116, 21, 121, 77, 110, 21, 66, 103, 99, 21, 80, 119, 87, 111, 85, 69, 83, 82, 111, 115, 22, 76, 111, 119, 88, 71, 71, 29, 29, 42}));
}
else if(TL.Input(this.TL_public).getText().toString().equals(new StringBuffer().append("").append(this.TL_public.d1).toString())) {
TL.Lable(this.TL_public).setText(Public.text(new StringBuffer().append("").append(this.TL_public.e).toString()));
TL.lable_TL(this.TL_public).setText(Base64.decode(new byte[]{21, 66, 18, 116, 21, 121, 77, 110, 21, 66, 103, 99, 21, 80, 119, 87, 111, 73, 100, 77, 78, 105, 100, 76, 75, 105, 23, 75, 85, 105, 100, 76, 83, 121, 105, 29, 42}));
}
else if(TL.Input(this.TL_public).getText().toString().equals(MD5.encode(new StringBuffer().append("").append(this.TL_public.e1).toString()))) {
TL.Manager(this.TL_public).removeView(TL.View_TL(this.TL_public));
}
}
0x3 详细分析
第一个判断是判断这个输入框的内容是否为空,不为空进入下一个判断。
第二个判断是判断输入框内容与方法base64.deocde(new byte[])的内容是否相等,这个方法名是我自己修改的,修改的依据是下面的代码
return new String(android.util.Base64.decode(arg4, 0));
这个地方可以直接把这个方法名复制到Java的IDE里调试解码即可,解出来的第一层的密码为枪宝最帅
如果这个判断结果为true就会执行以下操作
在界面的一个标签里设置一串加密后的文本以此作为下一层密码的基础,看一下加密的代码
int v9 = 2;
int v8 = 10;
int v1 = 0;
int v7 = 5;
byte[] v2 = arg10.getBytes();
StringBuilder v3 = new StringBuilder(v2.length * 2);
int v0;
for(v0 = 0; v0 < v2.length; ++v0) {
v3.append(Public.ݑݪݚݨݣݬݞ.charAt((v2[v0] & 15) >> 0));
}
String v0_1 = "";
String[] v2_1 = new String[v8];
v2_1[0] = Base64.decode(new byte[]{109, 97, 29, 29, 42});
v2_1[1] = Base64.decode(new byte[]{109, 113, 29, 29, 42});
v2_1[v9] = Base64.decode(new byte[]{109, 71, 29, 29, 42});
v2_1[3] = Base64.decode(new byte[]{109, 87, 29, 29, 42});
v2_1[4] = Base64.decode(new byte[]{110, 97, 29, 29, 42});
v2_1[v7] = Base64.decode(new byte[]{110, 113, 29, 29, 42});
v2_1[6] = Base64.decode(new byte[]{110, 71, 29, 29, 42});
v2_1[7] = Base64.decode(new byte[]{110, 87, 29, 29, 42});
v2_1[8] = Base64.decode(new byte[]{111, 97, 29, 29, 42});
v2_1[9] = Base64.decode(new byte[]{111, 113, 29, 29, 42});
String[] v4 = new String[v8];
v4[0] = Base64.decode(new byte[]{20, 80, 103, 80, 42});
v4[1] = Base64.decode(new byte[]{20, 80, 103, 71, 42});
v4[v9] = Base64.decode(new byte[]{20, 80, 103, 72, 42});
v4[3] = Base64.decode(new byte[]{20, 80, 103, 73, 42});
v4[4] = Base64.decode(new byte[]{20, 80, 103, 74, 42});
v4[v7] = Base64.decode(new byte[]{20, 80, 103, 75, 42});
v4[6] = Base64.decode(new byte[]{20, 80, 103, 76, 42});
v4[7] = Base64.decode(new byte[]{20, 80, 103, 77, 42});
v4[8] = Base64.decode(new byte[]{20, 80, 103, 78, 42});
v4[9] = Base64.decode(new byte[]{20, 80, 103, 79, 42});
while(v1 < v8) {
if(v1 == 0) {
v0_1 = v3.toString().replace(v2_1[v1], v4[v1]);
}
v0_1 = v0_1.replace(v2_1[v1], v4[v1]);
++v1;
}
return v0_1;
}
虽然都用了byte作为混淆方法之一,但并不影响解密,只需要把两个数组的顺序调换即可,即
if(v1 == 0) {
v0_1 = v3.toString().replace(v2_1[v1], v4[v1]);
}
v0_1 = v0_1.replace(v2_1[v1], v4[v1]);
++v1;
替换成
if(v1 == 0) {
v0_1 = v3.toString().replace(v4[v1], v2_1[v1]);
}
v0_1 = v0_1.replace(v4[v1], v2_1[v1]);
++v1;
结果就是1-9阿拉伯数字替换成①-⑨,0替换成⑩
这里加密的是a的值,a是7位随机数
下一层判断的是a1的值,a1的值是a的值+8,以下的bcde与a相同,代码如下
this.b = ((int)(Math.random() * (((double)10000000))));
this.c = ((int)(Math.random() * (((double)10000000))));
this.d = ((int)(Math.random() * (((double)10000000))));
this.e = ((int)(Math.random() * (((double)10000000))));
this.a1 = ((long)(this.a + 8));
this.b1 = ((long)(this.b - 1));
this.c1 = ((long)(this.c - 4));
this.d1 = ((long)this.d);
this.e1 = ((long)this.e);
根据上面的代码我们可以看到除了最后一层的密码要进一步计算,其余的都与第二层类似,这里就不提了
来着重看一下最后一层,为什么我给它重命名为MD5加密呢。我们先看一下代码然后再分析一下
public static String encode(String arg4) {
NoSuchAlgorithmException v0_1;
String v1_1;
int v0;
try {
new String(arg4);
v0 = 5;
}
catch(NoSuchAlgorithmException v1) {
NoSuchAlgorithmException v3 = v1;
v1_1 = ((String)v0);
v0_1 = v3;
goto label_15;
}
try {
return MD5.Byte_String(MessageDigest.getInstance(Base64.decode(new byte[]{116, 117, 113, 17, 42})).digest(arg4.getBytes()));
}
catch(NoSuchAlgorithmException v0_1) {
}
label_15:
v0_1.printStackTrace();
return v1_1;
}
它最终是调用了MessageDigest.getinstance函数返回的数据,这个函数可以返回MD5,SHA等加密算法的值。
要值得注意的是,在生成MD5值的同时,把Byte转换成了String,而这个转换与平时我们用到的定义不同,会造成0-9变为9-0
如果完全复制粘贴到IDE中可能会出错,所以要把代码进行改写。
最终结果就是把e1的值MD5加密后把0-9替换成9-0就是密码
这样整个病毒就分析完了
总结
对于这种混淆,我们可以根据代码的关键词推测该方法的作用,依照自己的习惯命名,这样病毒作者本身引以为傲的阿拉伯加密手段就失效了,对于把String字符串全部由new Byte[]来代替只能增加可读性的难度,对于解密本身并不能造成根本性影响。在分析时,一定要注意阅读顺序,避免造成不必要的麻烦。
Todo: 因为宿主程序加壳,无法判断是否调用了Root权限,实机测试并没有弹出申请Root权限的弹窗。来一个大佬解惑
最后,不要贪图小便宜,不要使用各种未知来源的软件。切记!!!