【愚公系列】2023年05月 攻防世界-MOBILE(LoopCrypto)


前言

1.工具介绍

下面介绍两个反编译工具

  • jadx是一个用于反编译Android APK文件的开源工具,静态反编译,查找索引功能强大
  • jeb和IDA很像,属于动态调试,可以看java汇编也可以生成伪代码,还可以动态attach到目标调试

对于so文件的逆向工具选择

  • IDA逆向工具是一款反汇编器,被广泛应用于软件逆向工程领域,能够反汇编各种不同平台的二进制程序代码,并还原成可读的汇编代码。

Objection是一款移动设备运行时漏洞利用工具,该工具由Frida驱动,可以帮助研究人员访问移动端应用程序,并在无需越狱或root操作的情况下对移动端应用程序的安全进行评估检查。

安装命令

pip3 install objection 

frida是一款便携的、自由的、支持全平台的hook框架,可以通过编写JavaScript、Python代码来和frida_server端进行交互

frida的安装可以参考:https://www.jianshu.com/p/60cfd3f6afde

2.什么是fork

fork之后,操作系统会复制一个与父进程完全相同的子进程,虽说是父子关系,但是在操作系统 看来,他们更像兄弟关系,这2个进程共享代码空间,但是数据空间是互相独立的,子进程数据空间中的内容是父进程的完整拷贝,指令指针也完全相同,但只有一 点不同,如果fork成功,子进程中fork的返回值是0,父进程中fork的返回值是子进程的进程号,如果fork不成功,父进程会返回错误。
可以这样想象,2个进程一直同时运行,而且步调一致,在fork之后,他们分别作不同的工作,也就是分岔了。这也是fork为什么叫fork的原因。

一、LoopCrypto

1.题目

在这里插入图片描述

2.答题

2.1 java层分析

1、jdax来载入该apk,首先先查看APP的入口MainActivity

package com.a.sample.loopcrypto;

import android.os.Bundle;
import android.support.v7.app.c;
import android.widget.Button;
import android.widget.EditText;

/* loaded from: classes.dex */
public class MainActivity extends c {
    
    
    /* JADX INFO: Access modifiers changed from: protected */
    @Override // android.support.v7.app.c, android.support.v4.a.l, android.support.v4.a.h, android.app.Activity
    public void onCreate(Bundle bundle) {
    
    
        super.onCreate(bundle);
        setContentView(R.layout.activity_main);
        Button button = (Button) findViewById(R.id.button);
        button.setText(Decode.a(new byte[]{
    
    78, -65, 73, -45, 103}, 116));
        EditText editText = (EditText) findViewById(R.id.editText);
        editText.setHint(Decode.a(new byte[]{
    
    -72, -55, 35, -43, -108, -108, 93, -1, -91, 92, -39, -30, 44, 110, -127}, 170));
        button.setOnClickListener(new a(editText));
    }
}

在这里插入图片描述
MainActivity仅仅对控件进行了初始化操作,可以看得到显示的字符串都是经过Decode.a()方法解密得到的。

2、进入onClick方法

package com.a.sample.loopcrypto;

import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/* loaded from: classes.dex */
public class a implements View.OnClickListener {
    
    
    private EditText a;

    /* JADX INFO: Access modifiers changed from: package-private */
    public a(EditText editText) {
    
    
        this.a = editText;
    }

    @Override // android.view.View.OnClickListener
    public void onClick(View view) {
    
    
        String str;
        try {
    
    
            Signature[] signatureArr = view.getContext().getPackageManager().getPackageInfo("com.a.sample.loopcrypto", 64).signatures;
            MessageDigest instance = MessageDigest.getInstance("MD5");
            for (Signature signature : signatureArr) {
    
    
                instance.update(signature.toByteArray());
            }
            byte[] digest = instance.digest();
            StringBuilder sb = new StringBuilder();
            for (byte b : digest) {
    
    
                int i = b & 255;
                if (i < 16) {
    
    
                    sb.append("0");
                }
                sb.append(Integer.toHexString(i));
            }
            str = sb.toString();
        } catch (PackageManager.NameNotFoundException | NoSuchAlgorithmException e) {
    
    
            str = "";
        }
        Toast.makeText(view.getContext(), new Decode().check(this.a.getText().toString(), str), 1).show();
    }
}

在这里插入图片描述
a类先是获取了APP的签名,之后对签名进行md5运算,计算之后将用户的输入与签名md5都传入了 new Decode().check()方法中。然后直接将check()方法的返回结果用Toast提示了出来。

3、Decode类

package com.a.sample.loopcrypto;

import java.io.UnsupportedEncodingException;

