巧解一道CTF Android題

語言: CN / TW / HK

本文為看雪論壇優秀文章

看雪論壇作者ID:白雲精靈

用到工具:

1:jeb

2:ida

3:Pycharm

4:idea

5:010editor

6:frida

1.背景

網上能看到的相關解題方法基本都是窮舉爆破,還原始碼,這裡我巧解一下,

用到的辦法是XOR解密。無須還原始碼,窮舉爆破。

原理:經過XOR異或加密的字串都可以再次異或進行解密獲得key。

2.開始分析

把app安裝到手機:

輸入註冊碼,點選註冊,提示我們“您的註冊碼已儲存”:

我們獲取一下最頂層activity。 最頂層activity是 com.gdufs.xman/.RegActivity。

我們開啟jeb工具,定位到當前activity。

程式碼如下:

package com.gdufs.xman;

import android.app.Activity;
import android.app.AlertDialog.Builder;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Process;
import android.view.View.OnClickListener;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

public class RegActivity extends Activity {
private Button btn_reg;
private EditText edit_sn;

@Override // android.app.Activity
public void onCreate(Bundle arg3) {
super.onCreate(arg3);
this.setContentView(0x7F04001B); // layout:activity_reg
this.btn_reg = (Button)this.findViewById(0x7F0B0054); // id:button1
this.edit_sn = (EditText)this.findViewById(0x7F0B0055); // id:editText1
this.btn_reg.setOnClickListener(new View.OnClickListener() {
@Override // android.view.View$OnClickListener
public void onClick(View arg5) {
String sn = RegActivity.this.edit_sn.getText().toString().trim();
if(sn == null || sn.length() == 0) {
Toast.makeText(RegActivity.this, "您的輸入為空", 0).show();
return;
}

((MyApp)RegActivity.this.getApplication()).saveSN(sn);
new AlertDialog.Builder(RegActivity.this).setTitle("回覆").setMessage("您的註冊碼已儲存").setPositiveButton("好吧", new DialogInterface.OnClickListener() {
@Override // android.content.DialogInterface$OnClickListener
public void onClick(DialogInterface arg2, int arg3) {
Process.killProcess(Process.myPid());
}
}).show();
}
});
}
}

這個是獲取註冊碼編輯框內容:

String sn = RegActivity.this.edit_sn.getText().toString().trim();

把註冊碼傳入saveSN方法:

((MyApp)RegActivity.this.getApplication()).saveSN(sn);

我們看一下saveSN方法,可以看到這是一個native方法。

package com.gdufs.xman;

import android.app.Application;
import android.util.Log;

public class MyApp extends Application {
public static int m;

static {
MyApp.m = 0;
System.loadLibrary("myjni");
}

public native void initSN() {
}

@Override // android.app.Application
public void onCreate() {
this.initSN();
Log.d("com.gdufs.xman m=", String.valueOf(MyApp.m));
super.onCreate();
}

public native void saveSN(String arg1) {
}

public native void work() {
}
}

我們解包一下apk,獲取到so檔案。

下面進入ida分析。匯出函式並沒有相關java的native方法,說明是動態註冊。

我們看下JNI_ONLOAD函式:

jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
if ( !(*vm)->GetEnv(vm, (void **)&g_env, 65542) )
{
_android_log_print(2, "com.gdufs.xman", "JNI_OnLoad()");
native_class = (*(int (__fastcall **)(int, const char *))(*(_DWORD *)g_env + 24))(g_env, "com/gdufs/xman/MyApp");
if ( !(*(int (__fastcall **)(int, int, char **, int))(*(_DWORD *)g_env + 860))(g_env, native_class, off_5004, 3) )
{
_android_log_print(2, "com.gdufs.xman", "RegisterNatives() --> nativeMethod() ok");
return 65542;
}
_android_log_print(6, "com.gdufs.xman", "RegisterNatives() --> nativeMethod() failed");
}
return -1;
}

雙擊紅色箭頭的地方:

可以看到動態註冊的函式。

下面我們用frida hook一下函式地址。

frida程式碼如下:

