文章详细介绍了2025 VNCTF比赛中逆向方向的解题过程,包括Android逆向、加密解密、Linux提权等,涉及多种技术手段和工具使用。
AI 摘要

Hook Fish

字符串经过加密后,用加载下载的dex的check去检测

可以直接在data找到下载的dex

打开后看到了密文,其他可以直接cope到IDE

然后直接写exp即可(用爆破也可)

public class Main {
    public static String decrypt(String encryptedStr) {
        char[] encryptedChars = encryptedStr.toCharArray();

        for (int i = 0; i < encryptedChars.length; i++) {
            char c = encryptedChars[i];
            int originalAscii;
            if (c >= 'g' && c <= 'y') {
                originalAscii = c - '7' - (i % 10);
            } else {
                originalAscii = (c - (i % 4)) + '1';
            }
            encryptedChars[i] = (char) originalAscii;
        }

        code2(encryptedChars, 0);

        String hexString = new String(encryptedChars);

        byte[] bytes = new byte[hexString.length() / 2];
        for (int i = 0; i < bytes.length; i++) {
            int index = i * 2;
            String hexPair = hexString.substring(index, index + 2);
            bytes[i] = (byte) Integer.parseInt(hexPair, 16);
        }

        for (int i = 0; i < bytes.length; i++) {
            bytes[i] -= 68;
        }

        return new String(bytes);
    }

    private static void code2(char[] a, int index) {
        if (index >= a.length - 1) {
            return;
        }
        a[index] ^= a[index + 1];
        a[index + 1] ^= a[index];
        a[index] ^= a[index + 1];
        code2(a, index + 2);
    }

    public static void main(String[] args) throws Exception {
        System.out.println(decrypt("0qksrtuw0x74r2n3s2x3ooi4ps54r173k2os12r32pmqnu73r1h432n301twnq43prruo2h5"));
    }
}

Kotlindroid

Base64 + AES/GCM/NoPadding

利用Frida Hook出aad和key

Java.perform(function () {
    var SecretKeySpec = Java.use('javax.crypto.spec.SecretKeySpec');
    SecretKeySpec.$init.overload('[B', 'java.lang.String').implementation = function (keyBytes, algorithm) {
        console.log('SecretKeySpec constructor called with key: ' + keyBytes);
        var byteArrayStr = '';
        for (var i = 0; i < keyBytes.length; i++) {
            byteArrayStr += String.fromCharCode(keyBytes[i]);
        }
        console.log('Key byte array as string: ' + byteArrayStr);
        return this.$init(keyBytes, algorithm);
    };

    var JNI = Java.use('com.atri.ezcompose.JNI');
    JNI.native_natget.overload('[B').implementation = function (byteArray) {
        console.log('native_natget called with byteArray: ' + byteArray);
        var byteArrayStr = '';
        for (var i = 0; i < byteArray.length; i++) {
            byteArrayStr += String.fromCharCode(byteArray[i]);
        }
        console.log('Byte array content: ' + byteArrayStr);
        var result = this.native_natget(byteArray);
        console.log('native_natget returned: ' + result);
        return result;
    };
    var jniInstance = JNI.INSTANCE;
    var atMethod = jniInstance.getAt;
    jniInstance.getAt.implementation = function () {
        var result = atMethod.call(jniInstance);
        console.log('JNI getAt method returned: ' + result);
        return result;
    };
});

aad mysecretadd

key atrikeyssyekirta

得到后直接写exp即可

import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import java.util.Base64;

public class exp {
    public static final String SECRET_KEY = "atrikeyssyekirta";
    public static final String ENC = "MTE0NTE0HMuJKLOW1BqCAi2MxpHYjGjpPq82XXQ/jgx5WYrZ2MV53a9xjQVbRaVdRiXFrSn6EcQPzA==";
    public static final String AAD = "mysecretadd";

    public static void main(String[] args) throws Exception {
        byte[] encryptedData = Base64.getDecoder().decode(ENC);
        byte[] iv = Arrays.copyOfRange(encryptedData, 0, 6);
        byte[] cipherText = Arrays.copyOfRange(encryptedData, 6, encryptedData.length);

        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv);
        cipher.init(Cipher.DECRYPT_MODE, new javax.crypto.spec.SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), "AES"), parameterSpec);

        byte[] aad = AAD.getBytes(StandardCharsets.UTF_8);
        cipher.updateAAD(aad);

        byte[] decryptedBytes = cipher.doFinal(cipherText);

        String flag = new String(decryptedBytes, StandardCharsets.UTF_8);

        System.out.println(flag);

    }
}