/* loaded from: classes.dex */
public class Decode {
    
    
    static {
    
    
        System.loadLibrary(a(new byte[]{
    
    46, 1, -100, -4, -87}, 168));
    }

    public static String a(byte[] bArr, int i) {
    
    
        try {
    
    
            return new String(a(bArr, (long) i), "UTF-8");
        } catch (UnsupportedEncodingException e) {
    
    
            return new String(new byte[0]);
        }
    }

    public static byte[] a(byte[] bArr, long j) {
    
    
        for (int i = 0; ((long) i) < j; i++) {
    
    
            for (int i2 = 0; i2 < bArr.length; i2++) {
    
    
                bArr[i2] = (byte) (((bArr[i2] >> 4) & 15) + ((bArr[i2] & 15) << 4));
            }
            for (int length = bArr.length - 1; length >= 0; length--) {
    
    
                if (length != 0) {
    
    
                    bArr[length] = (byte) (bArr[length] ^ bArr[length - 1]);
                } else {
    
    
                    bArr[length] = (byte) (bArr[length] ^ bArr[bArr.length - 1]);
                }
                bArr[length] = (byte) (bArr[length] ^ 150);
            }
            for (int length2 = bArr.length - 1; length2 >= 0; length2--) {
    
    
                if (length2 != 0) {
    
    
                    bArr[length2] = (byte) (bArr[length2] - bArr[length2 - 1]);
                } else {
    
    
                    bArr[length2] = (byte) (bArr[length2] - bArr[bArr.length - 1]);
                }
                bArr[length2] = (byte) (bArr[length2] - 58);
            }
        }
        return bArr;
    }

    public native String check(String str, String str2);
}

在这里插入图片描述
方法的流程如下:

  1. r21-r23:每个字节的八位,前四位与后四位互换
  2. r24-r31:数组每位的值等于与前一位循环异或得到的值
  3. r32-r39:数组的每位循环减去前一位,同时减去58

4、复制java代码进行加密运算

public class App 
{
    
    

    public static String a(byte[] bArr, int i) {
    
    
        try {
    
    
            return new String(a(bArr, (long)i), "UTF-8");
        } catch (UnsupportedEncodingException e) {
    
    
            return new String(new byte[0]);
        }
    }

    public static byte[] a(byte[] bArr, long j) {
    
    
        for (int i = 0; i < j; i++) {
    
    
            for (int i2 = 0; i2 < bArr.length; i2++) {
    
    
                bArr[i2] = (byte) (((bArr[i2] >> 4) & 15) + ((bArr[i2] & 15) << 4));
            }
            for (int length = bArr.length - 1; length >= 0; length--) {
    
    
                if (length != 0) {
    
    
                    bArr[length] = (byte) (bArr[length] ^ bArr[length - 1]);
                } else {
    
    
                    bArr[length] = (byte) (bArr[length] ^ bArr[bArr.length - 1]);
                }
                bArr[length] = (byte) (bArr[length] ^ 150);
            }
            for (int length2 = bArr.length - 1; length2 >= 0; length2--) {
    
    
                if (length2 != 0) {
    
    
                    bArr[length2] = (byte) (bArr[length2] - bArr[length2 - 1]);
                } else {
    
    
                    bArr[length2] = (byte) (bArr[length2] - bArr[bArr.length - 1]);
                }
                bArr[length2] = (byte) (bArr[length2] - 58);
            }
        }
        return bArr;
    }

    public static void main( String[] args )
    {
    
    
        System.out.println("button.setText(\""+a(new byte[]{
    
    78, -65, 73, -45, 103}, 116)+"\");");
        System.out.println("editText.setHint(\""+a(new byte[]{
    
    -72, -55, 35, -43, -108, -108, 93, -1, -91, 92, -39, -30, 44, 110, -127}, 170)+"\");");
        System.out.println("System.loadLibrary(\""+a(new byte[]{
    
    46, 1, -100, -4, -87}, 168)+"\");");
    }
}

在这里插入图片描述
运行结果如下:

button.setText("Check");
editText.setHint("Input your flag");
System.loadLibrary("check");

5、使用Frida来展示,相关hook代码如下:

function main() {
    
    
    Java.perform(function () {
    
    
        Java.use("com.a.sample.loopcrypto.Decode").a.overload("[B", "long").implementation = function (b, i) {
    
    
            var result = this.a(b, i);
            console.log(Java.use("java.lang.String").$new(result));
            return result;
        };
    });
}

setImmediate(main);

启动命令与输出如下所示:

$ frida -H 192.168.0.102:8888 com.a.sample.loopcrypto -l loopcrypto.js

[Remote::com.a.sample.loopcrypto]-> Check
Input your flag

