本篇文章会通过一个案例对逆向破解的流程进行详细的解说
- 通过本篇文章你会知道逆向的一个简单流程
- 学会简单的逆向开发
下面是公司内部的一个打卡系统,smali修改的有两个地方
- 打卡的位置信息
-
右上角的验证按钮,公司的打卡系统逻辑是 用手机打卡之后必须使用 web 端或者电脑端登录进行验证,所以通过smali新增了一个界面进行web端验证
说到Android的逆向必须要了解的一个知识就是 smali
为什么不直接提取java代码修改?
- 因为apk反编译成java代码后是不完整的不能直接运行,原因上面也说过,就是android会把class打包成 dex文件并对代码进行优化,所以是无法还原成java代码的
什么是smali ?
Android程序用Java语言开发APP,编译工具会将Java源文件(.java)编译成Dalvik可执行文件(.dex)。Android系统中Dalvik Virtual Machine 会执行该文件。smali/baksmali则是Dalvik VM可执行文件的汇编器/反汇编器。反汇编Dalvik可执行文件(.dex)后,将会得到.smali后缀文件。smali代码拥有特定的语法。相比于.dex文件,smali文件的语法更容易理解些。
-
.dex文件和.smali文件可以通过smali/baksmali工具进行相互转换。而.smali文件无法完整转化成.java文件,可能是由于Android SDK 中dx工具将.java文件的字节码.class文件转换成.dex文件的过程中,进行了重新排列,去除了多余的信息,虽然提高了.dex文件的执行效率,却也丢失了信息,无法完全转化回去。
说了那么多现在开始案例的讲解
-
首先确定要逆向的APP,通过ApkTool反编译apk
//apktool是一个jar文件所以通过java运行jar的方式 java -jar apktool.jar d com.sejian.apk
反编译完成之后会在当前目录生成一个以当前apk名字的文件夹,文件夹内容结构如下(如果没有对应的内容,或者文件中的内容为空,表示反编译失败,apktool不支持)
smali 代码是获取到了,但是我们也看不懂smali啊,也不好看,所以我们还需借用一个工具dex2jar 用来提取apk中java代码,我们修改smali就通过分析java代码定位代码位置
/d2j-dex2jar.sh ../aa.apk
-
开始分析代码找到修改smali的位置
1、 首先用手机打开 考勤打卡的界面,然后使用 android adb 工具查看当前的Activity信息定位到具体的类
```bash
# 列出所有的activity信息,这样会出现当前手机所有的任务栈信息,
# 太多不好定位,可以通过下面的命令定位到具体的 activity
adb shell dumpsys activity activities
adb shell dumpsys activity activities | grep mFocusedActivity
```
通过adb操作我们定位到了com.hhly.RTX/.activity.PunchCardActivity 这个Activity,现在我们就可以打开前面提取的java代码进行分析了,因为我们提取的是class文件所以我们还需借助一个工具 jd-gui来分析java代码
java -jar jd-gui-1.4.0.jar
(jd-gui代码的搜索功能不怎么好用我们可以保存到本地通过其他编辑器进行搜索)
代码是找到了,我们怎么去找这个按钮呢?可以通过搜索 关键子,submit request 之类的操作,但是这样不好定位, 我们可以 打开之前用apktool反编译之后的目录里面有一个res文件夹,找到对应的布局文件,定位到按钮的id
// 通过一系列的查找我们找到了 按钮的点击事件
this.btn_repunch.setOnClickListener(new View.OnClickListener(){
public void onClick(View paramAnonymousView){
new CustomDialog.Builder(paramAnonymousView.getContext()).setMessage("再次打卡将重新计算打卡时间哦~").setNegativeButton(PunchCardActivity.this.getString(2131231000),0,new DialogInterface.OnClickListener(){
public void onClick(DialogInterface paramAnonymous2DialogInterface,int paramAnonymous2Int){
paramAnonymous2DialogInterface.dismiss();
}
}).setPositiveButton(PunchCardActivity.this.getString(2131230933),0,
new DialogInterface.OnClickListener(){
public void onClick(DialogInterface paramAnonymous2DialogInterface,int paramAnonymous2Int){
paramAnonymous2DialogInterface.dismiss();
paramAnonymous2DialogInterface=new HashMap();
paramAnonymous2DialogInterface.put("Check_Type","Check_Again");
MobclickAgent.onEvent(PunchCardActivity.this,"Check_Type",paramAnonymous2DialogInterface);
// 检查位置信息并提交打卡记录
PunchCardActivity.this.checkLocationInfoAndSubmitPunch();
}
}).create().show();
}
});
private void checkLocationInfoAndSubmitPunch(){
LatLng localLatLng = this.currentPt;
String str = this.currentPoiAddr;
if ((localLatLng != null) && (TextUtils.isEmpty(str)))
{
getPOIInfo(localLatLng);
return;
}
// 提交打卡记录
requestSubmitPunch(localLatLng, str);
}
private void requestSubmitPunch(LatLng paramLatLng, String paramString){
String str1 = SpTools.getString(getApplication(), "token2", "");
String str2 = SpTools.getString("userId2");
String str3 = getMacAddress(this);
String str4 = getImei(this);
String str5 = UniversalID.getUniversalID(this);
try
{
LatLng localLatLng = new LatLng(Double.valueOf(localConfig.getLatitude()).doubleValue(), Double.valueOf(localConfig.getLongitude()).doubleValue());
d3 = d1;
localObject4 = localObject1;
// 判断是否在规定的范围内打卡
boolean bool = SpatialRelationUtil.isCircleContainsPoint(localLatLng, localConfig.getAttendRange(), paramLatLng);
d2 = d1;
localObject3 = localObject1;
if (bool)
{
d3 = d1;
localObject4 = localObject1;
d4 = DistanceUtil.getDistance(localLatLng, paramLatLng);
if (d1 == 0.0D) {
break label565;
}
d2 = d1;
localObject3 = localObject1;
if (d4 < d1) {
break label565;
}
}
}
catch (Exception localException)
{
}
// 封装一系列的参数
ShowToast.mloading(this, false, getString(2131230742), getResources().getColor(2131689566));
localObject3 = new JSONObject();
((JSONObject)localObject3).put("userId", str2);
((JSONObject)localObject3).put("configId", Long.valueOf(((PunchConfig.Config)localObject2).getConfigId()));
((JSONObject)localObject3).put("address", paramString);
((JSONObject)localObject3).put("longitude", String.valueOf(paramLatLng.longitude));
((JSONObject)localObject3).put("latitude", String.valueOf(paramLatLng.latitude));
((JSONObject)localObject3).put("os", Integer.valueOf("3"));
((JSONObject)localObject3).put("mac", str3);
((JSONObject)localObject3).put("imei", str4);
((JSONObject)localObject3).put("code", str5);
// ......省略发起网络请求
}
我们通过分析代码发现有效距离的判断是这句代码
boolean bool = SpatialRelationUtil.isCircleContainsPoint(localLatLng, localConfig.getAttendRange(), paramLatLng);
找到代码之后就好修改了,我们可以 修改localLatLng这个变量的值,把它固定为一个公司有效范围内的值。 我在处理的时候并不是修改的这个位置,因为我通过分析代码发现定位用的是百度定位,并且封装了一个定位工具类,内部封装了一个 LocationInfo 看名字我们就知道这个是一个定位信息javabean, 我们通过代码可以知道定位成功之后会把定位信息包装成一个 LocationInfo返回给调用者,
这里就有两个修改点
- onReceiveLocation 中 LocationInfo赋值的时候
- LocationInfo 的get set方法中
我这里修改的是 get方法中,至于我为什么修改的是这,下面讲!!!!
public class BaiduSdkHelper {
private static BaiduSdkHelper mInstance = new BaiduSdkHelper();
private LocationListener locationListener;
private LocationService locationService;
private BDLocationListener mListener = new BDLocationListener() {
public void onConnectHotSpotMessage(String paramAnonymousString, int paramAnonymousInt) {
}
public void onReceiveLocation(BDLocation paramAnonymousBDLocation) {
if (BaiduSdkHelper.this.locationListener != null) {
BaiduSdkHelper.LocationInfo localLocationInfo = new BaiduSdkHelper.LocationInfo();
localLocationInfo.setAddr(paramAnonymousBDLocation.getAddrStr());
localLocationInfo.setCity(paramAnonymousBDLocation.getCity());
localLocationInfo.setCountry(paramAnonymousBDLocation.getCountry());
localLocationInfo.setDistrict(paramAnonymousBDLocation.getDistrict());
localLocationInfo.setStreet(paramAnonymousBDLocation.getStreet());
localLocationInfo.setLatitude(paramAnonymousBDLocation.getLatitude());
localLocationInfo.setLongitude(paramAnonymousBDLocation.getLongitude());
BaiduSdkHelper.this.locationListener.onReceiveLocation(bool1, localLocationInfo);
}
}
};
public static BaiduSdkHelper get() {
return mInstance;
}
public void registerListener(LocationListener paramLocationListener) {
if (this.locationService != null) {
this.locationService.registerListener(this.mListener);
}
this.locationListener = paramLocationListener;
}
public void startLocation() {
if (this.locationService != null) {
this.locationService.start();
}
}
public void stopLocation() {
if (this.locationService != null) {
this.locationService.stop();
}
}
public void unregisterListener() {
if (this.locationService != null) {
this.locationService.unregisterListener(this.mListener);
}
this.locationListener = null;
}
public static final class LocationInfo implements Serializable {
private String addr;
private String city;
private String country;
private String district;
private double latitude;
private double longitude;
private String street;
public String getAddr() {
return this.addr;
}
public String getCity() {
return this.city;
}
public String getCountry() {
return this.country;
}
public String getDistrict() {
return this.district;
}
public double getLatitude() {
return this.latitude;
}
public double getLongitude() {
return this.longitude;
}
public String getStreet() {
return this.street;
}
public class BaiduSdkHelper {
private static BaiduSdkHelper mInstance = new BaiduSdkHelper();
private LocationListener locationListener;
private LocationService locationService;
private BDLocationListener mListener = new BDLocationListener() {
public void onConnectHotSpotMessage(String paramAnonymousString, int paramAnonymousInt) {
}
public void onReceiveLocation(BDLocation paramAnonymousBDLocation) {
if (BaiduSdkHelper.this.locationListener != null) {
BaiduSdkHelper.LocationInfo localLocationInfo = new BaiduSdkHelper.LocationInfo();
localLocationInfo.setAddr(paramAnonymousBDLocation.getAddrStr());
localLocationInfo.setCity(paramAnonymousBDLocation.getCity());
localLocationInfo.setCountry(paramAnonymousBDLocation.getCountry());
localLocationInfo.setDistrict(paramAnonymousBDLocation.getDistrict());
localLocationInfo.setStreet(paramAnonymousBDLocation.getStreet());
localLocationInfo.setLatitude(paramAnonymousBDLocation.getLatitude());
localLocationInfo.setLongitude(paramAnonymousBDLocation.getLongitude());
BaiduSdkHelper.this.locationListener.onReceiveLocation(bool1, localLocationInfo);
}
}
};
public static BaiduSdkHelper get() {
return mInstance;
}
public void registerListener(LocationListener paramLocationListener) {
if (this.locationService != null) {
this.locationService.registerListener(this.mListener);
}
this.locationListener = paramLocationListener;
}
public void startLocation() {
if (this.locationService != null) {
this.locationService.start();
}
}
public void stopLocation() {
if (this.locationService != null) {
this.locationService.stop();
}
}
public void unregisterListener() {
if (this.locationService != null) {
this.locationService.unregisterListener(this.mListener);
}
this.locationListener = null;
}
public static final class LocationInfo implements Serializable {
private String addr;
private String city;
private String country;
private String district;
private double latitude;
private double longitude;
private String street;
public String getAddr() {
return this.addr;
}
public String getCity() {
return this.city;
}
public String getCountry() {
return this.country;
}
public String getDistrict() {
return this.district;
}
public double getLatitude() {
return this.latitude;
}
public double getLongitude() {
return this.longitude;
}
public String getStreet() {
return this.street;
}
public void setAddr(String paramString) {
this.addr = paramString;
}
public void setCity(String paramString) {
this.city = paramString;
}
public void setCountry(String paramString) {
this.country = paramString;
}
public void setDistrict(String paramString) {
this.district = paramString;
}
public void setLatitude(double paramDouble) {
this.latitude = paramDouble;
}
public void setLongitude(double paramDouble) {
this.longitude = paramDouble;
}
public void setStreet(String paramString) {
this.street = paramString;
}
}
public static abstract interface LocationListener {
public abstract void onReceiveLocation(boolean paramBoolean,
BaiduSdkHelper.LocationInfo paramLocationInfo);
}
}
public void setAddr(String paramString) {
this.addr = paramString;
}
public void setCity(String paramString) {
this.city = paramString;
}
public void setCountry(String paramString) {
this.country = paramString;
}
public void setDistrict(String paramString) {
this.district = paramString;
}
public void setLatitude(double paramDouble) {
this.latitude = paramDouble;
}
public void setLongitude(double paramDouble) {
this.longitude = paramDouble;
}
public void setStreet(String paramString) {
this.street = paramString;
}
}
public static abstract interface LocationListener {
public abstract void onReceiveLocation(boolean paramBoolean,
BaiduSdkHelper.LocationInfo paramLocationInfo);
}
}
-
开始smali的修改
- 找到 BaiduSdkHelper.smali文件(下面只保留了经纬度的get方法)
.class public final Lcom/hhly/baidusdk/BaiduSdkHelper$LocationInfo; .super Ljava/lang/Object; .source "BaiduSdkHelper.java" # interfaces .implements Ljava/io/Serializable; # 删除其他代码 # instance fields .field private addr:Ljava/lang/String; .field private city:Ljava/lang/String; .field private country:Ljava/lang/String; .field private district:Ljava/lang/String; .field private latitude:D .field private longitude:D .field private street:Ljava/lang/String; .method public getLatitude()D .locals 2 .prologue iput-wide v0, p0, Lcom/hhly/baidusdk/BaiduSdkHelper$LocationInfo;->latitude:D .line 177 iget-wide v0, p0, Lcom/hhly/baidusdk/BaiduSdkHelper$LocationInfo;->latitude:D return-wide v0 .end method .method public getLongitude()D .locals 2 .prologue iput-wide v0, p0, Lcom/hhly/baidusdk/BaiduSdkHelper$LocationInfo;->longitude:D .line 185 iget-wide v0, p0, Lcom/hhly/baidusdk/BaiduSdkHelper$LocationInfo;->longitude:D return-wide v0 .end method
- 找到smali之后我们就可以开始修改了,但是我们不会写smali ,怎么办呢? 我们可以写java代码通过一个idea的插件把java转换为smali,idea插件 然后再把smali复制到我们需要修改的地方
- smali 简单解释
# 这些应该都能理解,和java差不多
.class public final Lcom/hhly/baidusdk/BaiduSdkHelper$LocationInfo;
.super Ljava/lang/Object;
.source "BaiduSdkHelper.java"
# interfaces
.implements Ljava/io/Serializable;
# .field 属性声明
.field private latitude:D
# 声明一个方法 .method 方法的开始
.method public getLongitude()D
# 结束一个方法
.end method
.locals 2 # 用到的寄存器个数
. prologue # 表示方法 正文内容的开始,没什么实际作用
.line # 行号,调试用,删掉也啥关系
iput-object # 对象赋值
iget-object # 调用对象
return-wide v0 # 返回值
- 编写 java 代码 (上面说到我为什么 修改get方法,因为这样的话,我们只需创建一个简单的 javabean 就不需要其他的依赖,如果涉及到其他类的代码就不方便)
public class Entity {
double latitude;
double longitude;
public double getLatitude() {
latitude = 22.550431d;
//latitude = 39.926528;
return latitude;
}
public double getLongitude() {
longitude = 113.954007d;
//longitude = 116.403299;
return longitude;
}
}
- 把上面的代码通过idea插件转换为smali 得到如下smali代码,
.class public Lcom/libjpegcompress/activity/Entity;
.super Ljava/lang/Object;
.source "Entity.java"
# instance fields
.field addr:Ljava/lang/String;
.field latitude:D
.field longitude:D
# direct methods
.method public constructor ()V
.registers 1
.prologue
.line 11
invoke-direct {p0}, Ljava/lang/Object;->()V
return-void
.end method
.method public getLatitude()D
.registers 3
.prologue
.line 17
#
const-wide v0, 0x40368ce90bc7b45fL # 22.550431
iput-wide v0, p0, Lcom/libjpegcompress/activity/Entity;->latitude:D
.line 19
iget-wide v0, p0, Lcom/libjpegcompress/activity/Entity;->latitude:D
return-wide v0
.end method
.method public getLongitude()D
.registers 3
.prologue
.line 23
const-wide v0, 0x405c7d0e736049edL # 113.954007
iput-wide v0, p0, Lcom/libjpegcompress/activity/Entity;->longitude:D
.line 25
iget-wide v0, p0, Lcom/libjpegcompress/activity/Entity;->longitude:D
return-wide v0
.end method
```
** const-wide v0, 0x40368ce90bc7b45fL # 22.550431
const-wide v0, 0x405c7d0e736049edL # 113.954007
这就是我们修改值的代码, 我们只需要把这两句代码复制到BaiduSdkHelper$LocationInfo.smali文件中,下面是修改完成的代码**
```smali
.method public getLatitude()D
.locals 2
.prologue
const-wide v0, 0x40368ce90bc7b45fL # 22.550431
iput-wide v0, p0, Lcom/hhly/baidusdk/BaiduSdkHelper$LocationInfo;->latitude:D
.line 177
iget-wide v0, p0, Lcom/hhly/baidusdk/BaiduSdkHelper$LocationInfo;->latitude:D
return-wide v0
.end method
.method public getLongitude()D
.locals 2
.prologue
const-wide v0, 0x405c7d0e736049edL # 113.954007
iput-wide v0, p0, Lcom/hhly/baidusdk/BaiduSdkHelper$LocationInfo;->longitude:D
.line 185
iget-wide v0, p0, Lcom/hhly/baidusdk/BaiduSdkHelper$LocationInfo;->longitude:D
return-wide v0
.end method
到此经纬度修改完成!!!!!!!!!!!!!!!
-
创建右边菜单栏
在onCreate中调用此方法会生成对应的调用方式,如下:
invoke-virtual {p0}, Lcom/hhly/RTX/activity/WebviewActivity;->createRightView()V
我们在我们的smali文件中把调用方式复制到 对应的初始化方法中进行调用public void createRightView() { TextView textView = new TextView(this); textView.setText("验证"); textView.setTextColor(Color.WHITE); textView.setTextSize(17); FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(-2, -2); DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); params.topMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 31, displayMetrics); params.rightMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, displayMetrics); params.gravity = Gravity.RIGHT; textView.setLayoutParams(params); textView.setOnClickListener(new WebviewActivityMenuClick(this)); ViewGroup decorView = ((FrameLayout) getWindow().getDecorView()); decorView.addView(textView); }
.method public createRightView()V .registers 8 .prologue const/4 v6, 0x1 const/4 v5, -0x2 new-instance v3, Landroid/widget/TextView; invoke-direct {v3, p0}, Landroid/widget/TextView;->
(Landroid/content/Context;)V .local v3, "textView":Landroid/widget/TextView; const-string v4, "\u9a8c\u8bc1" invoke-virtual {v3, v4}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V const/4 v4, -0x1 invoke-virtual {v3, v4}, Landroid/widget/TextView;->setTextColor(I)V const/high16 v4, 0x41880000 # 17.0f invoke-virtual {v3, v4}, Landroid/widget/TextView;->setTextSize(F)V new-instance v2, Landroid/widget/FrameLayout$LayoutParams; invoke-direct {v2, v5, v5}, Landroid/widget/FrameLayout$LayoutParams;-> (II)V .local v2, "params":Landroid/widget/FrameLayout$LayoutParams; invoke-virtual {p0}, Lcom/hhly/RTX/activity/WebviewActivity;->getResources()Landroid/content/res/Resources; move-result-object v4 invoke-virtual {v4}, Landroid/content/res/Resources;->getDisplayMetrics()Landroid/util/DisplayMetrics; move-result-object v1 .local v1, "displayMetrics":Landroid/util/DisplayMetrics; const/high16 v4, 0x41f80000 # 31.0f invoke-static {v6, v4, v1}, Landroid/util/TypedValue;->applyDimension(IFLandroid/util/DisplayMetrics;)F move-result v4 float-to-int v4, v4 iput v4, v2, Landroid/widget/FrameLayout$LayoutParams;->topMargin:I const/high16 v4, 0x41200000 # 10.0f invoke-static {v6, v4, v1}, Landroid/util/TypedValue;->applyDimension(IFLandroid/util/DisplayMetrics;)F move-result v4 float-to-int v4, v4 iput v4, v2, Landroid/widget/FrameLayout$LayoutParams;->rightMargin:I const/4 v4, 0x5 iput v4, v2, Landroid/widget/FrameLayout$LayoutParams;->gravity:I invoke-virtual {v3, v2}, Landroid/widget/TextView;->setLayoutParams(Landroid/view/ViewGroup$LayoutParams;)V new-instance v4, Lcom/hhly/RTX/activity/WebviewActivityMenuClick; invoke-direct {v4, p0}, Lcom/hhly/RTX/activity/WebviewActivityMenuClick;-> (Landroid/app/Activity;)V invoke-virtual {v3, v4}, Landroid/widget/TextView;->setOnClickListener(Landroid/view/View$OnClickListener;)V # java转smali的时候注意包名,要改成你 所需要修改的smali文件所在的类,我的是这个 com/hhly/RTX/activity/PunchCardActivity; invoke-virtual {p0}, Lcom/hhly/RTX/activity/WebviewActivity;->getWindow()Landroid/view/Window; move-result-object v4 invoke-virtual {v4}, Landroid/view/Window;->getDecorView()Landroid/view/View; move-result-object v0 check-cast v0, Landroid/widget/FrameLayout; .local v0, "decorView":Landroid/view/ViewGroup; invoke-virtual {v0, v3}, Landroid/view/ViewGroup;->addView(Landroid/view/View;)V return-void .end method
-
创建 验证 按钮对应的webview界面, 创建完成之后也是通过 idea插件转换成对应的smali文件,直接把文件复制到对应的包下(随便哪个包,只要调用的时候注意改包名和smali文件中的包声名)
```java
public class WebviewActivity extends AppCompatActivity {
private WebView view;
@Override protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
view = new WebView(this);
setContentView(view);
WebSettings settings = view.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
settings.setDatabaseEnabled(true);
settings.setGeolocationEnabled(true);
view.setWebViewClient(new WebViewClient() {
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
return super.shouldInterceptRequest(view, request);
}
});
view.loadUrl("file:///android_asset/valid.html");
view.addJavascriptInterface(this, "webview");
createRightView();
}
@JavascriptInterface public void toast(String msg) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
}
```
修改完成之后我们就可以通过 apktool 重打包了
java -jar apktool.jar b ./71ant_setup/
等待打包完成之后我们还需要对apk重新进行签名
jarsigner -keystore debug.keystore MyApp.apk androiddebugkey
到此 Android简单逆向分析到此结束
总结
1. 如果 apk 被加固,apktool不一定可以反编译
2. 如果apk被混淆或者逻辑复杂,我们逆向的工作难度就会大大的提高
3. 我们在修改smali的时候需要特别 注意 smali中的 寄存器不要写错了,
因为我们是通过java代码 编译成 smali的,所以和源smali中的会有一些出入
4. 写的不好地方,请见谅,我也菜,O(∩_∩)O哈哈~
更多博客内容请关注:http://boke.liwg.top/