长亭百川云 - 文章详情

DASCTF 2024赛题解析:Reverse-BabyAndroid

看雪学苑

74

2024-08-08

BabyAndroid

题型:安卓-Hook-Native分析

DAS46-BabyAndroid.zip

题目不支持模拟器运行,只能真机。附件里面除了APK,还有一段抓包后的响应体。出题人说是加密后的数据,所以我们的目标就是解密这段数据了。

看看数据形式:

TwMkYUkg4bYsY0hL99ggYWnVjWyXQrWAdNmToB0eBXbS6wBzL6ktorjNWI9VOroTU4HgIUYyzGLpcHzd1zNGT+bFZZI7IoxJwpcgXfdwW1LSmiNSP+PuSUsqAzNclF1nJ07b4tYyLWg0zTypbzWsLhOIM+6uci3RFZLREUCALafi01M8mS+KMNxX1Pyn8mSP+KKKjQ5S5fasHRSn+L9qBFws0mWavpfI0QEiMgarxv0iGhYU8cfgonWyL70RvoXET5VUDP1vfYWIBLzzzaAqLC0OiMtUK3TTATSU7yijdgXm18OKMcGIke/NZIM6Sr5fL3t6psDOOkw2C/5uYrJVPn+D6U9KTL64bgREppDqMOvhvbhtuf/S3ASW/+rhtPMtoaD8FxDg0wWSLZA53fQfNA==

有两个==,
我们可以推测里面可能至少会用到base64编码。嗯,基本不可能只考base64。但具体是什么加密,我们目前不得而知。

故,用上算法助手,
算法助手作用域勾选
+算法助手开关,
同时勾选算法分析4个开关。

打开目标软件,让我们设置图形解锁。无所谓,这个不是关键点。进入软件后就会发现软件功能就是做笔记的,我们尝试下做个笔记。

然后打开算法助手:

非常nice,分析出了加密的算法AES/ECB/PKCS5Padding。
而下面的SHA-256,不用管,没有人拿这个当加密算法的,解密不出来的。

我们点进去看看。

加密密钥都已经被Hook出来了,
DQ0NDQ0NDQ0NDQ0NDQ0NDQ==

而加密的内容从1变成49,这中间应该还有一些操作,不可能就这么梭出来的,我们拿密钥和密文去在线网站上解密。

解密出来的明显不是最后的flag。我们可以验证下,用49.000000
和密钥进行加密,看看结果是不是FXb5k9BUw5T1EkGuNStrRw==。

嗯,结果确实是,那至少我们解出来了一层加密,接下来我们就要开始看dex了。不过我们不直接看,我们先看看算法助手里面的调用堆栈,看看是哪个函数调用了刚刚的加密。

还是刚刚的图片,我们看到其中有一行是

at site.qifen.note.ui.Encrypto.encrypt(Encrypto.java:37)

那我们先看看加密函数吧。

MT查看Dex:

我们发现了上面这个,但这明显和刚刚不一样啊,一个0一个o,又找不到带o的。这里面肯定是有蹊跷的,我们先点进去看看。

package site.qifen.note.ui;  
  
import android.util.Base64;  
import java.security.MessageDigest;  
import javax.crypto.Cipher;  
import javax.crypto.spec.SecretKeySpec;  
  
public class Encrypt0 {  
    private static final String KEY = "DSACTF";  
    private static final String TAG = "Encrypto";  
  
    public static String encrypt(String data) throws Exception {  
        MessageDigest sha = MessageDigest.getInstance("SHA-1");  
        byte\[\] keyBytes = sha.digest(KEY.getBytes("UTF-8"));  
        byte\[\] keyBytes16 = new byte\[16\];  
        System.arraycopy(keyBytes, 0, keyBytes16, 0, 16);  
        SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes16, "AES");  
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");  
        cipher.init(1, secretKeySpec);  
        byte\[\] encryptedBytes = cipher.doFinal(data.getBytes("UTF-8"));  
        return Base64.encodeToString(encryptedBytes, 2);  
    }  
}

加密是这个加密类型,但明显key不对劲。这里显示的key是DASCTF。
那这时候