当然在此时并不能使用Frida进行hook,因为该程序在so层添加了检测

2.2 so层的分析

我们开始对so的分析,解压APK,找到其lib/armeabi-v7a/libcheck.so文件,使用IDA32载入。

2.2.1 JNI_OnLoad

当拿到so文件的时候,首先要看其函数表,查看native方法是否为静态注册的,但本题很明显不是,于是寻找JNI_OnLoad函数。

jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
    
    
  jint v2; // r4
  int v4; // [sp+0h] [bp-10h] BYREF

  v2 = 65542;
  if ( (*vm)->GetEnv(vm, (void **)&v4, 65542) )
    return -1;
  if ( !sub_88C4(v4) )
    return -1;
  return v2;
}

在这里插入图片描述
该函数比较简单,获取了JNIEnv后将env传递给了sub_88C4函数,继续跟入该函数。

对该函数的变量名及类型重新定义后如下:

bool __fastcall sub_88C4(JNIEnv *env)
{
    
    
  jclass v2; // r1
  _DWORD method[3]; // [sp+8h] [bp-19Ch] BYREF
  char arg[128]; // [sp+14h] [bp-190h] BYREF
  char methodName[128]; // [sp+94h] [bp-110h] BYREF
  char className[128]; // [sp+114h] [bp-90h] BYREF

  decode_str((int)env, (int)&unk_BD9B, 30, 87, className);
  decode_str((int)env, (int)&unk_BDBA, 5, 122, methodName);
  decode_str((int)env, (int)&unk_BDC0, 56, 49, arg);
  method[0] = methodName;
  method[1] = arg;
  method[2] = sub_87FC;                         // 要注册的check函数地址
  v2 = (*env)->FindClass(env, className);
  return v2 && (*env)->RegisterNatives(env, v2, (const JNINativeMethod *)method, 1) >= 0;
}

其中decode_str()函数如下

char *__fastcall decode_str(JNIEnv *env, char data, int data_len, int arg2, char *result)
{
    
    
  jclass clazz; // r0
  void *clazz_; // r6
  _jmethodID *aID; // r5
  char *v12; // r11
  jbyteArray arg1_; // r8
  jobject v14; // r5
  const char *v15; // r6
  int arg2_; // [sp+8h] [bp-10h]

  clazz = (*env)->FindClass(env, "com/a/sample/loopcrypto/Decode");
  clazz_ = clazz;
  if ( !clazz )
    return 0;
  aID = (*env)->GetStaticMethodID(env, clazz, "a", "([BI)Ljava/lang/String;");
  if ( !aID )
    return 0;
  v12 = result;
  arg2_ = arg2;
  arg1_ = (*env)->NewByteArray(env, data_len);
  (*env)->SetByteArrayRegion(env, arg1_, 0, data_len, (const jbyte *)data);
  v14 = (*env)->CallStaticObjectMethod(env, clazz_, aID, arg1_, arg2_);// 执行Decode.a方法进行解密
  v15 = (*env)->GetStringUTFChars(env, v14, 0); // 从jstring中获取char字符数组
  strcpy(result, v15);
  (*env)->ReleaseStringUTFChars(env, v14, v15);
  return v12;                                   // 解密后字符串数组返回
}

在这里插入图片描述
可见是通过JNI方法来反射调用Decode.a()方法来进行解密,将传入的参数数据扣下来后,可以使用我们上文中的Java代码进行解密,代码如下:

System.out.println(a(new byte[]{
    
    
                0x29, (byte) 0xCF, 0x0B, 0x1E, (byte) 0xDC, (byte) 0xD2, 0x35, (byte) 0xA0, 0x0F, (byte) 0xB1, 0x47, (byte) 0x86, (byte) 0xEA, (byte) 0x90, (byte) 0xE7, (byte) 0x90,
                (byte) 0xDC, 0x30, (byte) 0xE8, (byte) 0x8C, 0x4F, 0x5A, 0x3F, 0x21, (byte) 0x9C, (byte) 0xA8, 0x04, 0x1E, 0x2C, 0x4A
        },87));
        System.out.println(a(new byte[]{
    
    
                0x02, (byte) 0xA1, (byte) 0xE7, 0x0B, (byte) 0xEB
        },122));
        System.out.println(a(new byte[]{
    
    
                (byte) 0xC2, (byte) 0x94, 0x1E, 0x6D, (byte) 0xA6, 0x6E, (byte) 0xF3, 0x3B, (byte) 0xCA, 0x54, (byte) 0xFB, (byte) 0xB2, 0x24, (byte) 0x9F, 0x58, (byte) 0xE3,
                (byte) 0xCF, 0x23, 0x5B, 0x13, 0x4A, (byte) 0x96, 0x01, (byte) 0x89, (byte) 0x9A, (byte) 0x87, (byte) 0x91, 0x17, 0x6B, (byte) 0xD1, 0x3A, (byte) 0xD6,
                (byte) 0xE6, 0x3F, (byte) 0xB0, 0x3E, (byte) 0xAD, 0x0C, 0x05, 0x7A, 0x08, (byte) 0xE2, 0x49, (byte) 0xEC, (byte) 0xA8, (byte) 0x90, 0x14, (byte) 0xAB,
                (byte) 0xD2, (byte) 0xE1, 0x25, (byte) 0x8D, (byte) 0xEE, (byte) 0xD4, 0x64, (byte) 0x84
        },49));

