D^3CTF2025

  1. D^3CTF2025
    1. d3rpg-revenge
    2. d3piano
      1. 一、前言
      2. 二、开始
      3. 三、结语
  • 最后
  • D^3CTF2025

    d3rpg-revenge

    是一个游戏,并且随着在游戏中的对话,会改变程序的内存(不可恢复,除非删了重来)。然后玩了下,发现走到二楼和NPC直接对话,会问你是reverse手,misc手还是musc手,选reverse手就会直接让你输入flag,然后回车,显示checking…,再回车,就会有回显,由于附件中有exe和不少dll,并且还有一个自定义的文件,.d3ssad文件。分析结果是:脱壳后的secret_dll.dll中的check_flag函数中的字符串就是密文
    a50912a3fc754cffe94fc13fba5a6d9b

    ad9c8b1f242312c34bc5bb485d49039c

    然后加密逻辑在d3rpg.dll文件中进行载入,载入的内容应该是d3rpg.d3ssad文件进行解密的结果(应该是这样)。那么我们知道当程序运行的时候,check flag的时候,肯定会将自定义的文件进行解密,加载到内存中,然后调用其中逻辑,进行check。既然如此,我们就可以用ce,重开游戏,直接走到二楼,和NPC对话,直接输入flag,然后回车,让程序就断在checking…这里,然后用ce去内存中找,找到加密逻辑
    image-20250531162103708

    module Scene_RPG
      class Secret_Class
        DELTA = 0x1919810 | (($de1ta + 1) * 0xf0000000)
        def initialize(new_key)
          @key = str_to_longs(new_key)
          if @key.length < 4
            @key.length.upto(4) { |i| @key[i] = 0 }
          end
        end
        def self.str_to_longs(s, include_count = false)
          s = s.dup
          length = s.length
          ((4 - s.length % 4) & 3).times { s << "\0" }
          unpacked = s.unpack('V*').collect { |n| int32 n }
          unpacked << length if include_count
          unpacked
        end
        def str_to_longs(s, include_count = false)
          self.class.str_to_longs s, include_count
        end
        def self.longs_to_str(l, count_included = false)
          s = l.pack('V*')
          s = s[0...(l[-1])] if count_included
          s
        end
        def longs_to_str(l, count_included = false)
          self.class.longs_to_str l, count_included
        end
        def self.int32(n)
          n -= 4_294_967_296 while (n >= 2_147_483_648)
          n += 4_294_967_296 while (n <= -2_147_483_648)
          n.to_i
        end
        def int32(n)
          self.class.int32 n
        end
    
        def mx(z, y, sum, p, e)
          int32(
            ((z >> 5 & 0x07FFFFFF) ^ (y << 2)) +
            ((y >> 3 & 0x1FFFFFFF) ^ (z << 4))
          ) ^ int32((sum ^ y) + (@key[(p & 3) ^ e] ^ z))
        end
        def self.encrypt(key, plaintext)
          self.new(key).encrypt(plaintext)
        end
        def encrypt(plaintext)
          return '' if plaintext.length == 0
          v = str_to_longs(plaintext, true)
          v[1] = 0 if v.length == 1
          n = v.length - 1
          z = v[n]
          y = v[0]
          q = (6 + 52 / (n + 1)).floor
          sum = $de1ta * DELTA
          p = 0
          while(0 <= (q -= 1)) do
            sum = int32(sum + DELTA)
            e = sum >> 2 & 3
            n.times do |i|
              y = v[i + 1];
              z = v[i] = int32(v[i] + mx(z, y, sum, i, e))
              p = i
            end
            p += 1
            y = v[0];
            z = v[p] = int32(v[p] + mx(z, y, sum, p, e))
          end
          longs_to_str(v).unpack('a*').pack('m').delete("\n")
        end
        def self.decrypt(key, ciphertext)
          self.new(key).decrypt(ciphertext)
        end
      end
    end
    
    def validate_flag(input_flag)
      c_flag = input_flag + "\0"
      result = $check_flag.call(c_flag)
      result == 1
    end
    
    
    def check
      flag = $game_party.actors[0].name
      key = Scene_RPG::Secret_Class.new('rpgmakerxp_D3CTF')
      cyphertext = key.encrypt(flag)
      if validate_flag(cyphertext)
        $game_variables[1] = 100
      else
        $game_variables[1] = 0
      end
    end
    
    def check1
      flag = $game_party.actors[0].name
      if flag == "ImPsw"
        $game_variables[2] = 100
      else
        $game_variables[2] = 0
      end
    end
    

    密钥也有了,delta的值,是

    0x1919810 | ((0 + 1) * 0xf0000000))
    

    $delta的值是0,那么就可以写脚本解密了

    from ctypes import *
    import libnum
    
    
    def MX(z, y, sum1, k, p, e):
        return c_uint32(((z.value >> 5 ^ y.value << 2) + (y.value >> 3 ^ z.value << 4)) ^ (
                (sum1.value ^ y.value) + (k[(p & 3) ^ e.value] ^ z.value)))
    
    
    def btea(v, k, n, delta):
        if n > 1:
            sum1 = c_uint32(0)
            z = c_uint32(v[n - 1])
            rounds = 6 + 52 // n
            e = c_uint32(0)
    
            while rounds > 0:
                sum1.value += delta
                e.value = ((sum1.value >> 2) & 3)
                for p in range(n - 1):
                    y = c_uint32(v[p + 1])
                    v[p] = c_uint32(v[p] + MX(z, y, sum1, k, p, e).value).value
                    z.value = v[p]
    
                y = c_uint32(v[0])
                v[n - 1] = c_uint32(v[n - 1] + MX(z, y, sum1, k, n - 1, e).value).value
                z.value = v[n - 1]
                rounds -= 1
        else:
            sum1 = c_uint32(0)
            n = -n
            rounds = 6 + 52 // n
            sum1.value = rounds * delta
            y = c_uint32(v[0])
            e = c_uint32(0)
    
            while rounds > 0:
                e.value = ((sum1.value >> 2) & 3)
                for p in range(n - 1, 0, -1):
                    z = c_uint32(v[p - 1])
                    v[p] = c_uint32(v[p] - MX(z, y, sum1, k, p, e).value).value
                    y.value = v[p]
    
                z = c_uint32(v[n - 1])
                v[0] = c_uint32(v[0] - MX(z, y, sum1, k, 0, e).value).value
                y.value = v[0]
                sum1.value -= delta
                rounds -= 1
    
        return v
    
    
    if __name__ == '__main__':
        a = [0x2e, 0x15, 0x6f, 0x7d, 0xea, 0x72, 0xc0, 0x52, 0x2c, 0x1d,
             0xbf, 0x06, 0xf2, 0x43, 0x5d, 0xbb, 0x8f, 0x49, 0xde, 0x4d]
        a = [int.from_bytes(a[i:i + 4], "little") for i in range(0, len(a), 4)]
        for i in range(4):
            print(hex(a[i]), end=", ")
        print()
        k = [0x72, 0x70, 0x67, 0x6d, 0x61, 0x6b, 0x65, 0x72, 0x78, 0x70,
             0x5f, 0x44, 0x33, 0x43, 0x54, 0x46]
        k = [int.from_bytes(k[i:i + 4], "little") for i in range(0, len(k), 4)]
    
        for i in range(4):
            print(hex(k[i]), end=", ")
        print()
    
        n = len(a)
        delta = 0xf1919810
        res = btea(a, k, -n, delta)
        print(','.join(hex(x) for x in res))
        flag = ''
        for i in res:
            flag += (libnum.n2s(i)[::-1].decode())
        print(flag)
    

    d3ctf{Y0u_R_RPG_M4st3r}


    d3piano

    一、前言

    有点可惜,比赛做的时候不够细心,导致很多错过关键性的一些细节处。赛后复现…

    二、开始

    是一个模拟钢琴的APK,但只包含了一个八度,7个白键5个黑键。Java层只有APK启动页面的琴键绘制的逻辑,check逻辑都在so层
    image-20250603193408723

    然后每个键所对应的琴音,根据so层的keysoundnames数组的内容来加载琴音资源
    image-20250603193635847

    image-20250603193723987

    但是点击的逻辑,不知道什么原因,我的jadx反编译不正常,但是使用jeb就好了
    image-20250603193833394

    完整代码

    @Override  // android.view.View
        public boolean onTouchEvent(MotionEvent event) {
            Intrinsics.checkNotNullParameter(event, "event");
            int v = event.getActionMasked();
            float f = event.getX();
            float f1 = event.getY();
            switch(v) {
                case 0: 
                case 5: {
                    int v2 = this.keyPaths.size();
                    for(int i = 0; i < v2; ++i) {
                        Region region = new Region();
                        RectF rectF = new RectF();
                        ((Path)this.keyPaths.get(i)).computeBounds(rectF, true);
                        region.setPath(((Path)this.keyPaths.get(i)), new Region(((int)rectF.left), ((int)rectF.top), ((int)rectF.right), ((int)rectF.bottom)));
                        if(region.contains(((int)f), ((int)f1))) {
                            this.pressedKeys.add(Integer.valueOf(i));
                            SoundPool soundPool0 = this.soundPool;
                            if(soundPool0 != null) {
                                soundPool0.play(((Number)this.keySounds.get(i)).intValue(), 1.0f, 1.0f, 1, 0, 1.0f);
                            }
    
                            if(this.check(i)) {
                                String s = this.getflag();
                                Toast.makeText(this.Mycontext, "Congratulations, this is music exclusive to Flag", 0).show();
                                Log.i("D3CTF_FLAG", s);
                            }
    
                            this.invalidate();
                            return true;
                        }
                    }
    
                    return true;
                }
                case 1: 
                case 6: {
                    int v4 = this.keyPaths.size();
                    break;
                }
                default: {
                    return true;
                }
            }
    
            for(int i = 0; i < v4; ++i) {
                Region region = new Region();
                RectF rectF = new RectF();
                ((Path)this.keyPaths.get(i)).computeBounds(rectF, true);
                region.setPath(((Path)this.keyPaths.get(i)), new Region(((int)rectF.left), ((int)rectF.top), ((int)rectF.right), ((int)rectF.bottom)));
                if(region.contains(((int)f), ((int)f1))) {
                    this.pressedKeys.remove(Integer.valueOf(i));
                    this.invalidate();
                    return true;
                }
            }
    
            return true;
        }
    

    获取当前点击位置,判断属于哪个琴键,切换琴键,然后发出声音。如果是0号白键就使用keySound[0],而keySound[0]的音调由keysoundnames数组决定,比如keysoundname[0] == ‘C’,则0号白键就发出”C”的音调。,每次按下都会将琴键的序号传给so层的check函数进行判断,如果返回true就执行getflag的逻辑,然后log出flag。

    接下来分析so层,从Java层分析得知apk只加载了一个libD3Piano.so。

    先看JNI_OnLoad函数,由于ida对一些结构体无法识别,手动修复一下
    image-20250603194633065

    很显然,check方法是动态注册的
    image-20250603194714070

    跟进分析

    __int64 sub_29BD4()
    {
      __int64 v0; // x0
      size_t n; // x0
      int i; // [xsp+40h] [xbp-480h]
      unsigned __int8 v4; // [xsp+5Ch] [xbp-464h]
      _BYTE result1[16]; // [xsp+80h] [xbp-440h] BYREF
      char result2[16]; // [xsp+90h] [xbp-430h] BYREF
      char resul3[1048]; // [xsp+A0h] [xbp-420h] BYREF
      __int64 v8; // [xsp+4B8h] [xbp-8h]
    
      v8 = *(_ReadStatusReg(TPIDR_EL0) + 40);
      memset(resul3, 0, sizeof(resul3));
      sub_29DB0(&input_str);
      if ( getlength(&input_str) != 42 )
        goto LABEL_7;
      __gmpz_inits();
      WHAT(&input_str);
      __gmpz_set_str(result1, v0, 12);              // 十二进制转十进制
      sub_29B44(result2, result1);                  // RSA
      __gmpz_get_str(resul3, 16, result2);
      for ( i = 0; i < __strlen_chk(resul3, 0x418u); ++i )
        resul3[i] = encrypt(resul3[i]);             // 还进行一个针对除大写字母以外的其他字符处理
      n = __strlen_chk(resul3, 0x418u);
      if ( !memcmp(resul3, aC901acacbb426c, n) )    // "c901acacbb426c9c447acda82513965ccc3faf6c9dc58d24ed34b62c7fb1548f9ad06b9355c7d20704cfdfdfc89a3f893801e31719564683fdc7de26d807ed27f898edb3efd51b6e8e2a192d6a0929554342adfed541cd8399da0fbacfeaa5b608b887fd74f4f0e31f9bb5816c54163b8e46d27553798233bef6eaf848c64e"
      {
        __android_log_print(4, "D3PianoLog", "YOooou are Right??!");
        v4 = 1;
      }
      else
      {
    LABEL_7:
        v4 = 0;
      }
      _ReadStatusReg(TPIDR_EL0);
      return v4;
    }
    

    根据具体逻辑的分析,对函数进行了一些重命名和注释

    跟进RSA加密函数

    __int64 __fastcall RSA_part(__int64 a1, __int64 a2, __int64 a3)
    {
      __int64 result; // x0
      int v4; // [xsp+3Ch] [xbp-84h]
      int n11; // [xsp+44h] [xbp-7Ch]
      unsigned int v6; // [xsp+48h] [xbp-78h]
      _BYTE v10[32]; // [xsp+68h] [xbp-58h] BYREF
      _BYTE v11[16]; // [xsp+88h] [xbp-38h] BYREF
      _BYTE v12[16]; // [xsp+98h] [xbp-28h] BYREF
      _BYTE v13[16]; // [xsp+A8h] [xbp-18h] BYREF
      __int64 v14; // [xsp+B8h] [xbp-8h]
    
      v14 = *(_ReadStatusReg(TPIDR_EL0) + 40);
      v6 = 0;
      srand(0x221221u);                             // 伪随机
      for ( n11 = 0; n11 <= 11; ++n11 )
      {
        v4 = *(key + 4LL * n11);
        v6 += v4 * rand();
      }
      __gmpz_inits();
      __gmp_randinit_mt();
      __gmp_randseed_ui(v10, v6);
      sub_29940(v13, v10, 512);
      sub_29940(v12, v10, 512);
      __gmpz_mul(a1, v13, v12);
      __gmpz_sub_ui(v13);
      __gmpz_sub_ui(v12);
      __gmpz_mul(v11, v13, v12);
      __gmpz_set_ui(a2, 65537);
      __gmpz_invert(a3, a2, v11);
      __gmpz_clears(v13, v12, v11, 0);
      result = __gmp_randclear(v10);
      _ReadStatusReg(TPIDR_EL0);
      return result;
    }
    

    for循环中有一个数组,交叉引用后发现是在Java_com_d3ctf2025_D3piano_PianoLayout_getkeysoundnames中进行初始化的,也就是琴键的琴音的索引组成的数组,然后根据index生成soundnames数组
    对部分数组进行重命名

    __int64 __fastcall Java_com_d3ctf2025_D3piano_PianoLayout_getkeysoundnames(JNIEnv *a1, __int64 a2, signed int a3)
    {
      __int64 v4; // [xsp+10h] [xbp-50h]
      signed int i; // [xsp+1Ch] [xbp-44h]
      __int64 v6; // [xsp+20h] [xbp-40h]
      jclass Class_w; // [xsp+28h] [xbp-38h]
      __int64 v10; // [xsp+48h] [xbp-18h]
      _BYTE v11[4]; // [xsp+54h] [xbp-Ch] BYREF
      __int64 v12; // [xsp+58h] [xbp-8h]
    
      v12 = *(_ReadStatusReg(TPIDR_EL0) + 40);
      keysoundindex = getkeysoundindex(a3);
      Class_w = FindClass_w(a1, "java/lang/String");
      if ( Class_w )
      {
        v6 = sub_293D4(a1, a3, Class_w, 0);
        if ( v6 )
        {
          for ( i = 0; i < a3; ++i )
          {
            v11[0] = aCdefgabdegab[*(keysoundindex + 4LL * i)];
            v11[1] = 0;
            v4 = sub_29418(a1, v11);
            if ( !v4 )
            {
              v10 = 0;
              goto LABEL_11;
            }
            sub_2944C(a1, v6, i, v4);
            sub_29490(a1, v4);
          }
          v10 = v6;
        }
        else
        {
          v10 = 0;
        }
      }
      else
      {
        v10 = 0;
      }
    LABEL_11:
      _ReadStatusReg(TPIDR_EL0);
      return v10;
    }
    

    再跟进分析getkeysoundindex函数,

    std::chrono::system_clock *__fastcall getkeysoundindex(int a1)
    {
      unsigned __int64 v1; // x0
      std::chrono::system_clock *v2; // x0
      int j; // [xsp+3Ch] [xbp-74h]
      __int64 v5; // [xsp+40h] [xbp-70h]
      __int64 v6; // [xsp+48h] [xbp-68h]
      unsigned int v7; // [xsp+54h] [xbp-5Ch]
      int i; // [xsp+58h] [xbp-58h]
      std::chrono::system_clock *v9; // [xsp+68h] [xbp-48h]
      char v11[8]; // [xsp+78h] [xbp-38h] BYREF
      __int64 v12; // [xsp+80h] [xbp-30h] BYREF
      __int64 v13; // [xsp+88h] [xbp-28h] BYREF
      _QWORD v14[4]; // [xsp+90h] [xbp-20h] BYREF
    
      v14[3] = *(_ReadStatusReg(TPIDR_EL0) + 40);
      sub_294C4(v14, a1);
      if ( a1 >> 62 )
        v1 = -1;
      else
        v1 = 4LL * a1;
      v2 = operator new[](v1);
      v9 = v2;
      for ( i = 0; i < a1; ++i )
      {
        v2 = sub_295E4(v14, i);
        *v2 = i;
      }
      v12 = std::chrono::system_clock::now(v2);
      v13 = sub_29608(&v12);
      v7 = sub_29660(&v13);
      sub_29678(v11, v7);
      v6 = sub_29718(v14);
      v5 = sub_29748(v14);
      sub_296A4(v6, v5, v11);
      for ( j = 0; j < a1; ++j )
        *(v9 + j) = *sub_295E4(v14, j);
      sub_29778(v14);
      _ReadStatusReg(TPIDR_EL0);
      return v9;
    }
    

    静态分析的时候,结合ai的分析结果,误以为是打乱随机的,没有把注意具体点击的时候发出的声音,其实根本没有打乱,琴键的音调是完全正确的。这里的生成乱序keysoundindex没有生效,keysoundindex就是0-11的顺序…(比赛的时候通过Frida hook的时候确实发现每个琴键所对应的index是固定的,但是没注意每个index和每个音调也是一一对应的)

    动态注册的check函数里使用的GMP大数库,看官方wp说很多函数的符号ida分析错误,但貌似影响也不大。check函数的逻辑就是把琴键的index拼接成一个12进制的字符串然后作为大数值,完了进行加密,再转换成十六进制字符串,进行比较。

    比赛的时候一直以为keysoundindex是随机的,所以RSA一直解不出来…

    官方wp中的解密脚本

    from Crypto.Util.number import long_to_bytes
    p = 0xcd88775691357147eea5dc584718edab9ca314cdd52a8c1cf847dbbb8371798f15e9bdca2bfaa4595d47eecae21bea38691a26e1c707867b5ea2f6f2f03bf4c+1
    q = 0x565c0138487b57e4b76d0924163f67facb17a77f83e354cc3c8432879dab4611c2442cdd73f71c9e6cb4e56a7c45a403148e6d558f986ec6505882ae095c34d2+1
    d = 0x2e4f4254c529f4ef40d28a6595e60bb28b1d11ab13328000fb63cf77836a423259af8a8881d73e1868cd66bbd78920f38033f9f1e0e406b82f0aee50fdbe2567e0ac329e80e9abbb7075f4711157844e9fcb666200b1c4ef05b3cc66278221c9e6bd7250caf2be2a3793cf1f2dd43918d6ead0ee851eccf43e48750b6fab2f9
    e = 65537
    c = 0xc901acacbb426c9c447acda82513965ccc3faf6c9dc58d24ed34b62c7fb1548f9ad06b9355c7d20704cfdfdfc89a3f893801e31719564683fdc7de26d807ed27f898edb3efd51b6e8e2a192d6a0929554342adfed541cd8399da0fbacfeaa5b608b887fd74f4f0e31f9bb5816c54163b8e46d27553798233bef6eaf848c64e
    n = p * q
    mm = pow(c,e,n)
    print("解密后的flag(字符串):", long_to_bytes(mm).decode())
    

    确实,如同check函数中的log提示,这是fake flag。

    回顾前面的分析过程,getkeysoundindex并没有生成打乱的0-11,并且在尝试调试或者frida hook的时候发现有检测,想hook只能通过附加进程调试,但是在这个so文件里并没有看到很明显的检测逻辑,同时lib目录里还有一个so文件,并且是可以在libD3Piano.so中验证另一个so文件的可疑点
    image-20250603201006676

    libD3Piano.so链接了另外的两个库,但是很明显使用了libgmp.so,没有使用另一个所以 这个libMediaPlayer.so肯定在 init_array里面藏了东西。因为在链接时会调用 init_array 的内容。
    image-20250603201326760

    这其中可疑的函数只有sub_8938CC()
    image-20250603201354366

    初始化了三个信号量,创建了四个线程,每个线程都执行不同的函数

    • 从 unk_BBBF04 到 unk_BBBF34,每间隔 16 字节初始化一个信号量(sem_init),初值为 0。
    • 4 个线程,每个线程执行的函数来自 off_BBAD20 表中的函数指针(共 4 个)。

    image-20250603202622501

    依次进行分析
    fun1是Frida检测的逻辑

    __int64 fun1()
    {
      __int64 v0; // x0
      __int64 *v2; // [xsp+48h] [xbp-388h]
      __int64 *i; // [xsp+D0h] [xbp-300h]
      __int64 v4; // [xsp+E0h] [xbp-2F0h]
      _QWORD v5[3]; // [xsp+118h] [xbp-2B8h] BYREF
      _QWORD v6[3]; // [xsp+130h] [xbp-2A0h] BYREF
      _BYTE v7[16]; // [xsp+148h] [xbp-288h] BYREF
      _QWORD v8[2]; // [xsp+158h] [xbp-278h] BYREF
      _BYTE v9[16]; // [xsp+168h] [xbp-268h] BYREF
      _QWORD v10[2]; // [xsp+178h] [xbp-258h] BYREF
      _BYTE v11[16]; // [xsp+188h] [xbp-248h] BYREF
      _BYTE v12[24]; // [xsp+198h] [xbp-238h] BYREF
      _BYTE v13[24]; // [xsp+1B0h] [xbp-220h] BYREF
      _BYTE v14[24]; // [xsp+1C8h] [xbp-208h] BYREF
      _BYTE v15[344]; // [xsp+1E0h] [xbp-1F0h] BYREF
      _BYTE v16[144]; // [xsp+338h] [xbp-98h] BYREF
      __int64 v17; // [xsp+3C8h] [xbp-8h] BYREF
    
      v17 = *(_QWORD *)(_ReadStatusReg(TPIDR_EL0) + 40);
      sub_892D64(v14, "/proc/self/task", 0);
      sub_892DC8(v13);
      sub_892DEC(v12);
      sub_892E68();
      sub_892E68();
      sub_892E68();
      sub_892E68();
      sub_892E68();
      sub_892E68();
      sub_892EEC(v11, v14);
      sub_892F50();
      sub_892F20(v10, v9);
      sub_892F7C(v9);
      sub_892F50();
      sub_892FA0(v8, v7);
      sub_892F7C(v7);
      while ( (sub_892FC8(v10, v8) & 1) != 0 )
      {
        v4 = sub_892FFC(v10);
        v0 = sub_893060(v4);
        sub_893074(v5, v0);
        sub_893020(v6, v5, "/comm");
        sub_8930AC();
        sub_9E4E94(v6);
        sub_9E4E94(v5);
        sub_8930E0(v15, v13, 24);
        if ( (sub_893124(v15) & 1) != 0 )
        {
          sub_893150(v15, v12);
          for ( i = (__int64 *)v16; i != &v17; i += 3 )
          {
            if ( sub_8931A8(v12, i, 0) != -1 )
            {
              _ReadStatusReg(TPIDR_EL0);
              exit(0);
            }
          }
        }
        sub_893220(v15);
        sub_89325C(v10);
      }
      sub_892F7C(v8);
      sub_892F7C(v10);
      sub_892F7C(v11);
      sleep(5u);
      v2 = &v17;
      do
      {
        sub_9E4E94(v2 - 3);
        v2 -= 3;
      }
      while ( v2 != (__int64 *)v16 );
      sub_9E4E94(v12);
      sub_893284(v13);
      sub_893284(v14);
      _ReadStatusReg(TPIDR_EL0);
      return 0;
    }
    

    通过检测线程名称来检测是否处于调试/hook状态。检测到就直接exit退出,绕过检测直接nop掉exit函数。

    剩下三个fun涉及到了信号量。执行顺序为fun3 -> fun2 ->fun4

    image-20250603203312384

    image-20250603203324671

    image-20250603203332915

    从fun3开始分析。

    image-20250603203937195

    并且官方wp中还进行了如何进行hook的补充。
    image-20250603204448926

    image-20250603204709478

    image-20250603204950110

    也可以进入**sub_893630()**函数里找一下,就是进去的第一个结构体。
    func3→MyListener1、func2→MyListener2、func4→MyListener3。

    func3 函数hook函数sub291B4。修改了返回值,然后func2运行。sub_896A28就是on_enter,sub_896A3C就是on_leave

    image-20250603205457465

    fun2 hook了基地址+0x29BD4地址的函数,就是check函数
    image-20250603205621847

    image-20250603205838316

    __int64 __fastcall fun2_on_enter(__int64 a1, __int64 a2)
    {
      _BYTE *v2; // x0
      unsigned __int8 *v3; // x0
      __int64 v4; // x0
      size_t n; // x0
      __int64 result; // x0
      char v7; // [xsp+14h] [xbp-ACh]
      int n90_1; // [xsp+28h] [xbp-98h]
      int n90; // [xsp+2Ch] [xbp-94h]
      __int64 v12; // [xsp+30h] [xbp-90h]
      int n11; // [xsp+4Ch] [xbp-74h] BYREF
      _OWORD s2_[6]; // [xsp+50h] [xbp-70h] BYREF
      int v15; // [xsp+B0h] [xbp-10h]
      __int64 v16; // [xsp+B8h] [xbp-8h]
    
      v16 = *(_ReadStatusReg(TPIDR_EL0) + 40);
      v12 = (*(*a2 + 24LL))(a2, 2);
      v15 = 0;
      memset(s2_, 0, sizeof(s2_));
      if ( sub_897580(a1 + 8) < 0x5B )              // 检测长度
      {
        result = sub_897630(a1 + 8, &aCdefgabdegab[v12]);// "CDEFGABdegab"
      }
      else
      {
        for ( n90 = 0; n90 <= 90; ++n90 )           // 长度大于90,先异或
        {
          v7 = byte_BBACC2[n90];
          v2 = sub_8975A0(a1 + 8, n90);
          *v2 ^= v7;
        }
        for ( n90_1 = 0; n90_1 <= 90; ++n90_1 )     // 然后根据异或后的字符串,获取到对应的音调的下标保存到_obj
        {
          for ( n11 = 0; n11 <= 11; ++n11 )
          {
            v3 = sub_8975A0(a1 + 8, n90_1);
            if ( *v3 == aCdefgabdegab[n11] )        // "CDEFGABdegab"
              sub_8975C4(&obj_, &n11);
          }
        }
        (*(*a1 + 32LL))(a1);                        // 调用虚表函数1
        v4 = sub_897580(a1 + 8);
        (*(*a1 + 40LL))(a1, s2_, v4, a1 + 32, aCdefgabdegab, 2232865);// 调用虚表函数2
        n = sub_897580(a1 + 8);
        result = memcmp(&s1_, s2_, n);              // cmp
        if ( !result )
          byte_BBBF00 = 1;
      }
      _ReadStatusReg(TPIDR_EL0);
      return result;
    }
    

    on_leave修改返回值
    image-20250603210355580

    在func2 attach上函数后,func4执行。func4 hook 偏移0x28E80,也就是get_flag 函数的tostr。
    image-20250603210458277

    其实就是把func2_on_enter函数里面根据异或加密后的音调在转换成12进制数,丢 给tostr。

    image-20250603210700057

    image-20250603210717852

    那么现在就需要分析两个虚表函数加密,然后写出解密获取到输入的音调数组转换成 12 进制再转换成字符串输出就可以。那么来分析 enc1 与 enc2,enc1 函数有点抽象, 先分析enc2函数。很明显的salsa20加密。
    image-20250603210808508

    可以尝试先解密enc2函数,key是类成员,找到类的构造函数。
    image-20250603211101417

    这就是key,被加密了,解密一下。
    image-20250603211427066

    # 模拟 .rodata 段中的两个 xmmword 数据(共32字节)
    xmmword_252578 = [
        0x88, 0x9A, 0x93, 0xBC, 0x90, 0xB2, 0xBA, 0xA0, 0xC8,
        0xCF, 0xA0, 0xBB, 0xCC, 0x9C, 0xC8, 0xB9
    ]
    xmmword_252588 = [
        0xA0, 0xCD, 0xCF, 0xCD, 0xCA, 0xD2, 0x8D, 0xCC, 0xA9,
        0xBA, 0xAD, 0xAC, 0xBA, 0xDE, 0xDE, 0xDE
    ]
    
    # 拼接两个 xmmword 成为 32 字节原始数据
    combined = xmmword_252578 + xmmword_252588
    
    # 对这 32 字节做按位取反(模仿 ~data 的行为)
    inverted = [~b & 0xFF for b in combined]
    
    # 模拟 a1:前32字节占位,后32字节是结果
    a1 = [0] * 32 + inverted
    
    # 打印可见字符
    for byte in a1:
        if 0x20 <= byte <= 0x7E:
            print(chr(byte), end='')
    
    print()
    

    key: welCoME_70_D3c7F_2025-r3VERSE!!!

    解密代码

    #include <cstdio>
    #include <cstring>
    #include <format>
    #include <iostream>
    #include <fstream>
    #include <cstdint>
    #include <vector>
    
    #define ROTL(a, b) ((((uint32_t)(a)) << ((uint32_t)(b))) | (((uint32_t)(a)) >> (32 - ((uint32_t)(b)))))
    #define QUARTERROUND(a, b, c, d) \
        a += b;                      \
        d ^= a;                      \
        d = ROTL(d, 16);             \
        c += d;                      \
        b ^= c;                      \
        b = ROTL(b, 12);             \
        a += b;                      \
        d ^= a;                      \
        d = ROTL(d, 8);              \
        c += d;                      \
        b ^= c;                      \
        b = ROTL(b, 7);
    
    unsigned char flag_enc[74] = {
        0x2E, 0xD2, 0xDF, 0x53, 0x41, 0xE6, 0x51, 0xA2, 0xD0, 0x8E,
        0x43, 0x59, 0x6F, 0xC4, 0x15, 0xAD,
        0x97, 0xC2, 0x98, 0xBD, 0x11, 0x05, 0xFE, 0xFF, 0x96, 0x4C,
        0xE8, 0x06, 0x50, 0x0E, 0x1D, 0xCA,
        0x0E, 0xB2, 0x18, 0xCA, 0x06, 0x54, 0x2E, 0xFA, 0xCD, 0x19,
        0xD2, 0x9E, 0xDB, 0x9E, 0x33, 0xCC,
        0x5D, 0xAF, 0xED, 0x69, 0x4A, 0xEF, 0x17, 0xB8, 0xD8, 0x40,
        0x14, 0x48, 0xCD, 0x37, 0xFC, 0xD0,
        0x14, 0x5C, 0x3C, 0x31, 0xC9, 0x15, 0xE6, 0xCF, 0x77, 0x28
    };
    
    void salsa20_core(uint32_t out[16], const uint32_t in[16]) {
        uint32_t x[16];
        int i;
    
        for (i = 0; i < 16; ++i) {
            x[i] = in[i];
        }
    
        for (i = 0; i < 10; ++i) {
            // 列轮
            QUARTERROUND(x[0], x[4], x[8], x[12])
            QUARTERROUND(x[5], x[9], x[13], x[1])
            QUARTERROUND(x[10], x[14], x[2], x[6])
            QUARTERROUND(x[15], x[3], x[7], x[11])
    
            // 行轮
            QUARTERROUND(x[0], x[1], x[2], x[3])
            QUARTERROUND(x[5], x[6], x[7], x[4])
            QUARTERROUND(x[10], x[11], x[8], x[9])
            QUARTERROUND(x[15], x[12], x[13], x[14])
        }
    
        for (i = 0; i < 16; ++i) {
            out[i] = x[i] + in[i];
        }
    }
    
    void salsa20_encrypt(uint8_t *ciphertext, size_t length, const uint8_t *key, const uint8_t *nonce, uint64_t counter) {
        uint32_t state[16];
        uint32_t block[16];
        size_t i, j;
        uint8_t *keystream = (uint8_t *)block;
        uint8_t *plaintext = (uint8_t *)malloc(74);
    
        for (int n = 0; n < 74; n++) {
            plaintext[n] = flag_enc[n];
        }
    
        // 初始化状态
        state[0] = 0x61707865;
        state[1] = *(uint32_t *)&key[0];
        state[2] = *(uint32_t *)&key[4];
        state[3] = *(uint32_t *)&key[8];
        state[4] = *(uint32_t *)&key[12];
        state[5] = 0x3320646e;
        state[6] = *(uint32_t *)&nonce[0];
        state[7] = *(uint32_t *)&nonce[4];
        state[8] = (uint32_t)(counter & 0xFFFFFFFF);
        state[9] = (uint32_t)(counter >> 32);
        state[10] = 0x79622d32;
        state[11] = *(uint32_t *)&key[16];
        state[12] = *(uint32_t *)&key[20];
        state[13] = *(uint32_t *)&key[24];
        state[14] = *(uint32_t *)&key[28];
        state[15] = 0x6b206574;
    
        for (i = 0; i < length; i += 64) {
            salsa20_core(block, state);
            for (j = 0; j < 64 && i + j < length; ++j) {
                ciphertext[i + j] = plaintext[i + j] ^ keystream[j];
            }
            // 更新计数器
            if (++state[8] == 0) {
                ++state[9];
            }
        }
    }
    
    int main() {
        uint8_t key[] = "welCoME_70_D3c7F_2025-r3VERSE!!!";
        uint8_t nonce[] = "CDEFGABdegab";
        uint64_t counter = (uint64_t)0x221221;
        uint8_t ciphertext[74];
    
        salsa20_encrypt(ciphertext, sizeof(flag_enc), key, nonce, counter);
    
        for (int i = sizeof(flag_enc) - sizeof(ciphertext); i < sizeof(flag_enc); i++) {
            printf("%c", ciphertext[i]);
        }
    
        return 0;
    }
    

    后面的题解参考官方wp

    image-20250603212432087

    void LZW_decode(std::vector<uint8_t> &compressed)
    {
        std::unordered_map<int, std::vector<uint8_t>> dictionary;
        for (int i = 0; i < 256; ++i)
        {
            dictionary[i] = {static_cast<uint8_t>(i)};
        }
    
        std::vector<uint8_t> codes;
        for (uint8_t byte : compressed)
        {
            codes.push_back(static_cast<int>(byte));
        }
    
        std::vector<uint8_t> result;
        uint8_t next_code = 0;
    
        if (codes.empty())
            return;
    
        uint8_t prev_code = codes[0];
        result.insert(result.end(), dictionary[prev_code].begin(), dictionary[prev_code].end());
    
        for (size_t i = 1; i < codes.size(); ++i)
        {
            uint8_t curr_code = codes[i];
            std::vector<uint8_t> entry;
    
            if (curr_code == next_code)
            {
                entry = dictionary[prev_code];
                entry.push_back(dictionary[prev_code][0]);
            }
            else if (dictionary.count(curr_code))
            {
                entry = dictionary[curr_code];
            }
            else
            {
                std::cerr << "Error: Invalid code " << curr_code << std::endl;
                return;
            }
    
            result.insert(result.end(), entry.begin(), entry.end());
    
            std::vector<uint8_t> new_entry = dictionary[prev_code];
            new_entry.push_back(entry[0]);
            dictionary[next_code++] = new_entry;
    
            prev_code = curr_code;
        }
    
        std::string decoded_string(result.begin(), result.end());
        std::cout << decoded_string << std::endl;
    }
    
    int main()
    {
        uint8_t key[] = "welCoME_70_D3c7F_2025-r3VERSE!!!";
        uint8_t nonce[] = "CDEFGABdegab";
        uint64_t counter = (uint64_t)0x221221;
        uint8_t ciphertext[74];
    
        salsa20_encrypt(ciphertext, sizeof(flag_enc), key, nonce, counter);
    
        std::vector<uint8_t> inputkeysname;
    
        for (int i = 0; i < sizeof(ciphertext); i++)
        {
            inputkeysname.push_back(ciphertext[i]);
        }
    
        LZW_decode(inputkeysname);
        return 0;
    }
    

    得到:

    bEbAACEBCGGGBdBECECbdaECCGGBAaAaedBEeDdGAdDaBgededFFBFeEaFGdFEbAFEAF gdgDBDBgeggbAeFagaEedbA

    from Crypto.Util.number import long_to_bytes
    
    enc = "bEbAACEBCGGGBdBECECbdaECCGGBAaAaedBEeDdGAdDaBgededFFBFeEaFGdFEbAFEAFgdgDBDBgeggbAeFagaEedbA"
    keys = "CDEFGABdegab"
    twelve = "0123456789AB"
    tmp = ""
    
    for i in enc:
        tmp += twelve[keys.index(i)]
    
    tmp = int(tmp, 12)
    print(long_to_bytes(tmp).decode())
    

    d3ctf{Fly1ng_Pi@n0_Key$_play_4_6e@utiful~melody}

    三、结语

    好题,但感觉像像套着apk的PC题
    image-20250603213245716

    最后

    一次蛮不错体验的比赛,就是比赛时间不太友好(端午假期+周五晚八点-周六晚九点[因开赛时平台卡顿延迟了一小时])。其他题目就没复现啦,详见https://github.com/D-3CTF/D3CTF-2025-Official-Writeup


    转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1621925986@qq.com

    💰

    ×

    Help us with donation