我们就需要考虑是不是这东西从其他地方加载了dex(例如PYCC2024的challengemobile

其实,我们查看APK的时候可以发现,在assets目录下有个sex.jpg,但它又不是图片。

大概这个就存储了dex数据,我们用算法助手看看是不是这样。

重启,保存一个笔记。

然后发现确实是读入了sex.jpg。

我们点进去看调用堆栈,发现这两个比较特殊。

我们再用MT找到先相关的函数。

public byte\[\] loadData(String str) {  
        try {  
            InputStream open = getAssets().open(str);  
            byte\[\] encryptedData = new byte\[open.available()\];  
            open.read(encryptedData);  
            open.close();  
            byte\[\] key = "DASCTF".getBytes();  
            return rc4Decrypt(key, encryptedData);  
        } catch (IOException e) {  
            Log.e("错误", "加载数据时发生错误", e);  
            return null;  
        }  
    }  
  
    private byte\[\] rc4Decrypt(byte\[\] key, byte\[\] data) {  
        int\[\] S = new int\[256\];  
        for (int i = 0; i < 256; i++) {  
            S\[i\] = i;  
        }  
        int j = 0;  
        for (int i2 = 0; i2 < 256; i2++) {  
            j = ((S\[i2\] + j) + (key\[i2 % key.length\] & 255)) % 256;  
            int temp = S\[i2\];  
            S\[i2\] = S\[j\];  
            S\[j\] = temp;  
        }  
        int i3 = data.length;  
        byte\[\] result = new byte\[i3\];  
        int i4 = 0;  
        int j2 = 0;  
        for (int k = 0; k < data.length; k++) {  
            i4 = (i4 + 1) % 256;  
            j2 = (S\[i4\] + j2) % 256;  
            int temp2 = S\[i4\];  
            S\[i4\] = S\[j2\];  
            S\[j2\] = temp2;  
            int t = (S\[i4\] + S\[j2\]) % 256;  
            result\[k\] = (byte) (data\[k\] ^ S\[t\]);  
        }  
        return result;  
    }

看看如上代码,loadData
函数 getAssest 获取文件,然后调用rc4解密函数和一个key来解密,最终返回所解密的内容。那我们直接HookloadData
函数,来看看。

但这里我们要换成SimpleHookR 或者用Frida,因为最新版算法助手自动把数据转换成编码字节集了,我们用SimpleHookR记录这个函数的参返。

我们把返回值复制下来,用python脚本把其转换化成dex文件。

def to\_unsigned\_bytes(byte\_list):  
    return bytes(\[(b + 256) % 256 for b in byte\_list\])  
  
your\_byte\_list = \[数据\]  
converted\_bytes = to\_unsigned\_bytes(your\_byte\_list)  
  
with open('dump.dex', 'wb') as file:  
    file.write(converted\_bytes)  
  
print("Data written to dump.dex")

用MT打开试试。

发现了我们之前在找的Encrypto。

package site.qifen.note.ui;  
  
import android.util.Base64;  
import javax.crypto.Cipher;  
import javax.crypto.spec.SecretKeySpec;  
  
public class Encrypto {  
    private static final String KEY = "DSACTF";  
    private static final String TAG = "Encrypto";  
  
    private static byte\[\] customHash(String input) {  
        byte\[\] keyBytes = new byte\[16\];  
        int\[\] temp = new int\[16\];  
        for (int i = 0; i < input.length(); i++) {  
            int charVal = input.charAt(i);  
            for (int j = 0; j < 16; j++) {  
                temp\[j\] = ((temp\[j\] \* 31) + charVal) % 251;  
            }  
        }  
        for (int i2 = 0; i2 < 16; i2++) {  
            keyBytes\[i2\] = (byte) (temp\[i2\] % 256);  
        }  
        return keyBytes;  
    }  
  
    public static String encrypt(String data) throws Exception {  
        byte\[\] keyBytes = customHash(KEY);  
        SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "AES");  
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");  
        cipher.init(1, secretKeySpec);  
        byte\[\] encryptedBytes = cipher.doFinal(data.getBytes("UTF-8"));  
        return Base64.encodeToString(encryptedBytes, 2);  
    }  
}

现在我们去看看另一个红框里的东西。

package site.qifen.note.ui;  
  
import android.os.AsyncTask;  
import android.os.Build;  
import android.util.Log;  
import dalvik.system.InMemoryDexClassLoader;  
import java.lang.reflect.Method;  
import java.nio.ByteBuffer;  
import java.text.SimpleDateFormat;  
import java.util.Date;  
import site.qifen.note.model.Note;  
import site.qifen.note.model.sendRequest;  
import site.qifen.note.util.NoteUtil;  
  