得到的输出如下:

com/a/sample/loopcrypto/Decode
check
(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;

如果碰到复杂的动态注册,我们可以使用https://github.com/lasting-yang/frida_hook_libart中的hook_RegisterNatives.js脚本,通过hook RegisterNatives函数来直接获得动态注册的地址。
在这里插入图片描述

2.2.2 init so的初始化与反调试

如果我们想使用Frida HOOK或者IDA动态调试,是会出现错误的,在使用System.loadLibrary()方法来加载一个so文件时,还需要查看so是否有初始化函数,这些初始化函数会被Linker调用执行,用于完成解密等初始化操作。

返回IDA,使用ctrl+s快捷键来查看so文件的段,可以看到有.init_array段
在这里插入图片描述
当Linker加载so时,就会执行该段的每一个函数,双击进入,可以看到确实有一个初始化函数。
在这里插入图片描述
双击进入该函数,F5查看伪代码,如下图所示。
在这里插入图片描述
在这里插入图片描述
当调试Android应用程序时候,必须调用ptrace(PTRACE_TRACEME)来附加进程。如果进程已经被Trace,则ptrace会失败。利用这一点便可以做反调试。

程序先对字符串进行解密,然后fork一个子进程,之后使用ptrace附加自己防止被调试。之后每隔2秒读取/proc/%d/status文件,检测TracerPid字段,来判断自己是否被附加。

其中解密字符串的脚本如下:

void decode_init_str() {
    
    
    unsigned char format[128] = {
    
    
            0xC6, 0x99, 0x9B, 0x86, 0x8A, 0xC6, 0xCC, 0x8D, 0xC6, 0x9A, 0x9D, 0x88, 0x9D, 0x9C, 0x9A, 0xE9,
            0x9B, 0xE9, 0xBD, 0x9B, 0x88, 0x8A, 0x8C, 0x9B, 0xB9, 0x80, 0x8D, 0xE9, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    };
    format[0] = 47;
    int i;
    for ( i = 1; i != 128; ++i )
        format[i] ^= 0xE9u;
    std::cout<<format<<std::endl;
    std::cout<<&format[16]<<std::endl;
    std::cout<<&format[18]<<std::endl;
}

输出如下:

/proc/%d/status
r
TracerPid

对于Frida Hook初始化的函数,在上文我们已经简要分析了so的加载流程,因此可以hook linker中的call_function函数,当检测到要执行该初始化函数时,便不执行该函数。

Frida脚本代码如下:

function hook_init() {
    
    
    // 寻找linker
    var linker = Process.findModuleByName("linker");
    var call_function_addr = null;
    // 遍历linker的符号
    var symbols = linker.enumerateSymbols();
    for (var i = 0; i < symbols.length; i++) {
    
    
        // 取到名称
        var name = symbols[i].name;
        // 如果名称是call_function,则得到该地址并返回
        if (name.indexOf("call_function") >= 0) {
    
    
            call_function_addr = symbols[i].address;
            break;
        }
    }
    console.log("call_function_addr->", call_function_addr);
    // 来附加call_function函数
    Interceptor.attach(call_function_addr,{
    
    
        onEnter:function(args){
    
    
            // 进入函数的时候判断要执行的函数地址末尾是否是3dd
            if(String(args[1]).indexOf("3dd")>=0){
    
    
                console.log("found");
                // 如果根据函数地址找到了初始化函数,则对其进行hook,置空处理
                Interceptor.replace(
                    args[1],
                    new NativeCallback(
                        function (s, addr, rp) {
    
    
                            console.log("destory")
                        },
                        "int",
                        []
                    )
                );
            }
        },
        onLeave: function(retval){
    
    

        }
    })
    
}

setImmediate(hook_init);

命令及输出如下:

$ frida -H 192.168.0.102:8888 -f com.a.sample.loopcrypto -l loopcrypto.js


Spawning `com.a.sample.loopcrypto`...                                   
call_function_addr-> 0xe7d1453d
Spawned `com.a.sample.loopcrypto`. Use %resume to let the main thread start executing!
[Remote::com.a.sample.loopcrypto]-> %resume
[Remote::com.a.sample.loopcrypto]-> found
destory

在使用hook来停止执行初始化函数后,此时已经可以在Frida附加的情况下进入app了。

当然我们也可以采用IDA patch的方式,但比起Frida过于麻烦。

2.2.3 check函数执行

在绕过了程序的反调试,可以随心所欲的使用Frida后,我们便来分析check函数。

jstring __fastcall check(JNIEnv *a1, jobject a2, jstring a3, jstring a4)
{
    
    
  const char *v7; // r5
  const char *v8; // r6
  char v10[256]; // [sp+4h] [bp-110h] BYREF

  v7 = (*a1)->GetStringUTFChars(a1, a3, 0);
  v8 = (*a1)->GetStringUTFChars(a1, a4, 0);
  sub_8690(v7, v8, v10);
  (*a1)->ReleaseStringUTFChars(a1, a3, v7);
  (*a1)->ReleaseStringUTFChars(a1, a4, v8);
  return (*a1)->NewStringUTF(a1, v10);
}

在这里插入图片描述
该函数从两个参数中获取到字符串数组后,将其传入了check_函数中。继续跟入查看。

在这里插入图片描述
在这里插入图片描述
该函数中,定义了管道来传输数据,然后fork出一个子进程,在子进程中传入APK的md5签名来执行代码的动态解密,可见解密是依靠正确的APK签名的,如果签名不正确,会提示我们改变了签名,这是防止APK被重打包的防范措施。当然我们这里并没有对APK进行重打包。

在对代码动态解密之后,在第22行,传入用户输入的flag和管道,来执行相应的flag校验操作。

而主进程则是从管道中读取子进程的结果,并将提示结果字符串数组返回。

在sub_85E0函数中,先是将数据与签名进行异或操作,之后通过sub_84D0函数得到最终解密出的函数代码。

然后进入some_opt(sub_85E0)函数
在这里插入图片描述
跟入sub_84D0函数,虽然有一个字符串“1.2.11”作为提醒,但除非经验丰富,我们很难将其与zlib1.2.11解压联系起来。

int __fastcall sub_84D0(char *a1, int *a2, int a3, int *a4)
{
    
    
  int v7; // r4
  int v8; // r11
  int v9; // r8
  int v10; // r6
  int v11; // r0
  int v13; // [sp+4h] [bp-4Ch] BYREF
  int v14; // [sp+8h] [bp-48h]
  char *v15; // [sp+10h] [bp-40h]
  int i; // [sp+14h] [bp-3Ch]
  int v17; // [sp+18h] [bp-38h]
  int v18; // [sp+24h] [bp-2Ch]
  int v19; // [sp+28h] [bp-28h]
  int v20; // [sp+2Ch] [bp-24h]
  char v21; // [sp+3Fh] [bp-11h] BYREF

  v7 = *a2;
  v8 = *a4;
  if ( *a2 )
  {
    
    
    *a2 = 0;
  }
  else
  {
    
    
    v7 = 1;
    a1 = &v21;
  }
  v13 = a3;
  v9 = 0;
  v14 = 0;
  v18 = 0;
  v19 = 0;
  v20 = 0;
  v10 = sub_6808(&v13, -15, "1.2.11", 56);
  if ( !v10 )
  {
    
    
    v15 = a1;
    for ( i = 0; ; v9 = i )
    {
    
    
      if ( !v9 )
      {
    
    
        i = v7;
        v7 = 0;
      }
      if ( !v14 )
      {
    
    
        v14 = v8;
        v8 = 0;
      }
      v10 = sub_68EC(&v13, 0);
      if ( v10 )
        break;
    }
    *a4 -= v14 + v8;
    if ( a1 == &v21 )
    {
    
    
      v11 = v7;
      if ( v17 )
        v11 = 1;
      if ( v10 == -5 )
        v7 = v11;
    }
    else
    {
    
    
      *a2 = v17;
    }
    sub_7C22(&v13);
    if ( v10 == 1 )
    {
    
    
      return 0;
    }
    else if ( v10 == 2 )
    {
    
    
      return -3;
    }
    else if ( v10 == -5 && v7 + i )
    {
    
    
      return -3;
    }
  }
  return v10;
}

在这里插入图片描述
此时我们便可以通过Frida来hook获取该函数的解密结果,需要注意的是,该函数是运行在子进程中的,因此我们需要附加到子进程之上,才能hook到结果。

在运行着之前去除反调试的Frida脚本的前提下,编写如下python脚本并运行

import frida

device = frida.get_device_manager().add_remote_device("192.168.0.102:8888")
def on_spawned(spawn):
    print('on_spawned:', spawn)
    # 附加子进程的pid
    session1 = device.attach(spawn.pid)
    # 编写脚本hook
    script1 = session1.create_script("""
    var libcheck_addr =  Module.findBaseAddress("libcheck.so");
    console.log("libcheck_addr",libcheck_addr)
    // 对代码解密函数进行hook,thumb模式需要+1
    Interceptor.attach(libcheck_addr.add(0x85e0+1),{
        onEnter:function(args){
            console.log("0x85e0 onEnter")
            // var buffer = Memory.readByteArray(libcheck_addr.add(0xF1B0), 623);
            // console.log(buffer)
        }
        ,
        onLeave:function(retval){
            console.log("0x85e0 onLeave",retval)
            var buffer = Memory.readByteArray(retval, 0x270);
            
            console.log(buffer)
    
        }
    })
""").load()
    # 唤醒子进程
    device.resume(spawn.pid)
    
# 添加创建子进程回调
device.on('child-added', on_spawned)

session = device.attach("com.a.sample.loopcrypto")
# 开启子进程控制模式
session.enable_child_gating()

with open("./loopcrypto.js") as f:
    # 创建一个新脚本
    script = session.create_script(f.read())

# 加载脚本
script.load()

command = ""
while True:
    command = input("Enter `n` for leave: ")
    if command == "n":
        break

点击按钮来主动执行check函数后,运行结果如下图所示。

在这里插入图片描述
可见其已经成功打印了解密出的函数代码数据。

编写idapython脚本,将该段数据写入原来的数据段中,虽然会损坏文件,但方便我们进行静态分析。

import idc

addr = 0xF1B0
func = [0x2d, 0xe9, 0xf0, 0x4f, 0xad, 0xb0, 0x44, 0xf6, 0xa4, 0x61, 0x47, 0xf2, 0x7b, 0x4b, 0xc2, 0xf2,
0xbb, 0x61, 0x46, 0xf6, 0x66, 0x4e, 0x23, 0x91, 0x4d, 0xf6, 0x43, 0x41, 0xc6, 0xf6, 0xbb, 0x11,
0x46, 0xf6, 0x5f, 0x6a, 0x24, 0x91, 0x42, 0xf6, 0x5c, 0x71, 0xcd, 0xf6, 0x3f, 0x31, 0x03, 0xaa,
0x25, 0x91, 0x42, 0xf6, 0x9f, 0x71, 0xc7, 0xf6, 0xbd, 0x31, 0xc6, 0xf6, 0x48, 0x1b, 0x26, 0x91,
0x4f, 0xf6, 0x31, 0x61, 0xc6, 0xf2, 0xcd, 0x51, 0xc6, 0xf2, 0x61, 0x7e, 0x27, 0x91, 0x4a, 0xf6,
0xc4, 0x61, 0xc3, 0xf6, 0x91, 0x61, 0xc3, 0xf2, 0x25, 0x7a, 0x28, 0x91, 0x4a, 0xf6, 0x94, 0x71,
0xce, 0xf2, 0x03, 0x71, 0x29, 0x91, 0x4e, 0xf2, 0xf4, 0x11, 0xcc, 0xf2, 0xd1, 0x71, 0x2a, 0x91,
0x49, 0xf2, 0x20, 0x21, 0xc8, 0xf2, 0x1a, 0x21, 0x19, 0x91, 0x44, 0xf2, 0xf3, 0x31, 0xc9, 0xf2,
0xcd, 0x61, 0x1a, 0x91, 0x48, 0xf2, 0xba, 0x11, 0xc3, 0xf2, 0xd4, 0x61, 0x1b, 0x91, 0x40, 0xf2,
0x6d, 0x71, 0xcc, 0xf6, 0x2c, 0x31, 0x1c, 0x91, 0x45, 0xf2, 0x68, 0x71, 0xc0, 0xf6, 0xc6, 0x51,
0x1d, 0x91, 0x4d, 0xf6, 0x5d, 0x61, 0xcb, 0xf6, 0xb4, 0x21, 0x1e, 0x91, 0x49, 0xf6, 0x0f, 0x01,
0xc7, 0xf2, 0x71, 0x51, 0x1f, 0x91, 0x4e, 0xf6, 0xc7, 0x41, 0xce, 0xf2, 0x82, 0x31, 0x20, 0x91,
0x4e, 0xf6, 0xe0, 0x01, 0xcc, 0xf2, 0x33, 0x71, 0x21, 0x91, 0x42, 0xf6, 0x26, 0x71, 0xc7, 0xf2,
0x81, 0x41, 0x22, 0x91, 0x45, 0xf6, 0xa0, 0x51, 0xc1, 0xf6, 0xa0, 0x51, 0x0f, 0x91, 0x4e, 0xf2,
0x28, 0x31, 0xc0, 0xf6, 0x07, 0x31, 0x10, 0x91, 0x4d, 0xf2, 0x78, 0x71, 0xcb, 0xf2, 0x72, 0x51,
0x11, 0x91, 0x42, 0xf2, 0xbb, 0x71, 0xcc, 0xf2, 0xee, 0x31, 0x12, 0x91, 0x45, 0xf2, 0x1b, 0x41,
0xcd, 0xf2, 0x00, 0x21, 0x13, 0x91, 0x4c, 0xf6, 0xe0, 0x01, 0xca, 0xf2, 0x95, 0x51, 0x14, 0x91,
0x4a, 0xf2, 0xc8, 0x51, 0xc4, 0xf2, 0xb3, 0x41, 0x15, 0x91, 0x41, 0xf2, 0x04, 0x61, 0xc2, 0xf2,
0xf1, 0x11, 0x16, 0x91, 0x4b, 0xf6, 0x82, 0x21, 0xcd, 0xf6, 0x1e, 0x51, 0x17, 0x91, 0x4f, 0xf2,
0x9c, 0x11, 0xcd, 0xf2, 0xcb, 0x41, 0x18, 0x91, 0x00, 0x21, 0xd0, 0xe9, 0x00, 0x40, 0x00, 0x90,
0x45, 0xf6, 0x53, 0x70, 0xc2, 0xf2, 0x21, 0x40, 0x65, 0x5c, 0x1d, 0xb1, 0x55, 0x54, 0x01, 0x31,
0x27, 0x29, 0xf9, 0xd3, 0x00, 0x24, 0x54, 0x54, 0x01, 0x31, 0x01, 0x91, 0x43, 0xd0, 0x47, 0xf6,
0xb9, 0x16, 0x4f, 0xf0, 0x00, 0x09, 0xc9, 0xf6, 0x37, 0x66, 0x02, 0xeb, 0x09, 0x01, 0x52, 0xf8,
0x09, 0x50, 0x02, 0x91, 0x4f, 0x68, 0x20, 0x24, 0x31, 0x46, 0x01, 0xeb, 0x07, 0x0c, 0x0e, 0xeb,
0x07, 0x18, 0x88, 0xea, 0x0c, 0x02, 0x0b, 0xeb, 0x57, 0x13, 0x5a, 0x40, 0x01, 0x3c, 0x15, 0x44,
0x01, 0xeb, 0x05, 0x02, 0x31, 0x44, 0x00, 0xeb, 0x05, 0x13, 0x82, 0xea, 0x03, 0x02, 0x0a, 0xeb,
0x55, 0x13, 0x82, 0xea, 0x03, 0x02, 0x17, 0x44, 0xe7, 0xd1, 0x02, 0x99, 0x03, 0xaa, 0x42, 0xf8,
0x09, 0x50, 0x09, 0xf1, 0x08, 0x09, 0x4f, 0x60, 0x01, 0x99, 0x89, 0x45, 0xd5, 0xd3, 0x01, 0x99,
0x89, 0xb1, 0x9d, 0xf8, 0x0c, 0x10, 0xa4, 0x29, 0x0a, 0xd1, 0x23, 0xa9, 0x01, 0x24, 0x01, 0x9b,
0x9c, 0x42, 0x08, 0xd2, 0x66, 0x1c, 0x0b, 0x5d, 0x17, 0x5d, 0x34, 0x46, 0x9f, 0x42, 0xf6, 0xd0,
0x0d, 0xf1, 0x3c, 0x0c, 0x06, 0xe0, 0x01, 0x9a, 0x0f, 0xa9, 0x0d, 0xf1, 0x64, 0x0c, 0x00, 0x2a,
0x08, 0xbf, 0x8c, 0x46, 0x48, 0xf2, 0x47, 0x64, 0x4f, 0xf0, 0x00, 0x09, 0xc6, 0xf2, 0xc8, 0x14,
0x0c, 0xeb, 0x09, 0x08, 0x5c, 0xf8, 0x09, 0x70, 0x43, 0xf2, 0x20, 0x75, 0xd8, 0xf8, 0x04, 0x60,
0xcc, 0xf2, 0xef, 0x65, 0x20, 0x21, 0x00, 0xeb, 0x07, 0x12, 0x0a, 0xeb, 0x57, 0x13, 0x5a, 0x40,
0xeb, 0x19, 0x5a, 0x40, 0x01, 0x39, 0xa6, 0xeb, 0x02, 0x06, 0x05, 0xeb, 0x06, 0x02, 0x25, 0x44,
0x0e, 0xeb, 0x06, 0x13, 0x82, 0xea, 0x03, 0x02, 0x0b, 0xeb, 0x56, 0x13, 0x82, 0xea, 0x03, 0x02,
0xa7, 0xeb, 0x02, 0x07, 0xe7, 0xd1, 0x09, 0xf1, 0x08, 0x09, 0xc8, 0xe9, 0x00, 0x76, 0xb9, 0xf1,
0x28, 0x0f, 0xd5, 0xd3, 0x00, 0x98, 0x28, 0x23, 0x46, 0x68, 0x30, 0x46, 0x61, 0x46, 0x1a, 0x46,
0x4f, 0xf0, 0x04, 0x07, 0x00, 0xdf, 0x00, 0x20, 0x2d, 0xb0, 0xbd, 0xe8, 0xf0, 0x8f]
for i in range(len(func)):
    idc.patch_byte(addr+i,func[i])

之后将该部分alt+G切换为thumb模式,摁C转成代码,摁P声明成函数,便可以使用F5了。

在解密的函数代码中,先是对输入的字符进行tea加密,之后在103行到109行进行比对,如果不正确则会分别对字符串提示v26和v25进行解密并返回提示结果。

我们便可以编写脚本,来分别对flag和提示字符串进行解密,脚本代码如下:

#include <iostream>


void destr();


void tea_decode1(uint32_t *origin, uint32_t *key) {
    
    
    uint32_t v0 = origin[0], v1 = origin[1], i;  /* set up */
    uint32_t delta = 0x61C88647;
    uint32_t sum = 0xC6EF3720;
    uint32_t k0 = key[0], k1 = key[1], k2 = key[2], k3 = key[3];   /* cache key */
    for (i = 0; i < 32; i++) {
    
                             /* basic cycle start */
        v1 -= ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);
        v0 -= ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);
        sum += delta;
    }                                              /* end cycle */
    origin[0] = v0;
    origin[1] = v1;
}


