分析一道簡單安卓中級題
安裝app后打開,點擊驗證提示我們flag格式錯誤,請重試。

我們打開jeb分析工具進行分析。
紅色圖部分為源碼目錄,也就是dex文件反編譯后的源碼。

R文件存儲了資源相關的id,比如圖片資源,按鈕,文字等信息都存儲在這個R文件里。
在需要使用時就用R.xxx.xxx調用即可。例如R.anim.abc_fade_in,因為都是static修飾的,
所以可以直接打點直接調用,不需要創建對象。

如何知道當前類是什么呢?
執行如下命令即可獲取,方便進行分析。
adb shell dumpsys activity top
因為手機沒有root,所以這里以虛擬機演示。然后虛擬機又不支持26版本的sdkapp,所以只好以mt管理器為例子。
bin.mt.plus為包
.Main為activity,也就是類
bin.mt.plus/.Main

因為這里類就怎么一個MainActivity所以我直接分析了。
如下MainActivity為關鍵類,里面有點擊按鈕后彈出的相關信息。

源碼刨析
package cn.pojie52.cm01;//當前類MainActivity所在的包,也就是文件夾 import android.os.Bundle;//導入Bundle相關方法,變量import android.view.View.OnClickListener;//導入按鈕點擊事件相關方法,變量import android.view.View;//導入視圖相關方法,變量。import android.widget.EditText;//導入編輯框相關方法,變量import android.widget.Toast;//導入吐司彈窗相關方法,變量import androidx.appcompat.app.AppCompatActivity;//主類,一個activity必須要繼承一個activity父類 //加載so文件的方法。//static{}為靜態代碼塊,在MainActivity創建時會優先執行里面的方法//調用System對象里面的LoadLibrary方法,傳入so文件的名字,去除lib,.so,后的名字//在調用LoadLibrary方法后,底層會把lib,so進行拼接,然后去lib目錄里尋找相關的so進行加載static { System.loadLibrary("native-lib");} //如下是一個native方法,返回值為Boolean類型,也就是true和false,//方法名為check,參數為String類型//因為是native方法,所以實現邏輯必定在so里面,而so里面的代碼是由c或者c++編譯的//所以要執行這個System.loadLibrary("native-lib");代碼,把so文件提前加載到內存中public native boolean check(String arg1) {} @Override // androidx.appcompat.app.AppCompatActivity//@Override說明這是一個重寫方法,//這個方法OnCreate在AppCompatActivity類里//因為主類MainActivity后面加了extends AppCompatActivity//說明AppCompatActivity里的方法,變量都會繼承過來,也就是說我們可以直接使用//參數為Bundle protected void onCreate(Bundle arg3) { super.onCreate(arg3);//調用父類的OnCreate方法放入arg3參數//父類也就是AppCompatActivity this.setContentView(0x7F0A001C); // layout:activity_main//調用setContentView方法初始化界面//也就是我們在開始的時候看到的那個界面,//有個編輯框,一個驗證按鈕 EditText v3 = (EditText)this.findViewById(0x7F070058); // id:flag//調用findViewById方法傳入編輯框的id,然后轉為EditText,//因為獲取到的是一個view對象,這個view對象范圍太大了//我們直接轉為EditText方便些 this.findViewById(0x7F070045).setOnClickListener(new View.OnClickListener() { // id:check//這個是一種鏈式編程寫法,簡便,省去了定義變量來接收他//這個是調用findViewById傳入按鈕的id,//this指的是當前activity,因為繼承了AppCompatActivity類//所以我們可以直接使用里面的findViewById方法//調用這個方法后返回的是一個對象,然后調用setOnclickListener方法//傳入匿名內部類對象,給按鈕綁定一個監聽事件//onClick方法是重寫的 @Override // android.view.View$OnClickListener public void onClick(View arg4) { } });} 在Onclick方法里的代碼邏輯 String v4 = v3.getText().toString().trim();//v3為編輯框,獲取編輯框里面的信息,轉為string,去除空格,//賦值為string變量v4 if(v4.length() != 30) {//判斷v4的長度是否等于30//如果不等于就提示flag格式錯誤,請重試//return為返回 Toast.makeText(MainActivity.this, "flag格式錯誤,請重試", 0).show();//調用Toast對象里面的makeText方法,放入MainActiviy//因為onclick方法在內部類里面,所以是MainActivity.this,而不是this.MainActiviy return; } if(MainActivity.this.check(v4)) {//調用native層的check方法,傳入v4字符串 Toast.makeText(MainActivity.this, "恭喜你,驗證正確!", 0).show();//如果check方法的返回值為true那么彈出恭喜你,驗證正確的提示 return; } Toast.makeText(MainActivity.this, "flag錯誤,再接再厲", 0).show();//否則提示flag錯誤,再接再厲 }
我們已經知道了具體邏輯是在so層,且密碼長度是30位的。
接下來我們進入so層進行分析,我們需要用到的工具是ida。
在拖入ida前,我們需要把apk包進行解壓,獲取lib目錄里面的so文件。