class NoteActivity$EncryptAndSendTask extends AsyncTask<String, Void, String> {  
    final NoteActivity this$0;  
  
    private NoteActivity$EncryptAndSendTask(NoteActivity noteActivity) {  
        this.this$0 = noteActivity;  
    }  
  
    @Override  
    public String doInBackground(String... params) {  
        String contentText = params\[0\];  
        try {  
            byte\[\] dexData = this.this$0.loadData("Sex.jpg");  
            ByteBuffer dexBuffer = ByteBuffer.wrap(dexData);  
            InMemoryDexClassLoader classLoader = null;  
            if (Build.VERSION.SDK\_INT >= 26) {  
                classLoader = new InMemoryDexClassLoader(dexBuffer, this.this$0.getClassLoader());  
            }  
            Class<?> checkerClass = classLoader.loadClass("site.qifen.note.ui.Encrypto");  
            Method checkMethod = checkerClass.getMethod("encrypt", String.class);  
            this.this$0.contentText\_back = contentText;  
            String cipher = (String) checkMethod.invoke(checkerClass.getDeclaredConstructor(new Class\[0\]).newInstance(new Object\[0\]), this.this$0.sendInit(contentText));  
            String response = sendRequest.sendPost("http://yuanshen.com/", "data=" + cipher);  
            Log.d("JNITest", "Server Response: " + response);  
            return cipher;  
        } catch (Exception e) {  
            e.printStackTrace();  
            return null;  
        }  
    }  
  
    @Override  
    public void onPostExecute(String cipher) {  
        if (cipher != null) {  
            String titleText = this.this$0.noteWriteTitleEdit.getText().toString();  
            String tagText = this.this$0.noteWriteTagEdit.getText().toString();  
            String date = new SimpleDateFormat("yyyy-MM-dd HH:mm").format(new Date());  
            if (NoteActivity.access$100(this.this$0) == null) {  
                NoteActivity.access$200(this.this$0).insertNote(new Note(tagText, titleText, this.this$0.contentText\_back, date, false));  
                NoteUtil.toast("保存成功");  
                this.this$0.finish();  
                return;  
            }  
            NoteActivity.access$100(this.this$0).setTitle(titleText);  
            NoteActivity.access$100(this.this$0).setContent(this.this$0.contentText\_back);  
            NoteActivity.access$100(this.this$0).setDate(date);  
            NoteActivity.access$100(this.this$0).setTag(this.this$0.contentText\_back);  
            NoteActivity.access$200(this.this$0).updateNote(NoteActivity.access$100(this.this$0));  
            NoteUtil.toast("修改成功");  
            this.this$0.finish();  
            return;  
        }  
        NoteUtil.toast("加密失败");  
    }  
}

我们可以发现,就是doInBackground
函数调用了loadData,
并且还使用了classLoader
去加载我们刚刚得到得到的类。

然后我们发现cipher
就是加密后的数据,它是由我们输入的文本经sendInit
处理,然后再被刚刚加载的类加密得来的。

我们跳转去看看sendInit。

原来就在刚刚的NoteActivity里面。

这里native,
说明我们要去分析/lib里面的so文件了。当然,我们可以先Hook一下,验证一下。

参数1
返回49.000000

没问题,就是他。我们用ida64打开so,我们直接找带sendInit
的。

v20 = \*(\_QWORD \*)(\_ReadStatusReg(ARM64\_SYSREG(3, 3, 13, 0, 2)) + 40);  
StringUTFChars = \_JNIEnv::GetStringUTFChars(a1, a3, 0LL);  
sub\_15994(v19, StringUTFChars);  
\_JNIEnv::ReleaseStringUTFChars(a1, a3, StringUTFChars);  
v8 = sub\_15A40(v19);  
v7 = sub\_15AB4(v19);  
std::vector<int>::vector<std::\_\_wrap\_iter<char \*>>(v18, v8, v7);  
encrypt(v18);  
sub\_15C34(v16);  
v15 = sub\_15C74(v17);  
v14 = sub\_15CB4(v17);  
while ( (sub\_15CF0(&v15, &v14) & 1) != 0 )  
{  
    v3 = (std::\_\_ndk1 \*)sub\_15D38(&v15);  
    std::to\_string(v3, \*(double \*)v3);  
    sub\_15D50(v12, ",");  
    sub\_15D98(v16, v13);  
    std::string::~string(v13);  
    std::string::~string(v12);  
    sub\_15E34(&v15);  
}  
if ( (sub\_15E5C(v16) & 1) == 0 )  
    sub\_15EA0(v16);  