int main() {
    
    
    destr();
    uint32_t v27[8];
    v27[0] = 0x26BB4EA4;
    v27[1] = 0x69BBDC43;
    v27[2] = 0xDB3F2F5C;
    v27[3] = 0x7BBD2F9F;
    v27[4] = 0x65CDFE31;
    v27[5] = 0x3E91AEC4;
    v27[6] = 0xE703AF94;
    v27[7] = 0xC7D1E1F4;
    uint32_t key[4];
    key[0] = 0x67616C66;
    key[1] = 0x6948747B;
    key[2] = 0x24215F53;
    key[3] = 0x37256E5F;

    for (int i = 0; i < 8; i += 2) {
    
    
        tea_decode1(&v27[i], key);
    }
    std::cout << (char *) v27 << std::endl;
    return 0;
}

void destr() {
    
    
    uint32_t v26[10], v25[10];
    v26[0] = 0x821A9220;
    v26[1] = 0x96CD43F3;
    v26[2] = 0x36D481BA;
    v26[3] = 0xCB2C076D;
    v26[4] = 0xDC65768;
    v26[5] = 0xBAB4DE5D;
    v26[6] = 0x7571980F;
    v26[7] = 0xE382ECC7;
    v26[8] = 0xC733E8E0;
    v26[9] = 0x74812F26;
    v25[0] = 0x1DA05DA0;
    v25[1] = 0xB07E328;
    v25[2] = 0xB572D778;
    v25[3] = 0xC3EE27BB;
    v25[4] = 0xD200541B;
    v25[5] = 0xA595C8E0;
    v25[6] = 0x44B3A5C8;
    v25[7] = 0x21F11604;
    v25[8] = 0xDD1EBA82;
    v25[9] = 0xD4CBF19C;
    uint32_t key[4];
    key[0] = 0x67616C66;
    key[1] = 0x6948747B;
    key[2] = 0x24215F53;
    key[3] = 0x37256E5F;

    for (int i = 0; i < 10; i += 2) {
    
    
        tea_decode1(&v25[i], key);
    }
    std::cout << (char *) v25 << std::endl;

    for (int i = 0; i < 10; i += 2) {
    
    
        tea_decode1(&v26[i], key);
    }
    std::cout << (char *) v26 << std::endl;
}

在这里插入图片描述
得到flag:flag{LOoK|N9_An_3@&9_s%Lue?!?!}

猜你喜欢

转载自blog.csdn.net/aa2528877987/article/details/130461450