分析一個安卓簡單CrackMe
我們把apk拖入模擬器,然后打開:

隨意輸入一串密碼點擊輸入密碼試試。可以看到,提示我們驗證碼校驗失敗:

我們打開jeb進行分析,直接把apk拖進去:

聊聊jeb的使用,拖入apk即可進行自動反編譯。
下面是反編譯出來的代碼,也就是dex文件反編譯后的相關代碼:

這個就是相關的一些資源文件:


配置清單里面存放有apk的相關配置信息,例如activity,service,都在這里面。
下面這個就是代碼區了,雙擊ByteCode即可進入,不過jeb會默認給你打開ByteCode這個字節碼區。


下面這個字符串就是一些代碼中出現的方法,類所在路徑,還有傳參時的字符串。


我們順著之前的驗證碼校驗失敗的Toast彈窗來找到相關的邏輯。
在屏幕上顯示一段文字,沒別的東西的話,那這個一般就是Toast彈窗了:

我們右鍵,點擊如下箭頭所在的位置,這個就是查找引用,看那個方法引用了這個字符串。

點擊后,我們可以看到相關引用地方:

我們雙擊顯示出來的引用,來到如下位置:

我們按一下tab鍵,轉為java代碼:

我們把代碼復制出來,然后進行分析:
package com.yaotong.crackme;import android.app.Activity; import android.content.Intent;import android.os.Bundle; import android.view.View.OnClickListener;import android.view.View; import android.widget.Button;import android.widget.EditText;import android.widget.Toast; public class MainActivity extends Activity {public Button btn_submit; public EditText inputCode; static { System.loadLibrary("crackme"); } @Override // android.app.Activity //說明這個方法重寫了 protected void onCreate(Bundle arg3) { //protected super.onCreate(arg3); this.setContentView(0x7F030000); // layout:activity_main this.getWindow().setBackgroundDrawableResource(0x7F020000); // drawable:bg this.inputCode = (EditText)this.findViewById(0x7F060000); // id:inputcode this.btn_submit = (Button)this.findViewById(0x7F060001); // id:submit this.btn_submit.setOnClickListener(new View.OnClickListener() { @Override // android.view.View$OnClickListener public void onClick(View arg6) { if(MainActivity.this.securityCheck(MainActivity.this.inputCode.getText().toString())) { MainActivity.this.startActivity(new Intent(MainActivity.this, ResultActivity.class)); return; } Toast.makeText(MainActivity.this.getApplicationContext(), "驗證碼校驗失敗", 0).show(); } }); } public native boolean securityCheck(String arg1) { }}package com.yaotong.crackme; //包路徑,也就是這個MainActivity所在的路徑,寫上這個后, //com.yaotong.crackme這個里面的所有相關類都可以在MainActivity里使用了//package為關鍵字,固定寫法,后面為你的當前類所在的文件夾,也就是包//下面這些就是導入這個類里面的相關方法需要用到的包//import為固定寫法,一個關鍵字,我們不能用這個關鍵字給變量取名字,//import 后面跟你的類中要用到的方法所在的包就行import android.app.Activity; //活動 activity相關方法import android.content.Intent; //意圖 Intent 相關方法import android.os.Bundle; //Bundle相關方法import android.view.View.OnClickListener; //OnClickListener 點擊事件相關方法import android.view.View; //View 視圖相關方法import android.widget.Button; //Button 按鈕相關方法import android.widget.EditText;//EditText 編輯框相關方法import android.widget.Toast;//Toast彈窗相關方法public class MainActivity extends Activity { //定義一個類為MainActivity 繼承了Activity ,Activity //里面的相關方法,變量,在MainActivity 里面都可以使用//extends 為繼承的意思 后面跟一個類//public class 為固定寫法,前面的public 可以換,可以用private,protected//public ,private,protected,這些為權限限定符,可以限定你類,變量或者方法不讓其他類訪問,}public Button btn_submit; //定義一個按鈕對象,名字為btn_submit ,權限為publicpublic EditText inputCode;//定義一個編輯框對象,名字為inputCode 權限為pubilc static { System.loadLibrary("crackme"); //static 靜態代碼塊,當MainActivity對象創建時,首先執行里面的代碼,//System是一個對象,打點.調用loadLibrary方法,傳入參數crackme,//意思為加載so庫,名字為crackme前后省略lib,.so,這個文件我在之前有說,//是一個c\c++編寫的文件,需要jni才能調用//loadLibrary方法在System里面 } @Override // android.app.Activity //說明這個方法重寫了 protected void onCreate(Bundle arg3) { //權限為protected,返回值為空 方法名為onCreate,//(Bundle arg3)這個為參數,參數類型為Bunldle,名字為arg3 super.onCreate(arg3);//super.onCreate(arg3),調用父類的onCreate方法,傳入的參數為arg3,這個 //arg3也就是上面那個protected權限修飾的一個方法//父類也就是MainActivity繼承的那個activity類,也就是說調用activity類里面的onCreate方法 this.setContentView(0x7F030000); // layout:activity_main//這個setContentView為設置布局的一個方法,布局文件的id為0x7F030000//這個id為 layout:activity_main,后面會說明id所在位置,這個id是開發工具自動為我們生成的//this指的是當前所在類的對象,也就是MainActivity實例化后的對象//為什么這個類中沒有setContentView方法也能調用呢?//因為MainActivity這個類繼承了activity類 this.getWindow().setBackgroundDrawableResource(0x7F020000); // drawable:bg//getWindow().setBackgroundDrawableResource設置窗口背景為 drawable目錄下的bg.png圖片//getWindow()返回一個對象,然后打點.調用setBackgroundDrawableResource()方法,傳入R文件中的id//R文件在后面我截圖了,R類中,會保存resource文件中的每一個信息,產生id,這樣我們的代碼才能引用他 this.inputCode = (EditText)this.findViewById(0x7F060000); // id:inputcode//調用findViewById方法,傳入inputcode 在R文件中的id,返回的是一個view對象,//findViewById這個方法的意思是通過id來查找相關視圖//我們需要把他轉為EditText對象,因為這個對象實際上是一個編輯框,(EditText)這個就是強轉//強轉可以把父類轉為子類,可以把int數據類型轉為double類型等//然后把EditText對象賦值給在前面定義的public 權限的EditText對象 this.btn_submit = (Button)this.findViewById(0x7F060001); // id:submit//調用findViewById方法,傳入submit在R文件中的id,返回的是一個view對象,//findViewById這個方法的意思是通過id來查找相關視圖//我們需要把他轉為Button對象,因為這個對象實際上是一個編輯框,(Button)這個就是強轉//強轉可以把父類轉為子類,可以把int數據類型轉為double類型等//然后把Button對象賦值給在前面定義的public 權限的Button對象this.btn_submit.setOnClickListener(new View.OnClickListener() { //給按鈕btn_submit綁定一個監聽事件,setOnClickListener就是設置點擊事件//這個setOnClickListener方法需要一個OnClickListener的實現類//在這里用的是匿名內部類的方式實現的//這個OnClickListener在View類中,所以前面要加View,然后打點 @Override // android.view.View$OnClickListener// @Override 說明這個方法是一個重寫方法,這個onClick方法在OnClickListener里面//這個方法是一個抽象方法,需要我們自己實現 public void onClick(View arg6) { //這個方法前面都是固定的 public void onClick(View arg6) {}//里面的內容需要我們自己寫,在這里面我們可以看到相關的邏輯//下面我分開講這個校驗邏輯 if(MainActivity.this.securityCheck(MainActivity.this.inputCode.getText().toString())) { MainActivity.this.startActivity(new Intent(MainActivity.this, ResultActivity.class)); return; } Toast.makeText(MainActivity.this.getApplicationContext(), "驗證碼校驗失敗", 0).show(); } }); } if(MainActivity.this.securityCheck(MainActivity.this.inputCode.getText().toString())) { MainActivity.this.startActivity(new Intent(MainActivity.this, ResultActivity.class)); return; } Toast.makeText(MainActivity.this.getApplicationContext(), "驗證碼校驗失敗", 0).show(); } }); }//if(){}固定寫法 ()這里面寫相關的返回值為真或者為假的代碼,//比如==,> < <= >=等,或者寫一個方法,//這個方法的返回值為true或者false即可//MainActivity.this.securityCheck(MainActivity.this.inputCode.getText().toString())//為什么是MainActivity.this呢?一個我們新建了一個類,//如果我們this放前面的話,就調用的是我們的實//現類里面的方法,也就是說只有onClick方法可以被調用。MainActivity.this//就是指定在MainActivity里面方法,類的話,是調用不到,我們只能new一個對象//也就是新建一個對象,創建對象的寫法,new 類名();如果括號里面沒有寫值,那么調用的是無參構造方法//構造方法沒有返回值,例如public 類名(){}這個就是一個無參構造方法,如果里面寫參數,比如基本數據類型//int double float long 等,還有其他的數據類型也就是引用數據類型,比如對象,放一個接口也行,只不過我們要給他傳入一個實現類
基本數據類型

引用數據類型

//然后調用了securityCheck,傳入了一個參數MainActivity.this.inputCode.getText().toString()//這個參數是String類型的,這個inputCode就是那個編輯框對象,也就是crackme軟件的那個輸入框,//調用了getText().toString()方法,getText返回一個對象,然后用這個對象調用方法,返回一個String //如果這個securityCheck方法返回值為true那么執行下面這個startActivity方法MainActivity.this.startActivity(new Intent(MainActivity.this, ResultActivity.class));//這個方法會開啟新的activity,傳入的參數是一個intent,//這個intent傳入的參數第一個為當前MainActivity//第二個參數為要開啟的activity的類,.class就是類//調用完這個開啟activity方法后,執行 return;這個就是結束當前方法,//如果這個securityCheck方法返回值為false的話,那么就執行下面的Toast彈窗 Toast.makeText(MainActivity.this.getApplicationContext(), "驗證碼校驗失敗", 0).show();//調用Toast對象里面的makeText方法,傳入三個參數,第一個為上下文,也就是context,這個//MainActivity.this.getApplicationContext()的返回值為一個上下文,第二個參數為我們想要顯示的字符串,//第三個參數為顯示多長時間
我們看一下新開啟的activity:

package com.yaotong.crackme; import android.app.Activity;import android.os.Bundle;import android.widget.TextView; public class ResultActivity extends Activity { @Override // android.app.Activity public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); TextView tv = new TextView(this); tv.setText("Congratulations!!!You Win!!"); this.setContentView(tv); }}
很多代碼我都在前面詳細講了,這里我講一下沒講的代碼,new TextView(this),這個就是創建一個TextView對象,傳入的參數為this,調用的是有參構造。
返回的對象用tv變量保存,TextView為這個變量的引用數據類型。然后用tv調用了setText方法,傳入一個字符串String類型的參數,這里面的意思
大概是恭喜你,成功了。
然后調用setContentView()方法傳入tv參數,這樣就能把這個TextView顯示到界面上。在界面顯示的對象實際上是一個view對象,是所有控件的父類,因此我們可以傳入textview。
this指的是當前activity,也就是ResultActivity ,setContentView方法在繼承activity后才能使用,也就是 extends Activity。
接下來我們看看這個securityCheck方法:
public native boolean securityCheck(String arg1) { }
這個方法是一個native方法,說明這個方法的邏輯在so層,也就是libcrackme.so這個文件里面。
下面我們要用到的工具是ida,因為ida可以很好的分析so。
我們首先要解壓apk,然后獲得如下文件:

這個so文件在lib文件夾里。
這里擴展一下文件結構,lib是存放一些so文件的,這個so文件由ndk生成
C\C++語言開發,在java層無法直接調用,但是可以通過jni間接調用
我們只要聲明這個方法為native方法,就可以調用了,但前提是,在so中有相關這個方法的實現。
META-INF這個是存放簽名的,AndroidMainfest.xml這個文件是存放相關的配置信息,比如activity,service,包名,是否全屏,標題,apk的圖標等,都可以在這里進行設置。
Classes.dex這個是相關的java代碼轉成了class字節碼文件。
為什么不直接是.class后綴呢?因為版權問題,所以谷歌自己寫了個編譯轉換方式,直接轉為dex文件,不直接轉為class文件,同時也為了方便,因為你要編寫大量的類,那么就會生成大量的class字節碼文件,這樣也不方便,所以直接合在一起了。
resources.arsc這里面存放了相關資源的索引,比如布局文件里面id,他的相關索引會出現在resources.arsc里面,詳細參考下面。
布局文件的id,按鈕視圖,編輯框視圖的id所在位置,這個id每當你在Resources文件中聲明時都會自動生成。


Resource資源產生相關的id信息都存儲在R類下:



這些是權限限定符的詳解:

我們進入存放so文件的lib文件夾,可以看到這里面還有一層文件夾,armeabi,這個代表這個so文件是arm架構的。用于運行在arm的手機中:

我們把這個文件拖入ida里面,這里我們默認即可,然后點擊ok
然后出現提示,我們點擊ok。


如果不想再彈出這個提示,我們可以勾選下面這個框框。

函數相關的都在這里:

我們找到那個securityCheck方法在so層的實現。我們可以看到有securityCheck函數,這個其實就是check方法的實現了。

Java_com_yaotong_crackme_MainActivitysecurityCheck
Java代表這是一個java層的,com yaotong crackme 這個就是包名
MainActivity就是activity。
SecurityCheck這個就是在java中的方法名,合起來就是在java層有個securityCheck方法,他在MainActivity類里,這個類在com.yaotong.crackme包下。中間我們換成就是在so層的函數名了
我們只傳了一個參數,為什么這個函數有三個參數呢?
int __fastcall Java_com_yaotong_crackme_MainActivity_securityCheck(int a1, int a2, int a3)
實際上有兩個參數ida未識別,一個JNIEnv *, 還有一個是jclass,第一個是JNI環境的指針,第二個是java的類。
我們可以導入安卓jni開發的相關頭文件,然后修改類型,ida就會自動為我們識別代碼。
導入方式如下:

然后我們選擇jni的頭文件:

然后會提示我們解析成功:

下面我們修改參數類型,前兩個參數是我之前講的那兩個參數,一個JNIEnv*,一個jclass。
對著第一個int類型右鍵,點擊Set lvar type:

然后我們把類型寫上,JNIEnv*,點擊ok。

第二個參數,同樣右鍵,然后點擊set。

輸入jclass,點ok。

我們可以看到效果,未識別前:

識別后:


第三個變量的類型不應該是int類型,而是string類型,我們可以在string前面加個j。
代碼java的string類型,也就是jstring:

我們可以改改變量名字,方便閱讀。對著變量名右鍵,也就是a1,然后選擇箭頭所在的選項,Rename:

第一個參數是JNIEnv*,所以我們把他取名為env,這個名字可以任意,只是我們方便閱讀。輸入要改的名字后,我們點擊ok。

重復前面的操作:


因為class是關鍵字,所以會報錯,我們應該在前面加個_


重復前面的操作,右鍵,選擇Rename。
這個是改好后的函數
int __fastcall Java_com_yaotong_crackme_MainActivity_securityCheck(JNIEnv *env, jclass _class, jstring string)

可以看到方便閱讀多了,當然我們也可以右鍵選擇這個。

我們選擇JNIEnv,第一個的是類型名,第二個是這個JNIEnv結構體的聲明,
第三個是占用內存大小。可以看到第二個結構體聲明里面又有一個結構體指針,并且是const修飾,因此這個*functions不可修改,struct類型名,說明這是一個結構體類型,后面的是這個結構體,functions是這個結構體的指針,const,struct,都是固定寫法。
指針相關知識:

這里我說一下結構體是什么,怎么定義一個結構體。
struct person{char *name;//姓名,char類型的指針,里面可以存放字符int age;//年齡 int類型,存放整型變量char *sex;//性別 char類型的指針,里面可以存放字符}定義格式struct xxx{這里面寫數據類型;}下面是其中一種定義格式,也是常見的一種定義格式#include int main() { struct person { char* name;//姓名 int age;//年齡 char* sex;//性別 }; struct person Person; Person.name = "starry"; Person.age = 2; Person.sex = "男"; printf("%s %d %s", Person.name, Person.age, Person.sex);}struct person Person;//struct person是數據類型,類似int一樣,Person是變量名//struct person是一個整體,Person.name = "starry";//給name變量賦值為starryPerson.age = 2;//給age變量賦值為2Person.sex = "男";//給sex變量賦值為男printf("%s %d %s", Person.name, Person.age, Person.sex); //分別打印name,age,sex//%s是指與char類型匹配, %d是與整型匹配,//%s %d %s 分別對應Person.name, Person.age, Person.sex
這個只是其中一種寫法,也是常見的一種寫法。
如下是運行圖:

我們主要分析下面這個代碼:

v5 = env->functions->GetStringUTFChars(env, string, 0);//把我們在java層傳入的字符串轉成c類型的char,//env是一個結構體指針,如果是指針那我們就要用->箭頭這種形式來訪問里面變量,函數等.//env->functions,這個functions實際上還是一個結構體指針所以又有一個->箭頭//最后這個就是functions結構體指針里面的函數了GetStringUTFChars//這個函數有三個參數,第一個是env,第二個是string,第三個是0//我演示一下結構體指針 #include //包含一個系統頭文件iostreamusing namespace std;//使用命名空間 using namespace為固定寫法 int main() { struct functions { void PriStr(int a,double b,const char *c) { cout << a << b << c << endl; } }; struct JNIEnv { functions* fu; }; JNIEnv JNIENv; JNIEnv* JNI=&JNIENv; functions fun; JNI->fu = &fun; JNI->fu->PriStr(66, 77, "hello");} //在這里我定義了兩個結構體struct functions和struct JNIEnvstruct functions結構體里有函數,返回值為void類型,參數有三個,//類型分別為int,double,const char*//struct JNIEnv這個結構體里面保存了struct functions結構體的指針//擴展:在c++中結構體前面可以省略struct,//也就是說沒必要這樣來定義一個結構體變量struct JNIEnv JNIENv;JNIEnv JNIENv;//定義一個JNIEnv結構體,名字為JNIENvJNIEnv* JNI=&JNIENv;//定義一個JNIEnv結構體指針,&JNIENv為取JNIENv變量的地址functions fun;//定義一個fun結構體變量JNI->fu = &fun;//因為JNI是一個結構體指針,所以我們不能打.點來獲取結構體里面的相關變量//獲得fu變量后,給fu變量賦值為fun的地址;JNI->fu->PriStr(66, 77, "hello");//JNI是結構體指針所以->箭頭指向內部的值,fu也是結構體指針//,所以再次指向箭頭來獲取里面的東西,也就是PriStr函數,我們傳入66,77,”hello”//然后我們點擊運行,就會執行cout << a << b << c << endl;
然后打印相關數據到控制臺:

//因為是c中char *字符類型,所以我們可以改為c_string名字,方便閱讀。

v6 = off_628C;//這個不確定是什么,我們繼續向下看 while (1){ v7 = (unsigned __int8)*v6; if (v7 != *(unsigned __int8*)c_string) break; ++v6; ++c_string; v8 = 1; if (!v7) return v8;} //可以看到有個while(1)死循環,v7 = (unsigned __int8)*v6;//把*6的值轉為無符號int8類型,實際上就是char類型,大家查閱相關資料就知道了,然后賦值給v7if (v7 != *(unsigned __int8*)c_string)//如果v7不等于c_string那么執行下面的break代碼;//直接跳出了這個死循環,(unsigned __int8*)c_string,//這個就是把c_string轉為無符號unsigned __int8*類型,然后取*,獲得里面值 break;//如果break了會怎么樣呢?//就會執行下面的這個return 0;,然后把這個0給java層的那個if條件判斷語句//0就是假,如果為假,那么就執行toast彈窗,提示驗證碼錯誤的信息//我們繼續向下看++v6;//++v6這個就是v6這個指針加v6這個類型去*的類型的大小//比如char 類型他是1個字節的,同時這個v6又是char *類型的,那么就是+1//比如一個地址0x12340000,把這個地址給一個變量名為a,那么++a就是//這個地址+1,也就是0x12340001,++c_string;//這個也是給指針+對應類型長度也就是+1,一個字節的長度v8 = 1;//給v8賦值為1,這個是關鍵因為下面有個return v8,返回1的話,那么就是成功了if (!v7)//如果v7為空,代表對比完了,因為空取反就是真,就返回v8,然后就成功了return v8;
現在的關鍵是怎么找到這個正確的驗證碼,下面我們開始動態調試來獲取這個碼:

我們重新打開ida,然后選擇如下:

輸入對應的ip和端口號就可以開始調試:

在調試前,我們需要把ida的調試文件放到手機中。


放入手機的命令是adb push H:\IDA_Pro_v7.5_Portable(1)\dbgsrv\android_server /data/local/tmp
你也可以放到其他目錄,/data/local/tmp這個目錄比較常用而已。

放入后,我們進入/data/local/tmp。如果要進入我們先要adb shell

然后執行su命令,su用于獲取最高權限,方便調試,給相關文件設置權限。

我們輸入 cd /data/local/tmp進入文件夾,然后輸入 ls- a,可以列出所有的文件。
列出后我們可以看到自己剛剛放入的文件android_server:

我們需要給這個文件賦予最高權限,同時需要給他執行權限,修改權限命令chmod 777 你的文件。

然后我們./android_server執行這個調試文件:

然后我們就可以在ida上進行調試了。調試前,我們先要在手機上打開那個crackme。然后我們在ida上輸入我們手機的ip地址后,點擊ok:

選擇我們的進程:

雙擊進入,這個就是在下載手機上的相關文件了:

出現下面這個信息,我們點擊ok就行。


然后我們點擊綠色箭頭進行運行。

選擇一個我們ctrl+F搜索。

我們搜索libCrackme:

雙擊找到這個securitycheck函數:

雙擊securitycheck函數,進入后,按空格放大,然后按tab鍵,把匯編轉成c偽代碼。

我們改一下這個函數的參數,第一個是JNIEnv*第二個是jclass,第三個是jstring,在修改前,我們需要導入jni的頭文件。



可以看到相關函數名已經可以看到了:

我們在app上輸入一串字符串然后,點擊輸入密碼,可以看到斷下來了:

我們可以修改一下相關的變量名,方便閱讀分析。右鍵選擇rename。


按照前面的分析,我們執行一下這行代碼應該就能獲取真碼了。
F8是步過;F7是步入;Ctrl+F7是運行到return代碼處;F4是運行到鼠標指定位置。

這里我們F8,F8后,我們雙擊這個flag,這個flag是我修改的名字。

雙擊flag后進入到這個代碼區,我們可以看到一串字符串。

我們按鍵盤上的a,把這個字符串變為一串的。按a后我們點擊yes。

這個是轉換后的結果,aiyou,bucuoo

我們繼續單步,可以看到這個123456789數字其實就是我之前輸入的,我們試一下把這一串字符串改成上面轉換后的結果,看看能不能成功。
這個c_string在r0,所以我們到r0這個位置進行修改:

我們對著r0后面那個地址右鍵,然后選擇jmp。


Jmp后的代碼:

我們按一下a,把這一串123456789變為連起來的。

我們右鍵選擇Hex View-1,地址不一樣是因為剛剛ida卡死了。

我們選擇這一串字符串進行右鍵選擇edit。


修改好后,別忘了應用,然后我們繼續單步。



第一次比對V11=0x61 c_string=0x61
第二次V11=0X69 c_string=0x69
第三次V11= 0x79 c_string=0x79
第四次v11=0x6F c_string=0x6F
第五次V11=0x75 c_string=0x75
第六次v11=0x2C c_string=0x2C
第七次v11=0x62 c_string=2x62
第八次 v11=0x75 c_string=0x75
第九次 v11=0x63 c_string=0x63
第十次 v11=0x75 c_string=0x75
第十一次 v11=0x6F c_string=0x6F
第十二次 v11=0x6F c_string=0x6F
第十三次 v11=0 c_string=0
當我們執行完第十三次后,程序彈出Congratulations!!!You Win!!
我們把分析結果放入vs,進行轉換,不出意外的話,結果是aiyou,bucuoo
簡單寫了一下代碼,可以看到,這個真碼就是aiyou,bucuoo
代碼

我們拿著這個真碼,放到模擬器上試試,可以看到成功了。

