本篇文章會通過一個案例對逆向破解的流程進行詳細的解說
- 通過本篇文章你會知道逆向的一個簡單流程
- 學會簡單的逆向開發
下面是公司內部的一個打卡系統,smali修改的有兩個地方
- 打卡的位置信息
-
右上角的驗證按鈕,公司的打卡系統邏輯是 用手機打卡之后必須使用 web 端或者電腦端登錄進行驗證,所以通過smali新增了一個界面進行web端驗證
逆向.gif
說到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文件的執行效率,卻也丟失了信息,無法完全轉化回去。
image
說了那么多現在開始案例的講解
-
首先確定要逆向的APP,通過ApkTool反編譯apk
//apktool是一個jar文件所以通過java運行jar的方式 java -jar apktool.jar d com.sejian.apk
反編譯完成之后會在當前目錄生成一個以當前apk名字的文件夾,文件夾內容結構如下(如果沒有對應的內容,或者文件中的內容為空,表示反編譯失敗,apktool不支持)
imagesmali 代碼是獲取到了,但是我們也看不懂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 <init>()V
.registers 1
.prologue
.line 11
invoke-direct {p0}, Ljava/lang/Object;-><init>()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;-><init>(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;-><init>(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;-><init>(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/