v6 = (char \*)sub\_15FD4(v16);  
v5 = \_JNIEnv::NewStringUTF(a1, v6);  
std::string::~string(v16);  
sub\_15668(v17);  
sub\_15FF8(v18);  
std::string::~string(v19);  
\_ReadStatusReg(ARM64\_SYSREG(3, 3, 13, 0, 2));  
return v5;  
}

我刚看到这个伪代码的时候确实是崩溃的,因为我伪代码分析能力太弱了,压根看不懂,丢给chatgpt都没救。

但看到都有2个解了,就一个一个点进去看,到encrypt(v18);

这个其实满显眼的函数时:

v12\[1\] = \*(\_QWORD \*)(\_ReadStatusReg(ARM64\_SYSREG(3, 3, 13, 0, 2)) + 40);  
  v10 = sub\_15548(a1);  
  v12\[0\] = 0LL;  
  result = (double \*)std::vector<double>::vector(a2, v10, v12);  
  for ( i = 0; i < v10; ++i )  
  {  
    for ( j = 0; j < v10; ++j )  
    {  
      v7 = (double)\*(int \*)sub\_15608(a1, j);  
      v5 = cos(((double)j + 0.5) \* ((double)i \* 3.14159265) / (double)v10) \* v7;  
      v3 = (double \*)sub\_15638(a2, i);  
      \*v3 = \*v3 + v5;  
    }  
    if ( i )  
      v4 = sqrt(2.0 / (double)v10);  
    else  
      v4 = sqrt(1.0 / (double)v10);  
    result = (double \*)sub\_15638(a2, i);  
    \*result = \*result \* v4;  
  }  
  \_ReadStatusReg(ARM64\_SYSREG(3, 3, 13, 0, 2));  
  return result;  
}

直觉告诉我,就是这个了,丢给chatgpt,分析是什么离散啥啥的,简称DCT
又问有没有逆向的。

chatgpt给出了IDCT的python脚本:

import numpy as np  
  
def idct(dct\_data):  
    N = len(dct\_data)  
    result = np.zeros(N)  
  
    for n in range(N):  
        sum\_value = 0.0  
        for k in range(N):  
            cos\_term = np.cos((k \* 3.14159265 \* (n + 0.5)) / N)  
            if k == 0:  
                sum\_value += dct\_data\[k\] \* np.sqrt(1.0 / N) \* cos\_term  
            else:  
                sum\_value += dct\_data\[k\] \* np.sqrt(2.0 / N) \* cos\_term  
        result\[n\] = sum\_value  
  
    return result  
  
def decrypt\_to\_ascii(dct\_data):  
    # 执行IDCT解密  
    decrypted\_data = idct(dct\_data)  
    # 四舍五入并转化为整数  
    int\_data = np.rint(decrypted\_data).astype(int)  
    # 转换为ASCII字符  
    char\_data = \[chr(num) for num in int\_data\]  
    return ''.join(char\_data)  
  
\# 使用 DCT 变换后的数据  
encrypted\_data = \[458.853181,-18.325492,-18.251911,-2.097520,-21.198660,-22.304648,21.103162,-5.786284,-15.248906,15.329286,16.919499,-19.669045,30.928253,-37.588034,-16.593954,-5.505211,3.014744,6.553616,31.131491,16.472500,6.802400,-78.278577,15.280099,3.893073,56.493581,-34.576344,30.146729,4.445671,6.732204\]  
  
\# 解密并转换为ASCII字符  
decrypted\_message = decrypt\_to\_ascii(encrypted\_data)  
  
\# 打印解密后的消息  
print("Decrypted message:", decrypted\_message)

运行脚本就会得到flag。

看雪ID:sffool

https://bbs.kanxue.com/user-home-988654.htm

*本文为看雪论坛优秀文章,由 sffool 原创,转载请注明来自看雪社区



# 往期推荐

1.告别RegisterNatives获取JNI函数绑定的地址,迎接最底层的方式获取(3个案例)

2.内核漏洞学习记录(CVE-2021-22555)

3.corCTF 2024:位运算虚拟机及gpu hash爆破

4.Charles + Clash + Postern 对外网 App Vpx 抓包

5.修补微信Windows隐藏的深色模式

点击阅读原文查看更多

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备2024055124号-2