拖入64位的ida。
我們默認即可,點擊ok。

因為這個check方法不是系統方法,所以在export就可以看到。
Java_cn_pojie52_cm01_MainActivitycheck
在ida中顯示為Java類名_方法名。

我們雙擊進入。

按tab鍵轉為c偽代碼。

我們改一下參數,第一個參數固定為JNIEnv*。
在改之前,我們需要導入jni的頭文件,Jni頭文件里存放了相關的jni方法,方便分析。





第二個參數要么是jclass,要么是jobject。
如果是類就是jclass,如果是對象就是jobject。
可以看到這個check方法并沒有static修飾,也就是說要用對象才能調用,也就是說是jobject。

我們對著int64的參數右鍵,把他修改成jobject。


這個check方法的參數是一個string類型的,所以這個native方法第三個參數也是string類型的,在c語言中string被定義為jstring。

查找jni頭文件可以知道。這個jstring是一個jstring,而jstring是jobject,所以這個jstring是jobject類型。
Typedef是給一個變量進行重定義。*這個代表這是一個一級指針,多少個星花代表幾級指針。
指針是一個地址,地址里面有一塊空間,用來存放變量的數據。
比如int a=0;這個a是一個地址,給這個地址取名為a,地址里面存放了0這個數據。當我們&a時就是獲取a的地址,相當于把a還原成一個地址,我們可以用Printf(“%x”,&a);來打印這個地址。

我們修改一下第三個參數。