幸运转盘

静态看了半天,懒得打字了,上图吧

幸运转盘 so里面是字符+3 打错了

exp:

import base64

def rc4_xor(key: bytes, data: bytes) -> bytes:

    S = list(range(256))
    j = 0
    key_length = len(key)
    for i in range(256):
        j = (j + S[i] + key[i % key_length]) % 256
        S[i], S[j] = S[j], S[i]
    i = 0
    j = 0
    out = bytearray()
    for byte in data:
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]
        k = S[(S[i] + S[j]) % 256]
        out.append(byte ^ k ^ 40)
    return bytes(out)


fixed_key = b"Take_it_easy"

num_list = [101, 74, 76, 49, 101, 76, 117, 87, 55, 69, 118, 68, 118, 69, 55, 67, 61, 83, 62, 111, 81, 77, 115, 101, 53, 73, 83, 66, 68, 114, 109, 108, 75, 66, 97, 117, 93, 127, 115, 124, 109, 82, 93, 115]

result = ""
i=-1
for num in num_list:
    i= i + 1
    result += chr((num_list[i] ^7 ) -1 ) 
    
decoded_data = base64.b64decode(result)

def shift_string(data, shift_value):
    shifted_data = bytes([b - shift_value for b in data])
    return shifted_data

correct_key = rc4_xor(fixed_key, decoded_data)
shifted_string = shift_string(correct_key, 3)
print(shifted_string)

Fuko's starfish

AES/ECB/NoPadding

找 srand 的交叉引用去掉花指令即可看到key的逻辑

然后直接copy生成key即可(为什么不是C的? 因为当时直接丢给ai的时候想换行结果没按到shift直接发送了,不过也能用)

def c_rand():
    global seed
    # 模拟 MSVC 的 rand() 算法:
    # seed = (seed * 214013 + 2531011) mod 2^32
    # 返回值为 (seed >> 16) & 0x7FFF
    seed = (seed * 214013 + 2531011) & 0xFFFFFFFF
    return (seed >> 16) & 0x7FFF

# 初始化种子,相当于 srand(114514u)
seed = 114514