var RevealNativeMethods = function() {
var pSize = Process.pointerSize;
var env = Java.vm.getEnv();
var RegisterNatives = 215, FindClassIndex = 6; // search "215" @ https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html
var jclassAddress2NameMap = {};
function getNativeAddress(idx) {
return env.handle.readPointer().add(idx * pSize).readPointer();
}
// intercepting FindClass to populate Map<address, jclass>
Interceptor.attach(getNativeAddress(FindClassIndex), {
onEnter: function(args) {
jclassAddress2NameMap[args[0]] = args[1].readCString();
}
});
// RegisterNative(jClass*, .., JNINativeMethod *methods[nMethods], uint nMethods) // https://android.googlesource.com/platform/libnativehelper/+/master/include_jni/jni.h#977
Interceptor.attach(getNativeAddress(RegisterNatives), {
onEnter: function(args) {
for (var i = 0, nMethods = parseInt(args[3]); i < nMethods; i++) {
/*
https://android.googlesource.com/platform/libnativehelper/+/master/include_jni/jni.h#129
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
*/
var structSize = pSize * 3; // = sizeof(JNINativeMethod)
var methodsPtr = ptr(args[2]);
var signature = methodsPtr.add(i * structSize + pSize).readPointer();
var fnPtr = methodsPtr.add(i * structSize + (pSize * 2)).readPointer(); // void* fnPtr
var jClass = jclassAddress2NameMap[args[0]].split('/');
var methodName = methodsPtr.add(i * structSize).readPointer().readCString();
var str_name_so = "libmyjni.so"; //需要hook的so名
var n_addr_so = Module.findBaseAddress(str_name_so); //載入到記憶體後 函式地址 = so地址 + 函式偏移
console.log('\x1b[3' + '6;01' + 'm', JSON.stringify({
module: DebugSymbol.fromAddress(fnPtr)['moduleName'], // https://www.frida.re/docs/javascript-api/#debugsymbol
package: jClass.slice(0, -1).join('.'),
class: jClass[jClass.length - 1],
method: methodName, // methodsPtr.readPointer().readCString(), // char* name
signature: signature.readCString(), // char* signature TODO Java bytecode signature parser { Z: 'boolean', B: 'byte', C: 'char', S: 'short', I: 'int', J: 'long', F: 'float', D: 'double', L: 'fully-qualified-class;', '[': 'array' } https://github.com/skylot/jadx/blob/master/jadx-core/src/main/java/jadx/core/dex/nodes/parser/SignatureParser.java
address: (fnPtr-n_addr_so).toString(16)
}), '\x1b[39;49;00m');
}
}
});
}

Java.perform(RevealNativeMethods);

hook結果:

可以看到saveSN的地址為11f9。

[Redmi K20 Pro Premium Edition::com.gdufs.xman ]->  {"module":"libmyjni.so","package":"com.gdufs.xman","class":"MyApp","method":"initSN","signature":"()V","address":"13b1"}
{"module":"libmyjni.so","package":"com.gdufs.xman","class":"MyApp","method":"saveSN","signature":"(Ljava/lang/String;)V","address":"11f9"}
{"module":"libmyjni.so","package":"com.gdufs.xman","class":"MyApp","method":"work","signature":"()V","address":"14cd"}

我們直接ida定位。

int __fastcall n2(_DWORD *a1, int a2, int a3)
{
FILE *v5; // r7
_DWORD *v7; // r4
const char *v8; // r3
int v9; // r0
int v10; // r1
_WORD *v11; // r5
_DWORD *v12; // r0
int v13; // r4
int v14; // r3
signed int v15; // r6
const char *v16; // r9
char *v17; // r5
signed int v18; // r10
char v19; // r2
char v20; // r3
_BYTE v21[56]; // [sp+0h] [bp-38h] BYREF

v5 = fopen("/sdcard/reg.dat", "w+");
if ( !v5 )
return j___android_log_print(3, "com.gdufs.xman", byte_2DCA);
v7 = v21;
v8 = "W3_arE_whO_we_ARE";
do
{
v9 = *(_DWORD *)v8;
v8 += 8;
v10 = *((_DWORD *)v8 - 1);
*v7 = v9;
v7[1] = v10;
v11 = v7 + 2;
v7 += 2;
}
while ( v8 != "E" );
v12 = a1;
v13 = 2016;
*v11 = *(_WORD *)v8;
v14 = *a1;
v15 = 0;
v16 = (const char *)(*(int (__fastcall **)(_DWORD *, int, _DWORD))(v14 + 676))(v12, a3, 0);
v17 = (char *)v16;
v18 = strlen(v16);
while ( v15 < v18 )
{
if ( v15 % 3 == 1 )
{
v13 = (v13 + 5) % 16;
v19 = v21[v13 + 1];
}
else if ( v15 % 3 == 2 )
{
v13 = (v13 + 7) % 15;
v19 = v21[v13 + 2];
}
else
{
v13 = (v13 + 3) % 13;
v19 = v21[v13 + 3];
}
v20 = *v17;
++v15;
*v17++ = v20 ^ v19;
}
fputs(v16, v5);
return j_fclose(v5);
}