如下是改完后的完整代碼:
__int64 __fastcall Java_cn_pojie52_cm01_MainActivity_check(_JNIEnv *a1, jobject a2, jstring a3){ const char *v5; // x21 size_t v6; // w0 int v7; // w0 __int64 v8; // x0 _BYTE *v9; // x0 int8x16_t v10; // q0 int8x16_t v11; // q4 int8x16_t v12; // q2 int8x16_t v13; // q5 int8x16_t v14; // q1 int8x16_t v15; // q0 __int64 v16; // x8 unsigned int v17; // w19 _BYTE v19[33]; // [xsp+0h] [xbp-A0h] int v20; // [xsp+21h] [xbp-7Fh] char v21; // [xsp+25h] [xbp-7Bh] char v22; // [xsp+26h] [xbp-7Ah] char v23; // [xsp+27h] [xbp-79h] char v24; // [xsp+28h] [xbp-78h] char dest[16]; // [xsp+38h] [xbp-68h] BYREF __int128 v26; // [xsp+48h] [xbp-58h] __int128 v27; // [xsp+58h] [xbp-48h] __int128 v28; // [xsp+68h] [xbp-38h] __int64 v29; // [xsp+78h] [xbp-28h] v29 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40); if ( a1->functions->GetStringUTFLength((JNIEnv *)a1, a3) == 30 ) { v5 = a1->functions->GetStringUTFChars(a1, a3, 0LL); v28 = 0u; v27 = 0u; v26 = 0u; *(_OWORD *)dest = 0u; v6 = strlen(v5); strncpy(dest, v5, v6); a1->functions->ReleaseStringUTFChars((JNIEnv *)a1, a3, v5); v7 = strlen(dest); sub_B90((int)dest, v7, "areyousure??????"); v8 = strlen(dest); v9 = (_BYTE *)sub_D90(dest, v8); *(_OWORD *)v19 = unk_11A1; *(_OWORD *)&v19[16] = unk_11B1; *(_QWORD *)&v19[25] = unk_11BA; v10.n128_u64[0] = 0xB2B2B2B2B2B2B2B2LL; v10.n128_u64[1] = 0xB2B2B2B2B2B2B2B2LL; v11.n128_u64[0] = 0xFEFEFEFEFEFEFEFELL; v11.n128_u64[1] = 0xFEFEFEFEFEFEFEFELL; v19[0] = 53; v12 = veorq_s8( vaddq_s8(veorq_s8(vaddq_s8(*(int8x16_t *)&v19[1], v10), (int8x16_t)xmmword_1130), (int8x16_t)xmmword_1140), v11); v13.n128_u64[0] = 0x101010101010101LL; v13.n128_u64[1] = 0x101010101010101LL; v14.n128_u64[0] = 0x3E3E3E3E3E3E3E3ELL; v14.n128_u64[1] = 0x3E3E3E3E3E3E3E3ELL; *(int8x16_t *)&v19[1] = vaddq_s8( veorq_s8( vsubq_s8(v13, vorrq_s8(vshrq_n_u8(v12, 7uLL), vshlq_n_s8(v12, 1uLL))), (int8x16_t)xmmword_1150), v14); v20 = 1782990162; v15 = veorq_s8( vaddq_s8(veorq_s8(vaddq_s8(*(int8x16_t *)&v19[17], v10), (int8x16_t)xmmword_1160), (int8x16_t)xmmword_1170), v11); v21 = ((1 - ((2 * ((((unk_11C6 - 78) ^ 0xB2) - 117) ^ 0xFE)) | ((((unsigned __int8)(((unk_11C6 - 78) ^ 0xB2) - 117) ^ 0xFE) & 0x80) != 0))) ^ 0x25) + 62; v16 = 0LL; v22 = ((1 - ((2 * ((((unk_11C7 - 78) ^ 0xB1) - 118) ^ 0xFE)) | ((((unsigned __int8)(((unk_11C7 - 78) ^ 0xB1) - 118) ^ 0xFE) & 0x80) != 0))) ^ 0x26) + 62; *(int8x16_t *)&v19[17] = vaddq_s8( veorq_s8( vsubq_s8(v13, vorrq_s8(vshrq_n_u8(v15, 7uLL), vshlq_n_s8(v15, 1uLL))), (int8x16_t)xmmword_1180), v14); v23 = ((1 - ((2 * ((((unk_11C8 - 78) ^ 0xB0) - 119) ^ 0xFE)) | ((((unsigned __int8)(((unk_11C8 - 78) ^ 0xB0) - 119) ^ 0xFE) & 0x80) != 0))) ^ 0x27) + 62; v24 = ((1 - ((2 * ((((unk_11C9 - 78) ^ 0xBF) - 120) ^ 0xFE)) | ((((unsigned __int8)(((unk_11C9 - 78) ^ 0xBF) - 120) ^ 0xFE) & 0x80) != 0))) ^ 0x28) + 62; while ( v9[v16] == v19[v16] ) { if ( v9[v16] ) { if ( ++v16 != 41 ) continue; } v17 = 1; goto LABEL_9; } v17 = 0;LABEL_9: free(v9); } else { return 0; } return v17;}
為了方便分析,我們把參數的名字改一下。


重復前面的操作。

//判斷password的長度是否為30if ( env->functions->GetStringUTFLength((JNIEnv *)env, password) == 30 )//這個env是一個指針,指針里面有一個函數指針,函數指針里面有一個GetStringUTFLength函數//這個函數用于判斷字符串的長度//這個涉及到了結構體相關知識//把password轉為c語言中的char類型env->functions->GetStringUTFChars(env, password, 0LL);
為了方便,我提前改好了相關名字。

password_length = strlen(password_);//獲取password_的長度//把password_拷貝到password__里面,拷貝長度為password_lengthstrncpy(password__, password_, password_length); //釋放password_指向的字符串空間//也就是歸還空間,讓其他程序使用env->functions->ReleaseStringUTFChars((JNIEnv *)env, password, password_);//獲取password__的長度password__length = strlen(password__);
細心的朋友可以知道這個16是有問題的,因為我們的password是30位的,然而這個password確是16位的。
char password__[16]
這里我改為了32。

我們看一下下面的這個函數,可以看到傳入了我們password__,password_length,密碼與長度,還有"areyousure??????"。

我們雙擊進入這個函數,然后修改一下如下參數。

//獲取threeStr的長度threeStrLen = strlen(threeStr);do {v9 = *((unsigned __int8 *)v20 + v7); //v7加上轉換為無符號int類型的指針v20,也就是加上無符號int類型的步長。然后取*取出相加后里面空間的值v10 = v8 + v9 + (unsigned __int8)threeStr[v7 % threeStrLen];//v9加上v8然后加上無符號整型的threeStr[v7%threeStrlen];//對v7以threeStrlen進行取余,然后threeStr[取余的值]取出threeStr里面的字符串數據 v11 = v10 + 255;//v10加上255賦值給v11 if ( v10 >= 0 )//如果v10大于等于0就執行v10賦值給v11的操作 v11 = v10;把v10賦值給v11 v8 = v10 - (v11 & 0xFFFFFF00);//對v11與0XFFFFFF00進行與運算,然后v10減去運算結果,賦值給v8*((_BYTE *)v20 + v7++) = *((_BYTE *)v20 + v8);//把v20轉為Byte*類型然后加上v8,步長為Byte *的長度,然后*取值//然后把值賦值給把v20轉為Byte*類型加上自增的v7,獲得值后進行取*,賦值給這個取*花后的值*((_BYTE *)v20 + v8) = v9;//把v9賦值給把v20轉為Byte*類型然后加上v8個Byte*步長里面的值 } while ( v7 != 256 );如果v7不等于256就一直循環 //判斷如果password__length是否為空//在c語言中,非0就是true,也就是說可以用于判斷是否為空if ( password__length ) { v12 = 0;//給v12賦值0 v13 = 0;給v13賦值0v14 = password__length;//把密碼長度賦值給v14 do { v15 = v12 + 1; //把v12加上1的值賦值給v15 if ( v12 + 1 >= 0 )//判斷v12+1是否大于等于0 v16 = v12 + 1;//把v12加1的值賦值給v16 else v16 = v12 + 256;//把v12加上256賦值給v16 v12 = v15 - (v16 & 0xFFFFFF00);//對v16與0XFFFFFF00進行與運算,然后v15減去運算結果,賦值給v12 v17 = *((unsigned __int8 *)v20 + v12);//把v20轉為無符號int整型指針然后加上這個指針的步長,//對這個值進行取星花賦值給v17 v18 = v13 + v17;//把v13加上v17的值賦值給v18 v19 = v18 + 255;//把v18加上255的值賦值給v19 if ( v18 >= 0 )//判斷v18是否大于0 v19 = v18;//如果大于0就把v18賦值給v19 v13 = v18 - (v19 & 0xFFFFFF00);//對v19與0XFFFFFF00進行與運算,然后v18減去運算結果,賦值給v13 --v14;//v14減一 *((_BYTE *)v20 + v12) = *((_BYTE *)v20 + v13);//把v20轉為Byte*類型加上v13個Byte*的步長,然后取*取出里面的值,//把這個值賦值給v20轉為Byte*類型加上v12個Byte*步長的地址空間 *((_BYTE *)v20 + v13) = v17;//把v17的值賦值給v20轉為Byte*類型加上v13個Byte*步長的地址空間 *password__++ ^= *((_BYTE *)v20 + (unsigned __int8)(*((_BYTE *)v20 + v12) + v17));//取出password里面的值//把v20轉為Byte*類型,加上把V20轉為Byte*類型后加上v12進行取*加上v17后轉為無符號int類型//然后進行取*取出里面的值,把值與password進行異或然后賦值給password } while ( v14 );//如果v14不為空就一直循環 }
這兩塊代碼分別操作著threeStr,還有password。

如下代碼是關鍵:
while ( v9[v16] == v19[v16] ) //循環對比v9[v16]的值與v19[v16]的值 { if ( v9[v16] ) //判斷v9[16]里是否有值 { if ( ++v16 != 41 )//如果v16加一后不等于41就continue跳過代碼 continue; } v17 = 1;//如果為1那么flag正確 goto LABEL_9;//跳到LABEL_9的位置 } v17 = 0;//如果為0那么flag錯誤LABEL_9: free(v9);//釋放內存 } else { return 0; }return v17;//影響驗證結果
下面hook一下這個函數。看看這個password變成了什么。
import frida, sys//因為是frida hook,所以要導入frida模塊,//讀取系統輸入需要sys模塊 jscode = ''' function inline_hook() { var so_addr = Module.findBaseAddress("libnative-lib.so"); if (so_addr) { console.log("so_addr:", so_addr); var addr_b90 = so_addr.add(0xb90); var sub_b90 = new NativeFunction(addr_b90 , 'int', ['pointer', 'int','pointer']); var arg1 = Memory.allocUtf8String('111111111111111111111111111111'); var arg2 = 30; var arg3 = Memory.allocUtf8String('areyousure??????'); var ret_b90 = sub_b90(arg1,arg2,arg3); console.log(Memory.readByteArray(arg1,64)); var addr_d90 = so_addr.add(0xd90); var sub_d90 = new NativeFunction(addr_d90 , 'pointer', ['pointer', 'int' ]); var arg1 = Memory.allocUtf8String('111111111111111111111111111111'); var arg2 = 30; var ret_d90 = sub_d90(arg1,arg2); console.log(Memory.readByteArray(ret_d90,64)); } }setImmediate(inline_hook) '''def on_message(message, data): if message['type'] == 'send': print(" {0}".format(message['payload'])) else: print(message)pass#print(frida.enumerate_devices())# 查找USB設備并附加到目標進程device = frida.get_remote_device()#pid = device.spawn(["com.live.xctv"]) #session = device.attach(pid)session =device.attach('cn.pojie52.cm01') #這里是要注入的apk包名# 在目標進程里創建腳本script = session.create_script(jscode)# 注冊消息回調script.on('message', on_message)print(' Start attach')# 加載創建好的javascript腳本script.load()# 讀取系統輸入sys.stdin.read() var so_addr = Module.findBaseAddress("libnative-lib.so"); //查找so的基址。
在ida中基址為0000000000000,在動態調試時基址會變,所以我們需要通過基址加上函數的偏移來定位一個函數。

在這里我們需要hook的函數是sub_B90()。

我們按tab鍵查看函數偏移。

console.log("so_addr:", so_addr);//打印so的基址 var addr_b90 = so_addr.add(0xb90);//so的基址加上b90就是這個函數的地址 if (so_addr) //判斷基礎是否獲取到了 var sub_b90 = new NativeFunction(addr_b90 , 'int', ['pointer', 'int','pointer']);//創建一個本地函數,類似于指針函數,給指針函數賦值函數的地址。//然后進行調用,參數為_BYTE *password__, unsigned int password__length, char *threeStr//返回值為unsigned __int64//返回值對應'int',參數對應著['pointer', 'int','pointer']下面為封裝函數的參數,進行調用//分配內存創建111111111111111111111111111111字符串var arg1 = Memory.allocUtf8String('111111111111111111111111111111');var arg2 = 30;//長度//分配內存創建areyousure??????字符串var arg3 = Memory.allocUtf8String('areyousure??????');//調用sub_b90函數,傳入arg1,arg2,arg3var ret_b90 = sub_b90(arg1,arg2,arg3);//返回值為ret_b90Memory.readByteArray(arg1,64)//讀取arg1內存中數據,也就是password,讀64位byte的數據console.log(Memory.readByteArray(arg1,64));//打印讀取出來的password
這里還有一個函數傳入了password還有密碼的長度。

var addr_d90 = so_addr.add(0xd90);//基址加上0xb90的偏移就是sub_d90函數的地址 var sub_d90 = new NativeFunction(addr_d90 , 'pointer', ['pointer', 'int' ]);//創建一個本地函數,類似于指針函數,給指針函數賦值函數的地址。//然后進行調用,參數為char *a1, __int64 a2//返回值為void * 萬能指針,任何指針都可以賦值//返回值對應'pointer',參數對應著['pointer', 'int'] //分配內存創建111111111111111111111111111111字符串var arg1 = Memory.allocUtf8String('111111111111111111111111111111');//密碼長度var arg2 = 30;//調用sub_d90()函數,輸入arg1,arg2參數var ret_d90 = sub_d90(arg1,arg2);//返回值賦值給ret_d90讀取sub_d90返回值地址里面的空間的64位byte的數據console.log(Memory.readByteArray(ret_d90,64));
以下是dump的數據。
我們輸入的密碼是111111111111111111111111111111。當執行完異或操作后變為了下面的數據。

30個1對應著16進制為30個0x31。在ASCII碼中,49的ASCII碼為‘1’,這個49是10進制的,轉為16進制就是0x31。
e0,6b,37,a1,75,d7,f6,d4,ef,19,c6,c3,57,a0,f9,b4
73,ee,c8,d1,b3,30,1a,0a,09,52,06,8c,1f,7c
在計算機中,異或運算是可以進行解密的。

10 xor 5 =15

15 xor 10 = 5
把10當成我們輸入的密碼,把5當成要異或的值,把15當成異或后的值
我們知道了異或后的值,當我們再次異或我們的密碼時,就可以得到要異或的值。
e0,6b,37,a1,75,d7,f6,d4,ef,19,c6,c3,57,a0,f9,b4
73,ee,c8,d1,b3,30,1a,0a,09,52,06,8c,1f,7c
與30個0x31進行異或即可得到密碼。
public static void Xor(){ int xorData[]={0xe0,0x6b,0x37,0xa1,0x75,0xd7,0xf6,0xd4,0xef,0x19,0xc6,0xc3,0x57,0xa0,0xf9,0xb4, 0x73,0xee,0xc8,0xd1,0xb3,0x30,0x1a,0x0a,0x09,0x52,0x06,0x8c,0x1f,0x7c}; int xorDataMy[]={0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31, 0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31}; System.out.print("["); for (int i = 0; i < xorData.length; i++) { System.out.print(xorData[i]^xorDataMy[i]); if(i-1){ System.out.print(","); } } System.out.print("]"); }

得到結果如下:
[209,90,6,144,68,230,199,229,222,40,247,242,102,145,200,133,66,223,249,224,130,1,43,59,56,99,55,189,46,77]
如下是sub_d90函數的返回進行地址空間dump的數據,可以看到全是字母。

我們可以嘗試推斷一下是不是某種加密算法,比如aes,base64,md5加密。
aes加密是需要秘鑰的,有時還需要iv偏移,base64加密不需要秘鑰,也不需要iv偏移。
Md5加密的話,要么全是大寫,全是小寫,aes加密后綴有個等于,很復雜。
而這個sub_d90函數返回值不可能的aes加密,因為sub_d90中并沒有任何的秘鑰,iv偏移相關。
Md5也不太可能,因為md5要么全是大寫字母,或者全是小寫字母,并且不規則。
public static void baseEncode(){ System.out.println(); byte xorDataMy[]={0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31, 0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31}; String by=Base64.getEncoder().encodeToString( xorDataMy); System.out.println(by); }public static void md5(){ System.out.println(MD5Utils.stringToMD5("111111111111111111111111111111"));}
可以看到base64加密的值與sub_d90的值一樣。


我們回到這里。V9[v16]這個是返回的base64編碼的數據,V19[16]是真碼,也是base64的。只要我們把這個真碼的base64得到,然后通過解密base64,然后把解密后的數據與前面的key進行異或即可獲得flag。
while ( v9[v16] == v19[v16] ) { if ( v9[v16] ) { if ( ++v16 != 41 ) continue; } v17 = 1; goto LABEL_9; } v17 = 0;LABEL_9: free(v9); } else { return 0; } return v17;如何獲取v19呢?如下是v19的定義,地址為xsp+0_BYTE v19[33]; // [xsp+0h] [xbp-A0h] while ( v9[v16] == v19[v16] )
我們在v9[v16]這個位置按一下tab鍵即可看到偏移。
這塊代碼的偏移的是b2c。