bytes_list = []
for _ in range(16):
    v = c_rand()
    # 计算表达式:v + v // 255,并模拟 unsigned char 的截断(取低8位)
    byte_val = (v + v // 255) & 0xFF
    bytes_list.append(byte_val)

key= ''.join("{:02x}".format(b ^ 0x17) for b in bytes_list)
print(key)

然后用CyberChef一把🔒即可

AndroidLux

当时搞完魔改base64那里就没时间了😭,还是太菜了

下面AndroidLux的题解来自yur0

Android 里运行 linux? 很有趣

Android 那边没什么重要的东西,就初始化了 busybox 的环境,然后开了个 socket 用来通信,看 Service 可以发现他启动了 /root/env

    package work.pangbai.androidlux.Service$1;
    import java.lang.Runnable;
    import work.pangbai.androidlux.Service;
    import java.lang.Object;
    import java.lang.String;
    import work.pangbai.androidlux.cmdExer;
    
    class Service$1 implements Runnable	// class@000061 from classes4.dex
    {
        final Service this$0;
    
        void Service$1(Service this$0){
           this.this$0 = this$0;
           super();
        }
        public void run(){
           cmdExer.execute("gnu_linux_loader -r /data/data/work.pangbai.androidlux/files -0 -w /root -b /dev -b /proc -b /sys /bin/bash -c ./env", false, true);
        }
    }

那么文件在哪里呢,题目给了一个 env 文件,这个其实是 tar.gz 格式的,直接可以解压,然后火速去分析 root 目录下的 env

有几个花指令,但是 IDA 很智能,直接帮我们识别出来了,直接 nop 掉就行了

现在逻辑就很清楚了,传进来然后一个魔改 base64 加密了一下 魔改 base64

    void __fastcall encodeBase64(BYTE *data, int size, BYTE *out)
    {
      int v3; // w0
      int v4; // w1
      int v5; // w1
      int v6; // w0
      int v7; // w1
      int v8; // w0
      _BYTE *d; // [xsp+58h] [xbp+58h]
      int i; // [xsp+68h] [xbp+68h]
      int ii; // [xsp+68h] [xbp+68h]
      int j; // [xsp+6Ch] [xbp+6Ch]
    
      sqrt((double)25);
      j = 0;
      if ( size % 3 )
        v3 = 4;
      else
        v3 = 0;
      d = malloc(4 * (size / 3) + 1 + v3);
      for ( i = 0; i < size; ++i )
      {
        if ( size - i <= 2 )
        {
          d[j] = base64[data[i] >> 2];
          if ( size - i == 2 )
          {
            v7 = data[i++] & 3;
            d[j + 1] = base64[v7 | ((int)data[i] >> 2) & 0x3C];
            d[j + 2] = base64[data[i] & 0xF];
          }
          else
          {
            d[j + 1] = base64[data[i] & 3];
            d[j + 2] = '=';
          }
          v8 = j + 3;
          j += 4;
          d[v8] = '=';
        }
        else
        {
          d[j] = base64[data[i] >> 2];
          v4 = data[i] & 3;
          ii = i + 1;
          d[j + 1] = base64[v4 | ((int)data[ii] >> 2) & 60];
          v5 = data[ii] & 15;
          i = ii + 1;
          d[j + 2] = base64[v5 | (16 * (data[i] >> 6))];
          v6 = j + 3;
          j += 4;
          d[v6] = base64[data[i] & 0x3F];
        }
      }
      d[j] = 0;
      *(_QWORD *)out = d;
    }

密文: RPVIRN40R9PU67ue6RUH88Rgs65Bp8td8VQm4SPAT8Kj97QgVG== 自定义表: TUVWXYZabcdefghijABCDEF456789GHIJKLMNOPQRSklmnopqrstuvwxyz0123+/

base64 魔改点就在将第二部分的前 2 个 bit 和后 4 个 bit 互换,第三部分的前 4 个 bit 和后 2 个 bit 互换,其他不变

但是尝试解码的时候出现了问题,R 查表得到 40,转二进制就是 101000,第一个字符的最高位是 1?这不符合可打印字符的范围,说明这题还有其他地方藏了逻辑

在把这个程序翻了个底朝天也没找到东西后,只能看看是不是这个环境哪里对程序做了修改,掏出我们的 everything, 按时间排序一下文件,看看出题人在哪里干了坏事

接着就翻到了 ld.so.preload 文件,加载了 /usr/libexec/libexec.so

果不其然,在这个 so 里面 hook 了 readstrcmp 函数  hook read 和 strcmp函数

那么逻辑已经十分清晰了,exp:

def custom_rot13_decrypt(data: str) -> str:
    dec = []
    for c in data:
        if 'A' <= c <= 'M' or 'a' <= c <= 'm':
            dec.append(chr(ord(c) + 13))
        elif 'N' <= c <= 'Z' or 'n' <= c <= 'z':
            dec.append(chr(ord(c) - 13))
        else:
            dec.append(c)
    return ''.join(dec)

cipher_text = "RPVIRN40R9PU67ue6RUH88Rgs65Bp8td8VQm4SPAT8Kj97Qg"
table = list("TUVWXYZabcdefghijABCDEF456789GHIJKLMNOPQRSklmnopqrstuvwxyz0123+/")

eee = custom_rot13_decrypt(cipher_text)


enc = eee.encode()

print(enc)

idx = []

for i in enc:
    idx.append(table.index(chr(i)))

s = []

for i in idx:
    s.append(bin(i)[2:].zfill(6))

for i in range(0, len(s), 4):
    s[i + 1] = s[i + 1][4:] + s[i + 1][:4]
    s[i + 2] = s[i + 2][2:] + s[i + 2][:2]

res = ""
for i in s:
    res += i

b = [res[i:i+8] for i in range(0, len(res), 8)]

for i in b:
    print(chr(int(i, 2) ^ 1), end="")
    
print("}")
# VNCTF{Ur_go0d_@ndr0id&l1nux_Reve7ser}
文章目录