為了方便分析這邊匯入一下jni標頭檔案。

修改一下第一個引數為jnienv,第三個引數為我們的註冊碼。

如下程式碼在sd卡目錄建立了一個檔案叫reg.dat。

v5 = fopen("/sdcard/reg.dat", "w+");

如下程式碼進行寫入:

fputs(v16, v5);

我們看一下v16相關邏輯。

可以看到v16給了v17,v17每一個字元進行異或操作。

*v17++ = v20 ^ v19;

也就是說有多少字元就異或出多少個字元,我們去sdcard把檔案拉出來。

拖入010editor,可以看到我們輸入的是13個1,異或出13個數據。

我們再去分析一下是如何讀取這個檔案的。

因為當我們輸入註冊碼後,點選確定就結束程序了,那麼啟動程式肯定會讀取的。

new AlertDialog.Builder(RegActivity.this).setTitle("回覆").setMessage("您的註冊碼已儲存").setPositiveButton("好吧", new DialogInterface.OnClickListener() {
@Override // android.content.DialogInterface$OnClickListener
public void onClick(DialogInterface arg2, int arg3) {
Process.killProcess(Process.myPid());
}
}).show();

我們去看一下入口activity,可以看到入口activity是com.gdufs.xman.MainActivity。

<?xml version="1.0" encoding="UTF-8"?>
<manifest android:versionCode="1" android:versionName="1.0" package="com.gdufs.xman" platformBuildVersionCode="23" platformBuildVersionName="6.0-2704002" xmlns:android="http://schemas.android.com/apk/res/android">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="23"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
<application android:allowBackup="true" android:debuggable="true" android:icon="@drawable/aaron" android:label="@string/app_name" android:name="com.gdufs.xman.MyApp" android:theme="@style/AppTheme">
<activity android:label="@string/app_name" android:name="com.gdufs.xman.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:label="@string/title_activity_reg" android:name="com.gdufs.xman.RegActivity"/>
</application>
</manifest>

我們定位到這個activity:

package com.gdufs.xman;

import android.app.Activity;
import android.app.AlertDialog.Builder;
import android.content.ComponentName;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.os.Process;
import android.util.Log;
import android.view.Menu;
import android.view.View.OnClickListener;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends Activity {
private Button btn1;
private static String workString;

public void doRegister() {
new AlertDialog.Builder(this).setTitle("註冊").setMessage("Flag就在前方!").setPositiveButton("註冊", new DialogInterface.OnClickListener() {
@Override // android.content.DialogInterface$OnClickListener
public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.gdufs.xman", "com.gdufs.xman.RegActivity"));
MainActivity.this.startActivity(intent);
MainActivity.this.finish();
}
}).setNegativeButton("不玩了", new DialogInterface.OnClickListener() {
@Override // android.content.DialogInterface$OnClickListener
public void onClick(DialogInterface dialog, int which) {
Process.killProcess(Process.myPid());
}
}).show();
}

@Override // android.app.Activity
public void onCreate(Bundle savedInstanceState) {
String str2;
super.onCreate(savedInstanceState);
this.setContentView(0x7F04001A); // layout:activity_main
Log.d("com.gdufs.xman m=", "Xman");
this.getApplication();
int m = MyApp.m;
if(m == 0) {
str2 = "未註冊";
}
else {
str2 = m == 1 ? "已註冊" : "已混亂";
}

this.setTitle("Xman" + str2);
this.btn1 = (Button)this.findViewById(0x7F0B0054); // id:button1
this.btn1.setOnClickListener(new View.OnClickListener() {
@Override // android.view.View$OnClickListener
public void onClick(View v) {
MainActivity.this.getApplication();
if(MyApp.m == 0) {
MainActivity.this.doRegister();
return;
}

((MyApp)MainActivity.this.getApplication()).work();
Toast.makeText(MainActivity.this.getApplicationContext(), MainActivity.workString, 0).show();
}
});
}