我們需要在v19[v16]初始化后才能進行hook攔截,也就是獲取數據
最好的時機是在sub_b90執行后進行截取。我們可以看到第一個參數是xsp+38h,也就是說xsp+0是xsp+38-38即可。
我們需要攔截第一個參數獲取到地址,然后減去38就能獲取到xsp也就是v19。

下面進行分析frida hook代碼。
import frida, sys jscode = ''' var destAddr = ''; //定位xsp地址 function inline_hook() { var so_addr = Module.findBaseAddress("libnative-lib.so");//尋找基址 //在ida中基址為0000000000000//在動態調試時基址會變//所以我們需要通過基址加上函數的偏移來定位一個函數 if (so_addr) {//是否獲取到基址,如果沒有獲取到里面的代碼就不會執行 console.log("so_addr:", so_addr);//打印so的地址 var addr_b90 = so_addr.add(0xB90);//獲取sub_b90的函數地址 var sub_b90 = new NativeFunction(addr_b90 , 'int', ['pointer', 'int', 'pointer']);//創建一個本地函數,類似于指針函數,給指針函數賦值函數的地址。//然后進行調用,參數為_BYTE *password__, unsigned int password__length, char *threeStr//返回值為unsigned __int64//返回值對應'int',參數對應著['pointer', 'int','pointer'] //下面是一個攔截器,用于攔截函數的sub_b90的進入,與結束 Interceptor.attach(sub_b90, { onEnter: function(args) //進入函數時執行的代碼 { destAddr = args[0];//把第一個參數賦值給destAddr console.log('onEnter B90'); //打印'onEnter B90,用于確認函數是否進入 }, //在進入函數之后執行的語句 onLeave:function(retval) {//retval為函數的返回值 console.log('onLeave B90');//打印onLeave B90,用于確認函數是否離開 } }); var addr_b2c = so_addr.add(0xb2c);//0xb2c為偏移,也就是說攔截 b2c的地址 console.log("The addr_b2c:", addr_b2c);//打印addr_b2c的地址 Java.perform(function() { Interceptor.attach(addr_b2c, {//攔截b2c地址 onEnter: function(args) { //arg為參數 console.log("addr_b2c OnEnter :", Memory.readByteArray(destAddr.sub(0x38),64) ); // Memory.readByteArray(destAddr.sub(0x38),64)//讀取xsp的內容,也就v19的值//也就是base64編碼的真碼 } }) }) }}setImmediate(inline_hook) ''' //用于接收消息的函數//第一個參數為需要打印的信息def on_message(message, data): if message['type'] == 'send': print(" {0}".format(message['payload'])) else: print(message)Pass//跳過#print(frida.enumerate_devices())# 查找USB設備并附加到目標進程device = frida.get_remote_device()//調用frida的函數,獲取設備#pid = device.spawn(["com.live.xctv"]) #session = device.attach(pid)//session =device.attach('cn.pojie52.cm01') #這里是要注入的apk包名//這個包名可以通過frida-ps -U 來進行獲取# 在目標進程里創建腳本script = session.create_script(jscode)# 注冊消息回調script.on('message', on_message)print(' Start attach')# 加載創建好的javascript腳本script.load()# 讀取系統輸入sys.stdin.read()

