巧解一道CTF Android题
2022-8-10 18:2:26 Author: 看雪学苑(查看原文) 阅读量:23 收藏


本文为看雪论坛优秀文章

看雪论坛作者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详解

球分享

球点赞

球在看

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


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458461590&idx=1&sn=719ca0cd4fa9dfbd674fbd9a0aa7e010&chksm=b18e151c86f99c0a7ee78ce75e18069d0e9a9d175a5ab181b27622a3a3a39f923bcf0af9405c#rd
如有侵权请联系:admin#unsafe.sh