@Override // android.app.Activity
public boolean onCreateOptionsMenu(Menu menu) {
this.getMenuInflater().inflate(0x7F0D0000, menu); // menu:menu_main
return 1;
}

public void work(String str) {
MainActivity.workString = str;
}
}

可以看到當m=0時提示未註冊,等於1時提示註冊。

if(m == 0) {
str2 = "未註冊";
}
else {
str2 = m == 1 ? "已註冊" : "已混亂";
}

當m=0時呼叫了另外的方法doRegister。 這個方法其實是前面分析的方法,呼叫了saveSN方法。

if(MyApp.m == 0) {
MainActivity.this.doRegister();
return;
}

我們看一下後面這個塊程式碼, 呼叫了work方法,這個方法的實現是在native層,我們定位一下。

((MyApp)MainActivity.this.getApplication()).work();
Toast.makeText(MainActivity.this.getApplicationContext(), MainActivity.workString, 0).show();
public native void work() {
}

work在ida的地址是14cd。

[Redmi K20 Pro Premium Edition::com.gdufs.xman ]->  {"module":"libmyjni.so","package":"com.gdufs.xman","class":"MyApp","method":"initSN","signature":"()V","address":"13b1"}
{"module":"libmyjni.so","package":"com.gdufs.xman","class":"MyApp","method":"saveSN","signature":"(Ljava/lang/String;)V","address":"11f9"}
{"module":"libmyjni.so","package":"com.gdufs.xman","class":"MyApp","method":"work","signature":"()V","address":"14cd"}

我們去ida看一下:

int __fastcall n3(int a1)
{
int Value; // r0
int v3; // r0
void *v4; // r1
bool v5; // zf

n1(a1);
Value = getValue(a1);
if ( Value )
{
v5 = Value == 1;
v3 = a1;
if ( v5 )
v4 = &unk_2E6B;
else
v4 = &unk_2E95;
}
else
{
v3 = a1;
v4 = &unk_2E5B;
}
return callWork(v3, v4);
}

我們進入一下n1函式,可以看到這裡打開了reg.dat檔案進行讀取操作:

int __fastcall n1(int a1)
{
FILE *v2; // r0
FILE *v3; // r4
int v4; // r0
int v5; // r7
void *v6; // r5
int v8; // r0
int v9; // r1

v2 = fopen("/sdcard/reg.dat", "r+");
v3 = v2;
if ( !v2 )
{
v4 = a1;
return setValue(v4, 0);
}
fseek(v2, 0, 2);
v5 = ftell(v3);
v6 = malloc(v5 + 1);
if ( !v6 )
{
fclose(v3);
v4 = a1;
return setValue(v4, 0);
}
fseek(v3, 0, 0);
fread(v6, v5, 1u, v3);
*((_BYTE *)v6 + v5) = 0;
if ( !strcmp((const char *)v6, "[email protected]") )
{
v8 = a1;
v9 = 1;
}
else
{
v8 = a1;
v9 = 0;
}
setValue(v8, v9);
return j_fclose(v3);
}

我們看一下關鍵程式碼塊。v6是從reg.dat檔案裡讀取出來的資料。 進行比較,如果相同就設定為1,不相同就設定為0。

strcmp函式比較返回值如果相同返回0,所以需要取反。

if ( !strcmp((const char *)v6, "EoPAoY62@ElRD") )
{
v8 = a1;
v9 = 1;
}
else
{
v8 = a1;
v9 = 0;
}

我們看一下setvalue方法。這個方法把0,1這兩個值進行了設定。

進入後我們改一下第一個引數為JNIEnv*,方便識別。

