查看原文
其他

巧解一道CTF Android题

白云精灵 看雪学苑 2022-08-18


本文为看雪论坛优秀文章

看雪论坛作者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, "EoPAoY62@ElRD") ) { 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的值为EoPAoY62@ElRD就行,v6的值来源于reg.dat,EoPAoY62@ElRD这个是真码。

为13位的,也就是说需要输入13位注册码,才能异或出这个真码。
!strcmp((const char *)v6, "EoPAoY62@ElRD")

那我们直接反解真码即可。如下是输入的注册码与对应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详解






球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存