下面的數據是左邊的十六進制。
{0x35, 0x47, 0x68, 0x32, 0x2f, 0x79, 0x36, 0x50, 0x6f, 0x71, 0x32, 0x2f, 0x57, 0x49, 0x65, 0x4c, 0x4a, 0x66,0x6d, 0x68, 0x36, 0x79, 0x65, 0x73, 0x6e, 0x4b, 0x37, 0x6e, 0x64, 0x4b, 0x37, 0x6e, 0x64, 0x6e, 0x4a, 0x65, 0x57, 0x52, 0x45, 0x46, 0x6a, 0x52, 0x78, 0x38}
下面的數據是十六進制對應的ASCII碼。
5Gh2/y6Poq2/WIeLJfmh6yesnK7ndnJeWREFjRx8
import base64//導入base64的包,因為我們要用到base64的解密函數 //xorkey是我們前面通過異或解密得出的數據xorkey = [209, 90, 6, 144, 68, 230, 199, 229, 222, 40, 247, 242, 102, 145, 200, 133, 66, 223, 249, 224, 130, 1, 43, 59, 56, 99, 55, 189, 46, 77] //第一個參數為base64解密后的數據//第二個參數為base64解密后的數據的長度def sub_B90(data, l): ret = [] for i in range(l): ret.append(data[i] ^ xorkey[i])//異或解密,把結果拼接到ret里面 s = '' for i in ret://便利ret里面數據 s += chr(i) //把ret的解密結果轉換成字符,如何進行拼接。 print(s)//打印拼接后的解密數據 return ret//返回ret def resv(data): data = base64.b64decode(data,)//解密base64數據 t = sub_B90(data, len(data))//把解密后的base64數據傳入sub_b90進行異或解密 return (t) data="5Gh2/y6Poq2/WIeLJfmh6yesnK7ndnJeWREFjRx8"http://base64數據 resv(data)//調用這個函數進行解密base64數據與異或解密獲得真嗎

輸入真碼后,提示我們恭喜你,驗證正確。