int __fastcall setValue(_JNIEnv *a1, int a2)
{
jclass v4; // r5
jfieldID v5; // r0

v4 = a1->functions->FindClass(a1, "com/gdufs/xman/MyApp");
v5 = a1->functions->GetStaticFieldID(a1, v4, "m", "I");
return ((int (__fastcall *)(_JNIEnv *, jclass, jfieldID, int))a1->functions->SetStaticIntField)(a1, v4, v5, a2);
}

可以看到這裡獲取了com/gdufs/xman/MyApp類裡面的m屬性,型別為int型別,並設定了屬性值。

對應java程式碼如下:

package com.gdufs.xman;

import android.app.Application;
import android.util.Log;

public class MyApp extends Application {
public static int m;

static {
MyApp.m = 0;
System.loadLibrary("myjni");
}

public native void initSN() {
}

@Override // android.app.Application
public void onCreate() {
this.initSN();
Log.d("com.gdufs.xman m=", String.valueOf(MyApp.m));
super.onCreate();
}

public native void saveSN(String arg1) {
}

public native void work() {
}
}

我們已經知道,如果m等於1,那麼就是註冊成功。

那麼怎樣才會等於1呢?只要v6的值為[email protected]就行,v6的值來源於reg.dat,[email protected]這個是真碼。

為13位的,也就是說需要輸入13位註冊碼,才能異或出這個真碼。

!strcmp((const char *)v6, "[email protected]")

那我們直接反解真碼即可。如下是輸入的註冊碼與對應reg.dat裡面的資料:

1111111111111

31 31 31 31 31 31 31 31 31 31 31 31 31

FnPFnPFnPFnPF

46 6E 50 46 6E 50 46 6E 50 46 6E 50 46

我們反解一下密碼,程式碼如下:

public static  void Xor(){

int xorData[]={0x46,0x6E,0x50,0x46,0x6E,0x50,0x46,0x6E,0x50,0x46,0X6E,0X50,0X46};
int xorDataMy[]={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("0x"+Integer.toHexString (xorData[i]^xorDataMy[i]));
// if(i<xorData.length-1){
// System.out.print(",");
// }
System.out.print(xorData[i]^xorDataMy[i]);
if(i<xorData.length-1){
System.out.print(",");
}
}
System.out.print("]");

}

獲得的XOR密碼為:

[119,95,97,119,95,97,119,95,97,119,95,97,119]

我們開始解密,真碼的十六進位制

45 6F 50 41 6F 59 36 32 40 45 6C 52 44

我們列印一下需要異或的真碼資料。

  public static  void Xor1(){

int xorData[]={0x45,0x6f,0x50,0x41,0x6f,0x59,0x36,0x32,0x40,0x45,0x6c,0x52,0x44};
System.out.print("[");
for (int i = 0; i < xorData.length; i++) {

// System.out.print("0x"+Integer.toHexString (xorData[i]^xorDataMy[i]));
// if(i<xorData.length-1){
// System.out.print(",");
// }
System.out.print(xorData[i]);
if(i<xorData.length-1){
System.out.print(",");
}
}
System.out.print("]");

}

我們寫個Python程式碼進行解密:

import binascii

xorkey =[119,95,97,119,95,97,119,95,97,119,95,97,119]
realkey=[69,111,80,65,111,89,54,50,64,69,108,82,68]

def XorDecy(data, l):
ret = []
for i in range(l):
ret.append(data[i] ^ xorkey[i])
s = ''
for i in ret:
s += chr(i)
print(s)
return ret




XorDecy(realkey,len(realkey))

解密結果為:201608Am!2333

我們輸入解密結果:

得到flag為:xman{201608Am!2333}

文章用到的apk:

https://starrysp.lanzoum.com/iLwj108r0v5c

看雪ID:白雲精靈

https://bbs.pediy.com/user-home-814281.htm

*本文由看雪論壇 白雲精靈 原創,轉載請註明來自看雪社群

#

往期推薦

1. C++異常處理控制流下的OLLVM混淆

2. android so檔案攻防實戰-libDexHelper.so反混淆

3. 利用Frida破解黑盒環境的Dex函式抽取殼

4. 繞過iOS 基於svc 0x80的ptrace反除錯

5. 快速定位windows堆溢位

6. CobaltStrike ShellCode詳解

球分享

球點贊

球在看

點選“閱讀原文”,瞭解更多!