吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 870|回复: 4
上一主题 下一主题
收起左侧

[CTF] 【2026春节领红包】解题过程,纯手工分析(第八题、第十题未解出)

[复制链接]
跳转到指定楼层
楼主
hhxxhg 发表于 2026-3-5 19:29 回帖奖励
本帖最后由 hhxxhg 于 2026-3-5 19:38 编辑

第一次完整地参加这样的找flag活动。有些题连蒙带猜,有些题压根没做出来,但也有几题很有把握地答上了。全程凭感觉手工分析,ai只帮忙给了些提示。请大佬多多指教


第一题 送分题

不知道有没有不用关注公众号的办法

第二题 Windows 初级题

用ida静态分析,从start函数开始。函数开头出现了memset、strlen和memcpy等函数调用,可以推测这部分代码是用于命令行参数的处理。而最终用于返回的变量result来自sub_1CD130这个函数的返回值,这类似于c语言main函数的返回值,因此可以确定主函数就是sub_1CD130。

// write access to const memory has been detected, the output may be wrong!
int __usercall start@<eax>(int a1@<ebx>, int a2@<ebp>, int a3@<edi>, int a4@<esi>, char a5)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  dword_1DF054 = 0;
  v32 = retaddr;
  v31 = a2;
  v30 = a3;
  v29 = a4;
  v28 = a1;
  v27 = &a5;
  memset(v26, 0, sizeof(v26));
  StackBase = NtCurrentTeb()->NtTib.StackBase;
  while ( 1 )
  {
    v6 = _InterlockedCompareExchange(&dword_1DF030, (signed __int32)StackBase, 0);
    if ( !v6 )
    {
      v7 = 0;
      if ( dword_1DF034 == 1 )
        goto LABEL_38;
      goto LABEL_6;
    }
    if ( StackBase == (PVOID)v6 )
      break;
    Sleep(0x3E8u);
  }
  v7 = 1;
  if ( dword_1DF034 == 1 )
  {
LABEL_38:
    amsg_exit(31);
    if ( dword_1DF034 == 1 )
      goto LABEL_39;
LABEL_9:
    if ( v7 )
      goto LABEL_10;
    goto LABEL_40;
  }
LABEL_6:
  if ( dword_1DF034 )
  {
    dword_1DF010 = 1;
  }
  else
  {
    dword_1DF034 = 1;
    initterm(&dword_1E100C, &dword_1E1018);
  }
  if ( dword_1DF034 != 1 )
    goto LABEL_9;
LABEL_39:
  initterm(&First, &Last);
  dword_1DF034 = 2;
  if ( v7 )
    goto LABEL_10;
LABEL_40:
  _InterlockedExchange(&dword_1DF030, v7);
LABEL_10:
  if ( TlsCallback_0 )
    TlsCallback_0(0, 2, 0);
  sub_10A9E0();
  dword_1DF06C = (int)SetUnhandledExceptionFilter(TopLevelExceptionFilter);
  sub_121680(nullsub_1);
  sub_10A810();
  dword_1DF008 = 0x100000;
  v8 = _p__acmdln();
  v9 = 0;
  v10 = *v8;
  if ( !v10 )
    goto LABEL_25;
  while ( 1 )
  {
    v11 = *v10;
    if ( *v10 <= 32 )
      break;
    if ( v11 == 34 )
      v9 ^= 1u;
LABEL_17:
    ++v10;
  }
  if ( !v11 )
    goto LABEL_24;
  if ( (v9 & 1) != 0 )
  {
    v9 = 1;
    goto LABEL_17;
  }
  do
    v12 = *++v10;
  while ( v12 && v12 <= 32 );
LABEL_24:
  dword_1DF004 = (int)v10;
LABEL_25:
  if ( dword_1DF054 )
  {
    v13 = 10;
    if ( (v26[22] & 1) != 0 )
      v13 = v26[24];
    dword_1D1000 = v13;
  }
  v14 = dword_1DF024;
  v15 = 4 * dword_1DF024 + 4;
  v16 = malloc(v15);
  v24 = (int)v16;
  if ( v14 > 0 )
  {
    v17 = v16;
    v18 = (const char **)dword_1DF020;
    v23 = v15 - 4;
    v25 = dword_1DF020 + v15 - 4;
    do
    {
      v19 = *v18;
      ++v17;
      ++v18;
      Size = strlen(v19) + 1;
      v21 = malloc(Size);
      *(v17 - 1) = v21;
      memcpy(v21, *(v18 - 1), Size);
    }
    while ( (const char **)v25 != v18 );
    v16 = (_DWORD *)(v24 + v23);
  } //以上代码用于命令行参数处理等初始化工作
  *v16 = 0;
  dword_1DF020 = v24;
  sub_10A650();
  _initenv = dword_1DF01C;
  result = sub_1CD130(dword_1DF024);    //主函数
  dword_1DF018 = result;
  if ( !dword_1DF014 )
    exit(result);
  if ( !dword_1DF010 )
  {
    cexit();
    return dword_1DF018;
  }
  return result;
}

再看sub_1CD130。这个函数首先出现了大量提示文字字符串,说明这里确实包含了主函数的逻辑。然后查看Enter the password:字符串下方的代码。函数sub_1C5840的第二个参数传的是指针,容易联想到c语言中的scanf函数。可以断定inputted存储的是输入的文本。

int __cdecl sub_1CD130(char a1)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  v25 = &a1;
  v17 = sub_1CC800;
  p_Time = &Time;
  v18 = dword_1CE3D4;
  v19 = &savedregs;
  v20 = sub_1CD4EA;
  sub_10C330(v15);
  sub_10A650();
  v16 = -1;
  v1 = time(0);
  srand(v1);
  SetConsoleOutputCP(0xFDE9u);
  sub_1C7E50(&dword_1D27C0, "========================================");
  sub_1015B0(Time);
  sub_1C7E50(&dword_1D27C0, "   CrackMe Challenge v2.5 - 2026        ");
  sub_1015B0(Time);
  sub_1C7E50(&dword_1D27C0, "========================================");
  sub_1015B0(Time);
  sub_1C7E50(&dword_1D27C0, "Keywords: 52pojie, 2026, Happy new year");
  sub_1015B0(Time);
  sub_1C7E50(&dword_1D27C0, "Hint: Fake flag; length is key");
  sub_1015B0(Time);
  sub_1C7E50(&dword_1D27C0, "----------------------------------------");
  sub_1015B0(Time);
  inputted = &v24;
  v23 = 0;
  v24 = 0;
  v16 = 1;
  v13 = &inputted;
  sub_1C7E50(&dword_1D27C0, "\n[?] Enter the password: ");
  v13 = &inputted;
  v2 = ((int (*)(void))sub_101560)();
  v13 = &inputted;
  sub_1C5840(&dword_1D25E0, (int)&inputted, (void *)v2);
  …………

接下来是验证flag的部分。这段代码进行了多次验证:

  1. 调用sub_101740进行验证,验证成功后只会提示You're getting closer...,说明这是假验证

  2. 通过对比byte_1D3032的值进行验证,验证成功后只会提示Nice try, but not quite right...,说明这是假验证

  3. flag长度验证,观察提示文字The length is your first real challenge.可推测if判断中调用的函数(原名sub_120EB8)是strlen,31为正确flag的长度

  4. 调用doVerify(原名sub_1016D0)验证,成功后提示文字最多,所以是真验证

  5. Str += ++v9 * v8;计算校验值进行验证

int __cdecl sub_1CD130(char a1)
…………
  Str = inputted;
  v3 = sub_101740((int)inputted);   //第一次flag验证
  v4 = '5';
  v5 = v3;
  v6 = 0;
  if ( !v5 )   //第一次flag验证结果
  {
    while ( Str[v6] == v4 )
    {
      if ( ++v6 == 16 )   //第二次flag验证提示
      {
        v16 = 1;
        sub_1C7E50(&dword_1D27C0, "\n[!] You're getting closer...");// 52pojie2026Happy
        v16 = 1;
        goto exit_1;
      }
      v4 = byte_1D3032[v6];   //第二次flag验证
    }
    if ( strlen(Str) != 31 )   //flag长度验证
    {
      v16 = 1;
      sub_1C7E50(&dword_1D27C0, "\n[!] Hint: The length is your first real challenge.");
      goto exit_1;
    }
    v16 = 1;
    if ( doVerify((int)Str, 31) )   //第三次flag验证
    {
      Str = 0;
      v8 = *inputted;
      if ( !*inputted )
        goto chksum_failed;
      v9 = 0;
      do
      {
        Str += ++v9 * v8;
        v8 = inputted[v9];
      }
      while ( v8 );
      if ( Str != (char *)44709 )   //第四次flag验证
      {
chksum_failed:
        v16 = 1;
        sub_1C7E50(&dword_1D27C0, "\n[!] Checksum failed! Something is wrong...");
        sub_1015B0(Time);
        sub_1C7E50(&dword_1D27C0, "[!] Expected: 44709, Got: ");
        sub_17DE70(Str);
        sub_1015B0(v10);
exit:
        v16 = 1;
        goto exit_0;
      }
      v16 = 1;          //flag验证成功提示
      sub_1C7E50(&dword_1D27C0, "\n========================================");
      sub_1015B0(Time);
      sub_1C7E50(&dword_1D27C0, "        *** SUCCESS! ***                ");
      sub_1015B0(Time);
      sub_1C7E50(&dword_1D27C0, "========================================");
      sub_1015B0(Time);
      sub_1C7E50(&dword_1D27C0, "[+] Congratulations! You cracked it!");
      sub_1015B0(Time);
      v11 = sub_1C7E50(&dword_1D27C0, "[+] Correct flag: ");
      sub_1C4330(v11, inputted, v23);
    }
    else
    {
      v16 = 1;
      sub_1C7E50(&dword_1D27C0, "\n[X] Access Denied!");
      sub_1015B0(Time);
      sub_1C7E50(&dword_1D27C0, "[X] Wrong password. Keep trying!");
    }
    sub_1015B0(Time);
    goto exit;
  }   //第一次flag验证成功提示
  sub_1C7E50(&dword_1D27C0, "\n[!] Nice try, but not quite right...");// pojie_2026_HappyNewYearXXXX
exit_1:
  sub_1015B0(Time);
exit_0:
  sub_1017C0();
  sub_1B1AE0(v13);
  sub_10C600(v15);
  return 0;
}

所以,正确的flag应该藏在doVerify中。分析这个函数的逻辑,先是使用malloc分配内存,再调用fillBlock获取对比文本,也就是正确的flag。然后将对比文本与输入字符串inputtedStr比较,统计正确字符的个数,当所有字符均与flag时返回true。

fillBlock填充了一系列加密后的数值,再对数值进行了异或计算。

bool __cdecl doVerify(char *inputtedStr, int valueIs31)
{
  unsigned __int8 *Block; // ebp
  int index; // eax
  int countOfCorrectChars; // ebx
  bool isCurrentCharCorrect; // dl

  Block = (unsigned __int8 *)malloc_wrapped(0x64u);
  fillBlock((int)Block);
  if ( valueIs31 <= 0 )
  {
    countOfCorrectChars = 0;
  }
  else
  {
    index = 0;
    countOfCorrectChars = 0;
    do
    {
      isCurrentCharCorrect = inputtedStr[index] == Block[index];
      ++index;
      countOfCorrectChars += isCurrentCharCorrect;
    }
    while ( valueIs31 != index );
  }
  j_j_free(Block);
  return valueIs31 == countOfCorrectChars;
}

char *__cdecl fillBlock(int *memblk)
{
  char *result; // eax

  *memblk = 0x2D327077;
  memblk[1] = 0x63272B28;
  memblk[2] = 0x701D6363;
  memblk[3] = 0x1D747072;
  memblk[4] = 0x3232230A;
  memblk[5] = 0x272C1D3B;
  memblk[6] = 0x273B1D35;
  *((_BYTE *)memblk + 30) = 0x63;
  *((_WORD *)memblk + 14) = 0x3023;
  result = (char *)memblk;
  do
    *result++ ^= 0x42u;                         // xor byte by byte
  while ( result != (char *)memblk + 31 );
  *((_BYTE *)memblk + 31) = 0;
  return result;
}

只需执行一遍fillBlock函数就能得到正确的flag,我将这个函数改写为了python形式:

import array

memblk=array.array("L",bytes(32))
memblk[0] = 0x2D327077;
memblk[1] = 0x63272B28;
memblk[2] = 0x701D6363;
memblk[3] = 0x1D747072;
memblk[4] = 0x3232230A;
memblk[5] = 0x272C1D3B;
memblk[6] = 0x273B1D35;
memblk=array.array("B",memblk.tobytes())
memblk[30] = 0x63;
memblk=array.array("H",memblk.tobytes())
memblk[14] = 0x3023;

memblk=array.array("B",memblk.tobytes())
for i in range(len(memblk)-1):memblk[i]^=0x42

print(memblk.tobytes())

执行后输出b'52pojie!!!_2026_Happy_new_year!\x00',其中\x00是c字符串的尾部,去掉就是flag了

第三题 Android 初级题

这道题我是靠游戏通关得到flag的。apk拖进jeb分析了半天没找到思路。。。

第四题 PyInstaller

通过观察exe的图标,可看出这是通过PyInstaller进行的打包。

找到pyinstxtractor工具进行解包,得到运行组件库和pyc文件。再找到了一个在线还原pyc文件的网站

得到源代码:

import hashlib
import base64
import sys
def xor_decrypt(data,key):
  result = bytearray()
  for i,byte in enumerate(data):
    result.append(byte^key^i&255)

  return result.decode('utf-8',errors='ignore')

def get_encrypted_flag():
  enc_data = 'e3w+fiRvfW18fnx4ZAZ6Pj43YwB9OWMXfXo8Dg4O'
  return base64.b64decode(enc_data)

def generate_flag():
  encrypted = get_encrypted_flag()
  key = 78
  result = bytearray()
  for i,byte in enumerate(encrypted):
    result.append(byte^key)

  return result.decode('utf-8')

def calculate_checksum(s):
  total = 0
  for i,c in enumerate(s):
    total += ord(c)*i+1

  return total

def hash_string(s):
  return hashlib.sha256(s.encode()).hexdigest()

def verify_flag(user_input):
  correct_flag = generate_flag()
  if len(user_input) != len(correct_flag):
    return False
  else:
    for i in range(len(correct_flag)):
      if user_input[i] != correct_flag[i]:
        continue

      return False

    return True

def fake_check_1(user_input):
  fake_hash = 'a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890'
  return hash_string(user_input) == fake_hash

def fake_check_2(user_input):
  fake_hash = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
  return hash_string(user_input) == fake_hash

def main():
  print('==================================================')
  print('   CrackMe Challenge - Python Edition')
  print('==================================================')
  print('Keywords: 52pojie, 2026, Happy New Year')
  print('Hint: Decompile me if you can!')
  print('--------------------------------------------------')
  user_input = input('\n[?] Enter the password: ').strip()
  if fake_check_1(user_input):
    print('\n[!] Nice try, but not quite right...')
    input('\nPress Enter to exit...')
    return None
  else:
    if fake_check_2(user_input):
      print('\n[!] You\'re getting closer...')
      input('\nPress Enter to exit...')
      return None
    else:
      if verify_flag(user_input):
        checksum = calculate_checksum(user_input)
        expected_checksum = calculate_checksum(generate_flag())
        if checksum == expected_checksum:
          print('\n==================================================')
          print('        *** SUCCESS! ***')
          print('==================================================')
          print('[+] Congratulations! You cracked it!')
          print(f'''[+] Correct flag: {user_input}''')
        else:
          print('\n[!] Checksum failed!')

      else:
        print('\n[X] Access Denied!')
        print('[X] Wrong password. Keep trying!')

      input('\nPress Enter to exit...')
      return None

if __name__ == '__main__':
  try:
    main()
  except KeyboardInterrupt:
    print('''

[!] Interrupted by user''')
    sys.exit(0)

将generate_flag执行一次就能得到flag了

第五题 Nuitka

这个题我是靠动态调试和猜测破开的。。

首先用nuitka-extractor解包,得到的内容中crackme_hard.dll很明显是需要分析的dll。ida解析后,唯一一个导出函数是run_code。大概翻下来一看,这个函数先进行了初始化,加载了.bytecode、python对象和其他东西,然后在这个函数体即将结束的部分出现了一个函数指针的调用

ai告诉我,nuitka打包的程序“编译后的函数对象是本地(native)编译函数,code object 的 co_code 为空且没有字节码”,并且“无法对已编译的函数进行字节码级别的调试/追踪”。因此我认为只是分析初始化部分的逻辑可能不太行,所以在函数指针部分附近下断点开始动态调用。

配置调试器时发现crackme_hard无法直接启动,找了一个启动器,然后像这样配置

开始运行

没断下来,但执行到了下方的sub_7FFC540D6110,这个函数内容很简单

_DWORD *__fastcall sub_7FFC540D6110(__int64 a1)
{
  return main_sub_7FFDBF84D9B0(a1, "__main__");
}

出现了__main__,看来再跟几步就能出现主函数。sub_7FFDBF84D9B0又包含了很多函数逻辑,但同时也有PyUnicode_FromString这样的python库调用。我认为这个函数应该是nuitka自行插入的功能通用函数,而主函数藏在其中某个调用中,所以我狂按f8步过,同时观察控制台窗口,当提示文字出现就说明执行的最后一行代码会进入主函数。


按这样的方式步入sub_7FFDBF84D9B0、sub_7FFC5E78DC90、sub_7FFC5E78A9B0、sub_7FFC5E76B340、sub_7FFC5E783A00,最后在sub_7FFC5E783A00中进入一个函数指针,找到主函数sub_7FFC5E769C50,我把它命名为real_main。

  • 这个函数只有一个参数,并且这个参数会传给其他函数的第一个参数,所以我假设它为python库的上下文结构体。
  • 经过逐个的单步调试可以确定printstrfunc这个函数相当于python的print,并以此排除这个函数前半段冗长的打印提示文字的部分。这个函数的第三个参数是一个包含了PyUnicode结构体的PyTuple结构体,而对于PyUnicode结构体,其字符串就跟在开头的元数据后方
  • 直接运行然后直接暂停,ida这边会因为得不到控制权而断不下来。这时在控制台随便输入一个文本然后回车,ida这边才处于暂停状态,shift+f7让函数返回,回到real_main得到获取输入文本的位置。
  • 获得输入文本后,会再次出现大量不知所谓的代码,但经过这些代码后会开始调用一个返回python312__Py_FalseStruct的函数,并且python312__Py_FalseStruct会被get_bool_val处理为0。我猜测这应该是校验flag的逻辑。
    • 但是整个函数中类似的逻辑出现了好几次,那么可以大致说明这里也有和前几题一样的假验证和真验证。给每个验证加上断点逐个测试,最后发现输出结果至v112后就会出现Access Denied的提示。那么我猜测真验证大概就在这里了
// write access to const memory has been detected, the output may be wrong!
__int64 __fastcall real_main(_QWORD *ctx)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

    //初始化部分
………………
  v13 = (__int64 *)printstrfunc((__int64)ctx, v12, eqeqqeqeqeeq);
…………
  v20 = (__int64 *)printstrfunc((__int64)ctx, v19, crkmechellenge);
…………
  v25 = (__int64 *)printstrfunc((__int64)ctx, v24, eqeqqeqeqeeq);
………………
  v35 = (__int64 *)printstrfunc((__int64)ctx, v34, hint);
………………
  v40 = (__int64 *)printstrfunc((__int64)ctx, v39, trytrytry);
………………
  v45 = (__int64 *)printstrfunc((__int64)ctx, v44, mmmmm);
………………
  if ( !cfunc )
  {
………………
  }     // -----------------------------初始化结束
  if ( v54 )                 // 读取输入文本
    inputStrObj = (__int64 *)verify_func((__int64)ctx, (__int64)v53, v54);
  else
    inputStrObj = sub_7FFC5E783A00((__int64)ctx, (__int64)v53);
  if ( !inputStrObj )    //不知所谓的代码
  {
    v89 = ctx[12];
    is1stsucceeded = 0i64;
    inputstrptr = 0i64;
    v90 = 99i64;
    ctx[12] = 0i64;
    goto LABEL_115;
  }
  adict1.m128_u64[0] = (unsigned __int64)inputStrObj;
  inputstrptr = (*(__int64 (__fastcall **)(__int64, __m128 *, __int64, _QWORD))(PyMethodDescr_Type
                                                                              + *(_QWORD *)(*(_QWORD *)(PyMethodDescr_Type + 8)
                                                                                          + 56i64)))(
                  PyMethodDescr_Type,
                  &adict1,
                  1i64,
                  0i64);
  if ( (int)*inputStrObj >= 0 )
  {
    v55 = *inputStrObj - 1;
    *inputStrObj = v55;
    if ( !v55 )
      (*(void (__fastcall **)(__int64 *))(inputStrObj[1] + 48))(inputStrObj);
  }
  if ( !inputstrptr )
  {
    v89 = ctx[12];
    is1stsucceeded = 0i64;
    inputStrObj = 0i64;
    v90 = 99i64;
    ctx[12] = 0i64;
    goto LABEL_115;
  }
  v56 = *((_QWORD *)adict + 4);
  if ( !*(_BYTE *)(v56 + 0xA) )                 // 1
  {
    v165 = (__int64 *)sub_7FFC5E768B20((__int64)adict, *(__int64 *)&fakechk);
    if ( v165 )
    {
      ptrval1 = *v165;
      goto LABEL_243;
    }
    goto LABEL_150;
  }
  val46hreg = *(_DWORD *)(v56 + 0xC);
  if ( !val46hreg )
  {
    v58 = ctx[2];
    val46hreg = *(_DWORD *)(v58 + 270616);
    *(_DWORD *)(v58 + 0x42118) = val46hreg + 1;
    *(_DWORD *)(v56 + 12) = val46hreg;
  }
  if ( val46h != val46hreg )
  {
    val46h = val46hreg;
    v59 = sub_7FFC5E7803A0(v56, *(__int64 *)&fakechk, *(_QWORD *)(*(_QWORD *)&fakechk + 24i64));
    qword_7FFC5E7C8730 = v59;
    if ( v59 >= 0 )
      goto LABEL_63;
LABEL_150:
    v106 = *(_QWORD *)&fakechk;
    goto LABEL_151;
  }
  v59 = qword_7FFC5E7C8730;
  if ( qword_7FFC5E7C8730 < 0 )
    goto LABEL_150;
LABEL_63:
  __name__ = v56 + (1i64 << *(_BYTE *)(v56 + 9)) + 0x20;
  ptrval1 = *(_QWORD *)(__name__ + 16 * v59 + 8);
  if ( ptrval1 )
    goto LABEL_64;
  v221 = *(_QWORD *)&fakechk;
  v170 = sub_7FFC5E7803A0(v56, *(__int64 *)&fakechk, *(_QWORD *)(*(_QWORD *)&fakechk + 24i64));
  v106 = v221;
  qword_7FFC5E7C8730 = v170;
  if ( v170 < 0 )
  {
LABEL_151:
    v107 = (__int64 *)sub_7FFC5E768B20(dict2, v106);
    if ( !v107 || (ptrval1 = *v107) == 0 )
    {
      is1stsucceeded = 0i64;
      inputStrObj = 0i64;
      sub_7FFC5E781640((__int64)ctx, (__int64 *)&adict1, *(__int64 *)&fakechk);
      v89 = adict1.m128_u64[0];
      v90 = 101i64;
      goto LABEL_115;
    }
    goto LABEL_64;
  }
  ptrval1 = *(_QWORD *)(__name__ + 16 * v170 + 8);
LABEL_243:
  if ( !ptrval1 )
    goto LABEL_150;
LABEL_64:
  *(_DWORD *)(v4 + 0x28) = 'e';                 // a->e
  pybool_1 = verify_func((__int64)ctx, ptrval1, (_DWORD *)inputstrptr);// 第一次假校验,返回false即为失败
  inputStrObj = (__int64 *)pybool_1;
  if ( pybool_1 )
  {
    is1stsucceeded = (unsigned int)get_bool_val(pybool_1);
    v64 = *inputStrObj;
    if ( (_DWORD)is1stsucceeded == -1 )
    {
      v89 = ctx[12];
      ctx[12] = 0i64;
      if ( (int)v64 >= 0 )
      {
        v167 = v64 - 1;
        *inputStrObj = v167;
        if ( !v167 )
          (*(void (__fastcall **)(__int64 *))(inputStrObj[1] + 48))(inputStrObj);
      }
      v90 = 101i64;
      is1stsucceeded = 0i64;
      inputStrObj = 0i64;
      goto LABEL_115;
    }
    if ( (int)v64 >= 0 )
    {
      v65 = v64 - 1;
      *inputStrObj = v65;
      if ( !v65 )
        (*(void (__fastcall **)(__int64 *))(inputStrObj[1] + 48))(inputStrObj);
    }
    if ( (_DWORD)is1stsucceeded )
    {
    ……………………
    }
    else
    {
      *(_DWORD *)(v4 + 40) = 'j';
      v112 = invoke((__int64)ctx, (__int64)v16, *(__int64 *)&verify, (_DWORD *)inputstrptr);// 在此步入,如果这里返回false校验就会失败
      inputStrObj = (__int64 *)v112;
      if ( v112 )
      {
        is2ndsucceed = get_bool_val(v112);
        v115 = *inputStrObj;
        if ( is2ndsucceed == -1 )
        {
          v89 = ctx[12];
          ctx[12] = 0i64;
          if ( (int)v115 >= 0 )
          {
            v206 = v115 - 1;
            *inputStrObj = v206;
            if ( !v206 )
              (*(void (__fastcall **)(__int64 *))(inputStrObj[1] + 48))(inputStrObj);
          }
          v90 = 106i64;
          is1stsucceeded = 0i64;
          inputStrObj = 0i64;
          goto LABEL_115;
        }
        if ( (int)v115 >= 0 )
        {
          v116 = v115 - 1;
          *inputStrObj = v116;
          if ( !v116 )
            (*(void (__fastcall **)(__int64 *))(inputStrObj[1] + 48))(inputStrObj);
        }
        if ( !is2ndsucceed )
        {
          v156 = sub_7FFC5E7624F0(printStr, v113);
          *(_DWORD *)(v4 + 40) = 119;
          v157 = (__int64 *)printstrfunc((__int64)ctx, v156, qword_7FFC5E7C8298);// 输出: [X] Access Denied!
…………
}

接下来进一步步入,经过以下函数:

__int64 __fastcall invoke(__int64 a1, __int64 a2, __int64 a3, _DWORD *a4)
{
………………

  v8 = *(_QWORD *)(a2 + 8);
  v9 = *(__int64 (__fastcall **)(__int64, __int64))(v8 + 144);
  if ( !v9 )
  {
………………
  }
  v10 = v9(a2, a3);                             // PyObject_GenericGetAttr->null
  v11 = (_QWORD *)v10;
  if ( !v10 )
    return 0i64;
  v12 = *(_QWORD *)(v10 + 8);
  v13 = *(__int64 (__fastcall **)(_QWORD *, __int64, __int64))(v12 + 272);
  if ( v13 && *(_QWORD *)(v12 + 280) )
  {
………………
  }
  result = verify_func(a1, (__int64)v11, a4);   // 在此步入,返回false即为失败
  if ( (int)*v11 >= 0 )
  {
…………
  }
  return result;
}

__int64 __fastcall verify_func(__int64 a1, __int64 a2, _DWORD *str)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  m_object = *(_QWORD *)(a2 + 8);
  str_0 = str;
  a22 = a2;
  if ( (__int64 *)m_object == &qword_7FFC5E7B5260 )
  {
……………………
    return 0i64;
  }
  if ( (_UNKNOWN *)m_object == &null )
  {
……………………
        PyErr_Format(
          PyExc_TypeError,
          "unbound compiled_method %s%s must be called with %s instance as first argument (got %s instance instead)",
          CALLABLE_NAME,
          CALLABLE_DESC,
          CLASS_NAME,
          INSTANCE_CLASS_NAME);
      }
      return v10;
    }
    a22 = *(_QWORD *)(a2 + 16);
    if ( *(_BYTE *)(a22 + 80) )
    {
      v14 = *(_QWORD *)(a22 + 64);
      if ( v14 == 2 )
      {
        v49 = *(_DWORD *)v10;
        v88 = (_DWORD *)v10;
        v50 = v49 + 1;
        if ( v50 )
          *(_DWORD *)v10 = v50;
        v51 = *str_0;
        v89 = str_0;
        v52 = v51 + 1;
        if ( v52 )
          *str_0 = v52;
        return (*(__int64 (__fastcall **)(__int64, __int64, _DWORD **))(a22 + 120))(a1, a22, &v88); //在此步入,函数指针,进入验证函数
      }
      if ( v14 == *(_QWORD *)(a22 + 160) + 2i64 )
      {
        v43 = alloca(sub_7FFC5E79BFC0());
………………
}

最后进入验证函数verify_1(即sub_7FFC5E767880),在这里进行最后的分析。

  • 从第一句开始,省略掉冗余的代码后,出现了一个函数调用,被调用后返回的是PyLong类型的对象,分析这个函数(见下方代码注释)后,可得出这个函数功能是用于字符串长度(这里记为len),进而推测出这里存在长度验证
  • 再往后,后续代码中疑似出现了有关数值比较的逻辑(见下方代码注释),根据这些逻辑,反推出正确的长度的变量位于totlen1,查看内存得到正确长度30。
  • 重新开始调试,这次输入长度30的字符串123456789012345678901234567890,调试会进入后续的for循环代码块,这里应该开始进行字符串的对比
    • 继续单步调试,过上几步后会再次遇见函数调用,第三个参数是PyUnicode类型,根据它的字符串内容可判断这里是在获取正确flag的字符,因此分析这个调用返回的内容就能获得flag
    • 但是这里只返回了第一个字符5。所以将输入的内容改为523456789012345678901234567890再次进行调试,以此类推得到第二个字符2,第三个p……最终拼凑出整个flag
// write access to const memory has been detected, the output may be wrong!
int *__fastcall verify_1(_QWORD *a1, __int64 a2, __int64 **a3)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  v3 = typetype;
  total_len_source = *a3;
  instr = a3[1];    //a3应该是*args,而a3[1]应该是该函数对应的python代码的第一个参数
  if ( !typetype )
  {
LABEL_5:
………………
  }
  v6 = *(_QWORD *)typetype;
  if ( *(__int64 *)typetype > 1 )
  {
………………
  }
  if ( *(_QWORD *)(typetype + 16) )
  {
………………
  }
LABEL_6:
  v7 = (__int64 *)a1[7];
  v8 = *v7;
  *(_QWORD *)(v3 + 80) = *v7;
  *v7 = v3 + 72;
  if ( v8
    && (*(_BYTE *)(v8 + 70) == 1
     || *(_QWORD *)(v8 + 56) >= (unsigned __int64)(*(_QWORD *)v8 + 2i64 * *(int *)(*(_QWORD *)v8 + 176i64) + 192)) )
  {
………………
  }
  v11 = *(_DWORD *)v3;
  *(_DWORD *)(v3 + 64) = 0;
  v12 = v11 + 1;
  if ( v12 )
    *(_DWORD *)v3 = v12;
  instrlen = len((__int64)a1, (__int64)instr);  //输入的长度
  /*
  len函数(sub_7FFC5E781CB0)中包含字符串"object of type '%s' has no len()",因此我给该函数取名len
  */
  if ( !instrlen )
  {
…………
    return 0i64;
  }
  totlen1 = sub_7FFC5E797280((__int64)total_len_source, _total_len);// 正确的flag长度,步过此句后,查看totlen1的内存可知,正确的flag字符串长度应为0x1E也就是30
  totlen2 = (_QWORD *)totlen1;
  if ( !totlen1 )
  {
    v126 = (_QWORD *)a1[12];
    a1[12] = 0i64;
    v176 = v126;
    if ( (int)*(_QWORD *)instrlen >= 0 )
    {
      v127 = *(_QWORD *)instrlen - 1i64;
      *(_QWORD *)instrlen = v127;
      if ( !v127 )
        (*(void (__fastcall **)(__int64))(*(_QWORD *)(instrlen + 8) + 48i64))(instrlen);
    }
    instrlen = 0i64;
    v94 = 48i64;
    goto LABEL_195;
  }
  v17 = *(_QWORD *)(totlen1 + 8);
  v18 = PyLong_Type[0];
  if ( v17 == PyLong_Type[0] )
  {
    v28 = 0;
    if ( instrlen != totlen1 )
    {
      v28 = 1;
      v45 = *(_QWORD *)(instrlen + 16) >> 3;
      v14 = v45 * (1 - (*(_QWORD *)(instrlen + 16) & 3i64));
      if ( v14 == (totlen2[2] >> 3) * (1 - (totlen2[2] & 3i64)) )
      {
        do
        {
          if ( v45-- == 0 )
          {
            v28 = 0;
            goto LABEL_30;
          }
        }
        while ( *(_DWORD *)(instrlen + 4 * v45 + 24) == *((_DWORD *)totlen2 + v45 + 6) );
        v28 = 1;
      }
    }
    goto LABEL_30;
  }
  v19 = *(_QWORD *)(v17 + 344);
  if ( v19 )
  {
    v20 = *(_QWORD *)(v19 + 16);
    if ( v20 > 0 )
    {
      v21 = 0i64;
      while ( *(_QWORD *)(v19 + 8 * v21 + 24) != PyLong_Type[0] )
      {
        if ( v20 == ++v21 )
          goto LABEL_168;
      }
LABEL_23:
      v22 = *(__int64 (__fastcall **)(_QWORD *, __int64, __int64))(v17 + 200);
      if ( v22 )
      {
        v167 = PyLong_Type[0];
        v23 = v22(totlen2, instrlen, 3i64);   //出现了疑似比较数值大小的调用
        v18 = PyLong_Type[0];
        v24 = (_QWORD *)v23;
        if ( v23 != Py_NotImplementedStruct[0] )
        {
          if ( v23 )
            goto LABEL_26;
          goto LABEL_188;
        }
        v131 = (__int64 (__fastcall *)(__int64, _QWORD *, __int64))PyLong_Type[25];
        if ( v131 )
        {
          v132 = v131(instrlen, totlen2, 3i64);   //出现了疑似比较数值大小的调用
          v18 = PyLong_Type[0];
          v99 = (_QWORD *)v132;
          if ( (_QWORD *)v132 != v24 )
            goto LABEL_170;
        }
LABEL_190:
        v28 = instrlen != (_QWORD)totlen2;
        goto LABEL_30;
      }
      v97 = (__int64 (__fastcall *)(__int64, _QWORD *, __int64))PyLong_Type[25];
      if ( !v97 )
        goto LABEL_190;
      goto LABEL_169;
    }
  }
  else
  {
    v133 = *(_QWORD *)(totlen1 + 8);
    while ( 1 )
    {
      v133 = *(_QWORD *)(v133 + 256);
      if ( !v133 )
        break;
      if ( v133 == PyLong_Type[0] )
        goto LABEL_23;
    }
    if ( PyLong_Type[0] == PyBaseObject_Type[0] )
      goto LABEL_23;
  }
LABEL_168:
  v97 = (__int64 (__fastcall *)(__int64, _QWORD *, __int64))PyLong_Type[25];
  if ( !v97 )
  {
LABEL_185:
    v106 = *(__int64 (__fastcall **)(_QWORD *, __int64, __int64))(v17 + 200);
    if ( v106 )
    {
      v167 = v18;
      v107 = v106(totlen2, instrlen, 3i64);   //出现了疑似比较数值大小的调用
      v18 = v167;
      v24 = (_QWORD *)v107;
      if ( v107 != Py_NotImplementedStruct[0] )
      {
        if ( v107 )
        {
LABEL_26:
          bool_val = get_bool_val((__int64)v24);
          v18 = v167;
          v26 = bool_val;
          if ( (int)*v24 >= 0 )
          {
            v27 = *v24 - 1i64;
            *v24 = v27;
            if ( !v27 )
            {
              (*(void (__fastcall **)(_QWORD *))(v24[1] + 48i64))(v24);
              v18 = v167;
            }
          }
          v28 = v26 != 0;
          goto LABEL_30;
        }
LABEL_188:
        v28 = -1;
        goto LABEL_30;
      }
    }
    goto LABEL_190;
  }
LABEL_169:
  v98 = v97(instrlen, totlen2, 3i64);   //出现了疑似比较数值大小的调用
  v18 = PyLong_Type[0];
  v99 = (_QWORD *)v98;
  if ( v98 == Py_NotImplementedStruct[0] )
    goto LABEL_185;
LABEL_170:
  if ( !v99 )
    goto LABEL_188;
  v177 = v18;
  v100 = get_bool_val((__int64)v99);
  v18 = v177;
  v101 = v100;
  if ( (int)*v99 >= 0 )
  {
    v102 = *v99 - 1i64;
    *v99 = v102;
    if ( !v102 )
    {
      (*(void (__fastcall **)(_QWORD *))(v99[1] + 48i64))(v99);
      v18 = v177;
    }
  }
  v28 = v101 != 0;
LABEL_30:
  if ( (int)*(_QWORD *)instrlen >= 0 )
  {
    v29 = *(_QWORD *)instrlen - 1i64;
    *(_QWORD *)instrlen = v29;
    if ( !v29 )
    {
      v168 = v18;
      (*(void (__fastcall **)(__int64))(*(_QWORD *)(instrlen + 8) + 48i64))(instrlen);
      v18 = v168;
    }
  }
  if ( (int)*totlen2 >= 0 )
  {
    v30 = *totlen2 - 1i64;
    *totlen2 = v30;
    if ( !v30 )
    {
      v169 = v18;
      (*(void (__fastcall **)(_QWORD *))(totlen2[1] + 48i64))(totlen2);
      v18 = v169;
    }
  }
  if ( v28 == -1 )
  {
    v129 = (_QWORD *)a1[12];
    v94 = 48i64;
    totlen2 = 0i64;
    instrlen = 0i64;
    a1[12] = 0i64;
    v176 = v129;
    goto LABEL_195;
  }
  if ( v28 == 1 )
  {
    v31 = (int *)Py_FalseStruct;    //出现了布尔值,大概是判断长度验证是否通过
    if ( Py_FalseStruct != -1 )
      ++Py_FalseStruct;
    v32 = 0i64;
    instrlen = 0i64;
    goto criticalFailure;    //未通过将直接返回False
  }
  v170 = v18;
  v47 = sub_7FFC5E797280((__int64)total_len_source, _total_len);
  instrlen = v47;
  if ( !v47 )
  {
    v134 = (_QWORD *)a1[12];
    totlen2 = 0i64;
    a1[12] = 0i64;
    v94 = 51i64;
    v176 = v134;
    goto LABEL_195;
  }
  range = sub_7FFC5E789720(a1, v47);
  v49 = v170;
  totlen2 = (_QWORD *)range;
  if ( (int)*(_QWORD *)instrlen >= 0 )
  {
    v50 = *(_QWORD *)instrlen - 1i64;
    *(_QWORD *)instrlen = v50;
    if ( !v50 )
    {
      (*(void (__fastcall **)(__int64))(*(_QWORD *)(instrlen + 8) + 48i64))(instrlen);
      v49 = v170;
    }
  }
  if ( !totlen2 )
    goto LABEL_240;
  v171 = v49;
  v51 = sub_7FFC5E7995A0(totlen2);
  v52 = v171;
  v53 = (__int64 *)v51;
  if ( (int)*totlen2 >= 0 )
  {
    v54 = *totlen2 - 1i64;
    *totlen2 = v54;
    if ( !v54 )
    {
      (*(void (__fastcall **)(_QWORD *))(totlen2[1] + 48i64))(totlen2);
      v52 = v171;
    }
  }
  totlen2 = 0i64;
  if ( !v53 )
  {
LABEL_240:
    v130 = (_QWORD *)a1[12];
    instrlen = 0i64;
    a1[12] = 0i64;
    v94 = 51i64;
    v176 = v130;
    goto LABEL_195;
  }
  v181 = v52;
  for ( i = 0i64; ; i = (__int64 *)instrlen )   //这里出现了循环
  {
    _iternext = (_DWORD *)(*(__int64 (__fastcall **)(__int64 *))(v53[1] + 224))(v53);
    instrlen = (__int64)_iternext;
    if ( !_iternext )
    {
      v139 = i;
      if ( !(unsigned __int8)sub_7FFC5E7615A0(a1, PyExc_StopIteration) )//这里出现了迭代有关的处理逻辑,意味着这里大概存在字符串的对比
      {
        v157 = (_QWORD *)a1[12];
        a1[12] = 0i64;
        v176 = v157;
        if ( !i )
        {
          v94 = 51i64;
LABEL_164:
          if ( (int)*v53 >= 0 )
          {
            v96 = *v53 - 1;
            *v53 = v96;
            if ( !v96 )
            {
              v183 = v94;
              (*(void (__fastcall **)(__int64 *))(v53[1] + 48))(v53);
              v94 = v183;
            }
          }
          goto LABEL_195;
        }
        instrlen = (__int64)i;
        v94 = 51i64;
        goto LABEL_161;
      }
      v140 = a1[7];
      v141 = *(_QWORD *)(*(_QWORD *)v140 + 48i64);
      v142 = *(__int64 **)(v141 + 16);
      *(_QWORD *)v140 = *(_QWORD *)(*(_QWORD *)v140 + 8i64);
      *(_DWORD *)(v141 + 64) = -1;
      if ( v142 )
      {
        v143 = *v142;
        *(_QWORD *)(v141 + 16) = 0i64;
        if ( (int)v143 >= 0 )
        {
          v144 = v143 - 1;
          *v142 = v144;
          if ( !v144 )
            (*(void (**)(void))(v142[1] + 48))();
        }
      }
      if ( (int)*(_QWORD *)v141 >= 0 )
      {
        v145 = *(_QWORD *)v141 - 1i64;
        *(_QWORD *)v141 = v145;
        if ( !v145 )
          (*(void (__fastcall **)(__int64))(*(_QWORD *)(v141 + 8) + 48i64))(v141);
      }
      *(_QWORD *)(v141 + 80) = 0i64;
      if ( i )
      {
        if ( (int)*i >= 0 )
        {
          v146 = *i - 1;
          *i = v146;
          if ( v146 )
          {
            v147 = *v53;
            if ( (int)*v53 >= 0 )
              goto LABEL_277;
          }
          else
          {
            (*(void (__fastcall **)(__int64 *))(i[1] + 48))(i);
            v147 = *v53;
            if ( (int)*v53 >= 0 )
            {
LABEL_277:
              v148 = v147 - 1;
              *v53 = v148;
              if ( !v148 )
                (*(void (__fastcall **)(__int64 *))(v53[1] + 48))(v53);
              if ( Py_TrueStruct != -1 )
                ++Py_TrueStruct;
              v32 = totlen2;
              instrlen = (__int64)v139;
              v31 = (int *)Py_TrueStruct;   //验证成功时返回的布尔值True
              goto LABEL_49;
            }
          }
          v164 = (int *)Py_TrueStruct;
          v165 = Py_TrueStruct + 1;
          if ( Py_TrueStruct == -1 )
          {
LABEL_312:
            v32 = totlen2;
            instrlen = (__int64)v139;
            v31 = v164;
            goto LABEL_50;
          }
LABEL_311:
          *v164 = v165;
          goto LABEL_312;
        }
        v147 = *v53;
        if ( (int)*v53 >= 0 )
          goto LABEL_277;
        v164 = (int *)Py_TrueStruct;
        v165 = Py_TrueStruct + 1;
        if ( Py_TrueStruct != -1 )
          goto LABEL_311;
      }
      else
      {
        v147 = *v53;
        if ( (int)*v53 >= 0 )
          goto LABEL_277;
        v164 = (int *)Py_TrueStruct;
        if ( Py_TrueStruct != -1 )
          ++Py_TrueStruct;
      }
      v32 = totlen2;
      v31 = v164;
      goto LABEL_53;
    }
    if ( i )
    {
      if ( (int)*i < 0 )
      {
        v58 = *_iternext + 1;
        if ( *(_DWORD *)instrlen == -1 )
          goto LABEL_93;
      }
      else
      {
        v57 = *i - 1;
        *i = v57;
        if ( v57 )
        {
          if ( *(_DWORD *)instrlen != -1 )
          {
            ++*(_DWORD *)instrlen;
            v57 = *i;
          }
          goto LABEL_90;
        }
        (*(void (__fastcall **)(__int64 *))(i[1] + 48))(i);
        v58 = *(_DWORD *)instrlen + 1;
        if ( *(_DWORD *)instrlen == -1 )
        {
          v57 = *i;
          goto LABEL_90;
        }
      }
      *(_DWORD *)instrlen = v58;
      v57 = *i;
LABEL_90:
      if ( (int)v57 >= 0 )
      {
        v59 = v57 - 1;
        *i = v59;
        if ( !v59 )
          (*(void (__fastcall **)(__int64 *))(i[1] + 48))(i);
      }
      goto LABEL_93;
    }
    v79 = *_iternext + 1;
    if ( v79 )
      *(_DWORD *)instrlen = v79;
LABEL_93:
    if ( !total_len_source )
    {
      v149 = PyUnicode_FromFormat(
               "cannot access local variable '%U' where it is not associated with a value",
               qword_7FFC5E7C8150);
      v176 = (_QWORD *)verify_func((__int64)a1, PyExc_UnboundLocalError, (_DWORD *)v149);
      if ( (int)*(_QWORD *)v149 >= 0 )
      {
        v150 = *(_QWORD *)v149 - 1i64;
        *(_QWORD *)v149 = v150;
        if ( !v150 )
          (*(void (__fastcall **)(__int64))(*(_QWORD *)(v149 + 8) + 48i64))(v149);
      }
      sub_7FFC5E798040(a1[31], v176);
      v94 = 52i64;
      goto LABEL_161;
    }
    *(_DWORD *)(v3 + 40) = 52;
    v32 = (_QWORD *)invoke(
                      (__int64)a1,
                      (__int64)total_len_source,
                      *(__int64 *)&get_char_at_position,    //获取字符串指定位置的字符
                      (_DWORD *)instrlen);
    if ( !v32 )
    {
      v151 = (_QWORD *)a1[12];
      v94 = 52i64;
      a1[12] = 0i64;
      v176 = v151;
      goto LABEL_161;
    }
    if ( totlen2 )
    {
      if ( (int)*totlen2 >= 0 )
      {
        v60 = *totlen2 - 1i64;
        *totlen2 = v60;
        if ( !v60 )
          (*(void (__fastcall **)(_QWORD *))(totlen2[1] + 48i64))(totlen2);
      }
    }
    if ( v32 == (_QWORD *)Py_NoneStruct[0] )
      break;
    v61 = sub_7FFC5E762300((__int64)a1, (__int64)instr, instrlen);
    v62 = (_QWORD *)v61;
    if ( !v61 )
      goto LABEL_257;
    v63 = *(_QWORD *)(v61 + 8);
    if ( v32 == (_QWORD *)v61 )
    {
      if ( v63 == v181 )
      {
        if ( (int)*v32 >= 0 )
        {
          v105 = *v32 - 1i64;
          *v32 = v105;
          if ( !v105 )
            (*(void (__fastcall **)(_QWORD *))(v181 + 48))(v32);
        }
        goto LABEL_140;
      }
      if ( v63 == PyList_Type || v63 == PyTuple_Type )
      {
        if ( (int)*v32 >= 0 )
        {
          v108 = *v32 - 1i64;
          *v32 = v108;
          if ( !v108 )
            (*(void (__fastcall **)(_QWORD *))(v63 + 48))(v32);
        }
        goto LABEL_140;
      }
LABEL_131:
      v80 = *(__int64 (__fastcall **)(_QWORD *, _QWORD *, __int64))(v63 + 200);
      if ( !v80 )
        goto LABEL_179;
      goto LABEL_132;
    }
    v14 = v32[1];
    if ( v14 == v63 )
      goto LABEL_131;
    v64 = *(_QWORD *)(v14 + 344);
    if ( v64 )
    {
      v65 = *(_QWORD *)(v64 + 16);
      if ( v65 <= 0 )
        goto LABEL_176;
      v66 = 0i64;
      while ( *(_QWORD *)(v64 + 8 * v66 + 24) != v63 )
      {
        if ( v65 == ++v66 )
          goto LABEL_176;
      }
    }
    else
    {
      v138 = v32[1];
      while ( 1 )
      {
        v138 = *(_QWORD *)(v138 + 256);
        if ( !v138 )
          break;
        if ( v138 == v63 )
          goto LABEL_108;
      }
      if ( v63 != PyBaseObject_Type[0] )
      {
LABEL_176:
        v80 = *(__int64 (__fastcall **)(_QWORD *, _QWORD *, __int64))(v63 + 200);
        v63 = v32[1];
        if ( !v80 )
        {
LABEL_177:
          v103 = *(__int64 (__fastcall **)(_QWORD *, _QWORD *, __int64, __int64))(v14 + 200);
          if ( !v103 )
            goto LABEL_179;
          v82 = (_QWORD *)v103(v32, v62, 3i64, v63);
          if ( v82 == (_QWORD *)Py_NotImplementedStruct[0] )
            goto LABEL_179;
LABEL_133:
          if ( v82 )
          {
            v73 = get_bool_val((__int64)v82) != 0;
            if ( (int)*v82 >= 0 )
            {
              v83 = *v82 - 1i64;
              *v82 = v83;
              if ( !v83 )
              {
                v174 = v73;
                v75 = v82[1];
                v76 = v82;
                goto LABEL_137;
              }
            }
            goto LABEL_146;
          }
LABEL_254:
          if ( (int)*v62 >= 0 )
          {
            v135 = *v62 - 1i64;
            *v62 = v135;
            if ( !v135 )
              (*(void (__fastcall **)(_QWORD *, __int64, __int64, __int64))(v62[1] + 48i64))(v62, v69, v14, v63);
          }
LABEL_257:
          v136 = (_QWORD *)a1[12];
          totlen2 = v32;
          a1[12] = 0i64;
          v94 = 53i64;
          v176 = v136;
LABEL_161:
          if ( (int)*(_QWORD *)instrlen >= 0 )
          {
            v95 = *(_QWORD *)instrlen - 1i64;
            *(_QWORD *)instrlen = v95;
            if ( !v95 )
            {
              v182 = v94;
              (*(void (__fastcall **)(__int64))(*(_QWORD *)(instrlen + 8) + 48i64))(instrlen);
              v94 = v182;
            }
          }
          goto LABEL_164;
        }
LABEL_132:
        v175 = v63;
        v81 = v80(v62, v32, 3i64);
        v63 = v175;
        v82 = (_QWORD *)v81;
        if ( v81 != Py_NotImplementedStruct[0] )
          goto LABEL_133;
        v14 = v175;
        goto LABEL_177;
      }
    }
LABEL_108:
    v67 = *(__int64 (__fastcall **)(_QWORD *, _QWORD *, __int64))(v14 + 200);
    if ( !v67 )
    {
      v80 = *(__int64 (__fastcall **)(_QWORD *, _QWORD *, __int64))(v63 + 200);
      v63 = v32[1];
      if ( !v80 )
        goto LABEL_179;
      goto LABEL_132;
    }
    v172 = v63;
    v68 = v67(v32, v62, 3i64);
    v63 = v172;
    v70 = v68;
    if ( v68 != Py_NotImplementedStruct[0] )
    {
      if ( !v68 )
        goto LABEL_254;
      v173 = (_QWORD *)v68;
      v71 = get_bool_val(v68);
      v72 = v173;
      v73 = v71 != 0;
      if ( (int)*v173 >= 0 )
      {
        v74 = *v173 - 1i64;
        *v173 = v74;
        if ( !v74 )
        {
          v174 = v73;
          v75 = v72[1];
          v76 = v72;
LABEL_137:
          (*(void (__fastcall **)(_QWORD *))(v75 + 48))(v76);
          v73 = v174;
          if ( (int)*v62 >= 0 )
          {
            v84 = *v62 - 1i64;
            *v62 = v84;
            if ( !v84 )
              goto LABEL_181;
          }
          goto LABEL_139;
        }
      }
LABEL_146:
      if ( (int)*v62 >= 0 )
      {
        v87 = *v62 - 1i64;
        *v62 = v87;
        if ( !v87 )
          goto LABEL_181;
      }
      goto LABEL_139;
    }
    v137 = *(__int64 (__fastcall **)(_QWORD *, _QWORD *, __int64, __int64))(v172 + 200);
    if ( v137 )
    {
      v179 = (_QWORD *)v70;
      v82 = (_QWORD *)v137(v62, v32, 3i64, v63);
      if ( v82 != v179 )
        goto LABEL_133;
    }
LABEL_179:
    v73 = v32 != v62;
    if ( (int)*v62 >= 0 )
    {
      v104 = *v62 - 1i64;
      *v62 = v104;
      if ( !v104 )
      {
LABEL_181:
        v178 = v73;
        (*(void (__fastcall **)(_QWORD *))(v62[1] + 48i64))(v62);
        v73 = v178;
      }
    }
LABEL_139:
    if ( v73 )
      break;
LABEL_140:
    v85 = a1[2];
    if ( (*(_DWORD *)(*(_QWORD *)(v85 + 96) + 1236i64) || *(_DWORD *)(v85 + 416)) && (int)Py_MakePendingCalls() < 0 )
    {
      v93 = (_QWORD *)a1[12];
      if ( v93 )
        goto LABEL_159;
    }
    if ( *(_DWORD *)(v85 + 372) )
    {
      PyEval_SaveThread();
      PyEval_AcquireThread(a1);
      v86 = a1[16];
      if ( !v86 )
        goto LABEL_145;
LABEL_150:
      v88 = *(_QWORD *)(v86 + 8);
      v89 = (__int64 *)a1[12];
      a1[16] = 0i64;
      v90 = *(_DWORD *)(v88 + 168);
      if ( (v90 & 0x40000000) == 0 )
      {
        if ( v90 < 0 && (*(_BYTE *)(v86 + 171) & 0x40) != 0 )
        {
          v91 = sub_7FFC5E783A00((__int64)a1, v86);
          v176 = v91;
          if ( v91 )
          {
            v14 = v91[1];
            if ( (*(_BYTE *)(v14 + 171) & 0x40) != 0 )
              goto LABEL_155;
            v158 = PyUnicode_FromFormat("calling %R should have returned an instance of BaseException, not %R", v86);
            v159 = verify_func((__int64)a1, PyExc_TypeError, (_DWORD *)v158);
            if ( (int)*(_QWORD *)v158 >= 0 )
            {
              v160 = *(_QWORD *)v158 - 1i64;
              *(_QWORD *)v158 = v160;
              if ( !v160 )
              {
                v184 = v159;
                (*(void (__fastcall **)(__int64))(*(_QWORD *)(v158 + 8) + 48i64))(v158);
                v159 = v184;
              }
            }
            v161 = (__int64 *)a1[12];
            a1[12] = v159;
            if ( v161 )
            {
              if ( (int)*v161 >= 0 )
              {
                v162 = *v161 - 1;
                *v161 = v162;
                if ( !v162 )
                  (*(void (**)(void))(v161[1] + 48))();
              }
            }
            if ( (int)*v176 >= 0 )
            {
              v163 = *v176 - 1i64;
              *v176 = v163;
              if ( !v163 )
                (*(void (__fastcall **)(_QWORD *))(v176[1] + 48i64))(v176);
            }
          }
        }
        else
        {
          v152 = PyUnicode_FromFormat("exceptions must derive from BaseException", *(_QWORD *)(v88 + 24));
          v153 = verify_func((__int64)a1, PyExc_TypeError, (_DWORD *)v152);
          if ( (int)*(_QWORD *)v152 >= 0 )
          {
            v154 = *(_QWORD *)v152 - 1i64;
            *(_QWORD *)v152 = v154;
            if ( !v154 )
            {
              v180 = v153;
              (*(void (__fastcall **)(__int64))(*(_QWORD *)(v152 + 8) + 48i64))(v152);
              v153 = v180;
            }
          }
          v155 = (__int64 *)a1[12];
          a1[12] = v153;
          if ( v155 )
          {
            if ( (int)*v155 >= 0 )
            {
              v156 = *v155 - 1;
              *v155 = v156;
              if ( !v156 )
                (*(void (**)(void))(v155[1] + 48))();
            }
          }
        }
        v176 = 0i64;
        goto LABEL_155;
      }
      if ( *(_DWORD *)v86 != -1 )
        ++*(_DWORD *)v86;
      v176 = (_QWORD *)v86;
LABEL_155:
      a1[12] = v176;
      if ( v89 )
      {
        if ( (int)*v89 >= 0 )
        {
          v92 = *v89 - 1;
          *v89 = v92;
          if ( !v92 )
          {
            (*(void (__fastcall **)(__int64 *))(v89[1] + 48))(v89);
            v93 = (_QWORD *)a1[12];
LABEL_159:
            v176 = v93;
          }
        }
      }
      a1[12] = 0i64;
      totlen2 = v32;
      v94 = 51i64;
      goto LABEL_161;
    }
    v86 = a1[16];
    if ( v86 )
      goto LABEL_150;
LABEL_145:
    totlen2 = v32;
  }
  v31 = (int *)Py_FalseStruct;
  if ( Py_FalseStruct != -1 )
    ++Py_FalseStruct;
  if ( (int)*(_QWORD *)instrlen >= 0 )
  {
    v77 = *(_QWORD *)instrlen - 1i64;
    *(_QWORD *)instrlen = v77;
    if ( !v77 )
      (*(void (__fastcall **)(__int64))(*(_QWORD *)(instrlen + 8) + 48i64))(instrlen);
  }
  if ( (int)*v53 >= 0 )
  {
    v78 = *v53 - 1;
    *v53 = v78;
    if ( !v78 )
      (*(void (__fastcall **)(__int64 *))(v53[1] + 48))(v53);
  }
criticalFailure:
  v33 = a1[7];
  v34 = *(_QWORD *)(*(_QWORD *)v33 + 48i64);
  v35 = *(__int64 **)(v34 + 16);
  *(_QWORD *)v33 = *(_QWORD *)(*(_QWORD *)v33 + 8i64);
  *(_DWORD *)(v34 + 64) = -1;
  if ( v35 )
  {
    v36 = *v35;
    *(_QWORD *)(v34 + 16) = 0i64;
    if ( (int)v36 >= 0 )
    {
      v37 = v36 - 1;
      *v35 = v37;
      if ( !v37 )
        (*(void (**)(void))(v35[1] + 48))();
    }
  }
  if ( (int)*(_QWORD *)v34 >= 0 )
  {
    v38 = *(_QWORD *)v34 - 1i64;
    *(_QWORD *)v34 = v38;
    if ( !v38 )
      (*(void (__fastcall **)(__int64))(*(_QWORD *)(v34 + 8) + 48i64))(v34);
  }
  *(_QWORD *)(v34 + 80) = 0i64;
LABEL_49:
  if ( instrlen )
  {
LABEL_50:
    if ( (int)*(_QWORD *)instrlen >= 0 )
    {
      v39 = *(_QWORD *)instrlen - 1i64;
      *(_QWORD *)instrlen = v39;
      if ( !v39 )
        (*(void (__fastcall **)(__int64))(*(_QWORD *)(instrlen + 8) + 48i64))(instrlen);
    }
  }
LABEL_53:
  if ( v32 )
  {
    if ( (int)*v32 >= 0 )
    {
      v40 = *v32 - 1i64;
      *v32 = v40;
      if ( !v40 )
        (*(void (__fastcall **)(_QWORD *))(v32[1] + 48i64))(v32);
    }
  }
  if ( (int)*total_len_source >= 0 )
  {
    v41 = *total_len_source - 1;
    *total_len_source = v41;
    if ( !v41 )
      (*(void (__fastcall **)(__int64 *))(total_len_source[1] + 48))(total_len_source);
  }
  if ( (int)*instr >= 0 )
  {
    v42 = *instr - 1;
    *instr = v42;
    if ( !v42 )
      (*(void (__fastcall **)(__int64 *))(instr[1] + 48))(instr);
  }
  return v31;
}

第六题 番外

这道题我本以为和第二题一样也可以通过通关游戏获得flag,看到easy模式没有flag,就用hard模式玩了几局,输了几局之后就转而直接动手了。用ida分析提示可能有壳,但用stud_pe查壳又没查出结果。

然后看见目录下有lua5.1的库,感觉这个游戏的源代码可能是现成的,用winrar打开果然看到lua文件,能轻松找到生成flag的函数:

local function getWinMessage()
    local content = nil

    if love.filesystem.getInfo("assets/flag.dat") then
        content = love.filesystem.read("assets/flag.dat")
    end

    if not content or currentDifficulty ~= "hard" then
        return "You WIN!"
    end

    local key = "52pojie"
    local keyLen = #key
    local result = {}
    local bit = require("bit")

    for i = 1, #content do
        local b = string.byte(content, i)
        local k = string.byte(key, ((i - 1) % keyLen) + 1)
        table.insert(result, string.char(bit.bxor(b, k)))
    end

    return table.concat(result)
end

后面把这个函数改写后拿到单独的lua引擎上执行时,发现lua5.1不支持位操作,也没有bit库,就从网上找了一个实现位操作的代码补上。最终代码:

function Xor(num1,num2)
local tmp1 = num1
local tmp2 = num2
local str = ""
repeat
    local s1 = tmp1 % 2
    local s2 = tmp2 % 2
    if s1 == s2 then
        str = "0"..str
    else
        str = "1"..str
    end
    tmp1 = math.modf(tmp1/2)
    tmp2 = math.modf(tmp2/2)
until(tmp1 == 0 and tmp2 == 0)
return tonumber(str,2)
end

local content = nil

local f,err=io.open("flag.dat",'rb')
if err then print(err) end
local content=f:read("*a")
print(#content)

local key = "52pojie"
local keyLen = #key
local result = {}
-- local bit = require("bit")

for i = 1, string.len(content ) do
    local b = string.byte(content, i)
    local k = string.byte(key, ((i - 1) % keyLen) + 1)
    print(b)
    table.insert(result, string.char(Xor(b, k)))
    -- table.insert(result, b k)
end

print( table.concat(result))

第七题 HEX ME

看题干隐约感觉到,这道题的flag由检查正误和主动显示转变为了加密密码,指望调程序输出flag恐怕不行了。

  • 拿到题目我最先打开的是任务说明.html,但发现文件中的注释“\<!-- 这不是 Web 题,没有额外的彩蛋、线索哦 --\>”后就没再进一步查看了。
  • 然后直接用ida打开CM1,提示可能存在压缩,用stud_pe查壳没查出结果(可能因为工具太老旧)
  • 最后像第六题一样强行用winrar打开,观察列出的文件名才发现是UPX的壳……

脱壳后进ida,因为我的ida是论坛里的,整合有一些插件,就用findcrypt进行搜索,搜索到一个crc相关参数和一个aes相关参数。

难道是整合了crc和aes的加密算法?这能破解吗?我开始怀疑这道题到底能不能解开,但是回过头一看,这时候已经有很多大佬把这道题完成了,甚至很多是在几分钟之内完成的。我想那说明应该还是有解,于是硬着头皮做下去。

  • 先看字符串,有很多flag的提示文字,跟着交叉引用找到主函数

  • 然后从开始解密的逻辑部分开始进行分析(这里指do_decode,名字是我自己命名的),根据findcrypt的结果,给各函数大致取了个名字

起先,我真以为这里用到了crc和aes算法,将crc的部分复制给ai,ai分析认为确实是crc,致使我误认为不能靠分析算法代码得到flag。我开始仔细阅读html里的那个故事,html的内容和任务的描述是一样的,唯一的区别是多了一行暴力枚举不可取的提示。    起初我以为这个提示的意思是flag是以隐写形式隐藏的,然后我开始搜索题目文件每个细节,检查CM1资源中的png图片有无隐写,用binwalk扫描exe……忙活了一天之后一无所获。
最后发现不是这样。。

先看解密函数do_decode:

__int64 __fastcall do_decode(char *Str, FILE *Stream, FILE *a3)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  crc_init((__int64 *)cipherblk);
  crc_fill((__int64 *)cipherblk, "52pojie_2026_", 14i64);
  v6 = strlen(Str);
  crc_fill((__int64 *)cipherblk, Str, v6);
  crc_val = crc_final(cipherblk);
  fread(Buffer, 16ui64, 1ui64, Stream);
  v8 = prepare_aes_tuple((confuration *)cipherblk, crc_val, (encryptedFile *)Buffer);
  result = 1i64;
  if ( v8 )
  {
    fseek(Stream, 0, 2);    //指针移至文件结尾
    v10 = ftell(Stream);
    result = 2i64;
    v11 = v10 - 0x10;                           // v11 = v10 - 0x10 把文件当前大小(v10,由 ftell 得到)减去 16(0x10)。
    if ( (v10 & 7) == 0 )
    {
      fseek(Stream, 16, 0);
      datagrm = (char *)malloc(v10 - 0x10);
      datagrm2 = datagrm;
      if ( datagrm )
      {
        fread(datagrm, 1ui64, v10 - 0x10, Stream);
        decode((confuration *)cipherblk, datagrm2, v10 - 0x10);
        if ( (unsigned __int8)checksum_verify((confuration *)cipherblk) )
        {
          v14 = (unsigned __int8)datagrm2[v10 - 17];
          if ( v11 < v14 )
          {
            free(datagrm2);
            return 5i64;
          }
          else
          {
            fwrite(datagrm2, 1ui64, v11 - v14, a3);
            free(datagrm2);
            return 0i64;
          }
        }
        else
        {
          free(datagrm2);
          return 4i64;
        }
      }
      else
      {
        return 3i64;
      }
    }
  }
  return result;
}

代码中将52pojie_2026_和输入的密钥flag{...}组合起来计算crc,然后传入prepare_aes_tuple中,调用decode解密,最后checksum_verify通过比较校验值是否相等来判断密码是否正确。同时读取已加密的文件,回避了文件开头的16个字节。

再看prepare_aes_tuple和子函数的代码:

__int64 __fastcall prepare_aes_tuple(confuration *conf, __int64 crc, encryptedFile *fileBuffer)
{
  __int64 result; // rax
  __int32 checksum; // eax

  result = 0i64;
  if ( fileBuffer )
  {
    if ( fileBuffer->cm26_header == '62MC' )
    {
      getaeslong(conf, crc, (__int64)&fileBuffer->iv_or_salt2);
      checksum = fileBuffer->checksum;
      conf->computed_checksum = -1;
      conf->correct_checksum = checksum;
      return 1i64;
    }
  }
  return result;
}
void *__fastcall getaeslong(confuration *a1, __int64 crc, __int64 iv)
{
  __int64 v3; // rax

// 不是。这个函数没有执行 AES 加密运算;它的操作是:
// 
// 将 a2(你说是 CRC 结果)写入 a1[0](8 字节)。
// 将从 a3 开始的 16 字节逐字节拷贝到 a1+16 偏移处(即把 16 字节的块/IV/密钥材料复制到结构中)。
// 最后调用 memcpy 将名为 RijnDael_AES_LONG_7FF69282A270 的 0x100(256)字节数据拷贝到 a1+4*8(即把一个 256 字节的表/常量块复制进结构),并返回该 memcpy 的返回值。

  v3 = 0i64;
  *(_QWORD *)a1->crc = crc;
  do
  {
    a1->iv[v3] = *(_BYTE *)(iv + v3);
    ++v3;
  }
  while ( v3 != 16 );   // 16个字节
  return memcpy(a1->tbl, &RijnDael_AES_LONG_7FF65C3FA270, sizeof(a1->tbl));
}

既然ai说不是aes,那我也就继续放心分析了

为了分析方便,定义了结构体encryptedFile和configuration(代码里打错了。。),构成根据这里的代码逻辑而得出:

struct encryptedFile
{
  __int32 cm26_header;
  __int32 checksum;
  __int64 iv_or_salt2; //盐?
  byte data[]; //加密数据
};
struct confuration
{
  byte crc[16];
  byte iv[16]; //盐?
  byte tbl[256]; //RijnDael_AES_LONG_7FF65C3FA270
  __int32 computed_checksum;
  __int32 correct_checksum;
};

(文件中的crc只有32位、iv只有64位,但是configuration中留了16字节)

接下来是核心解密函数decode,这里的代码有些混淆的部分,我在注释中将逻辑理了出来

__int64 __fastcall decode(confuration *conf, char *data, __int64 dataLen)
{
  char *data_minus_24627; // rbx
  char *data_eof_minus_24627; // rdi
  __int32 *p_computed_checksum; // r12
  __int64 result; // rax

  data_minus_24627 = data - 24627;
  // 这个表达式指代文件头部
  data_eof_minus_24627 = &data[(dataLen & 0xFFFFFFFFFFFFFFF8ui64) - 24627];
  // dataLen是固定的,=0x160=0b0001_0110_0000
  // 又因为0xFFFFFFFFFFFFFFF8=0b1111_..._1111_1000
  // 因此 dataLen&0xFFFFFFFFFFFFFFF8也是固定的,=0x160
  // 这个表达式指代文件尾部
  if ( data - 24627 < data_eof_minus_24627 )
  {
    p_computed_checksum = &conf->computed_checksum;
    do
    {
      data_minus_24627 += 8;
      xor_by_iv(conf->crc, (byte *)data_minus_24627 + 24619);
      result = get_checksum(p_computed_checksum, (unsigned __int64)(data_minus_24627 + 24619), 8i64);
    }
    while ( data_minus_24627 < data_eof_minus_24627 );
      // 循环体代码翻译:
      // i=-8
      // do {
      // i+=8
      // ...(...,&data[i])
      // result = ...(...,&data[i],...)
      // } while(i<data_len)
  }
  return result;
}

从这里的循环体可以看出,数据应该是以8个字节为单位进行解密的

最后是xor_by_iv,它将先前计算的crc进行移位,再查找RijnDael_AES_LONG_7FF65C3FA270的值并做或运算,最后通过计算crc[i]^iv[i]^data_3解密数据。

byte __fastcall xor_by_iv(byte *crc_and_ivNo2, byte *data)
{
  int countDown; // r8d
  __int64 data_1; // r11
  __int64 iv_1; // rax
  byte *iv_minus_21569; // rcx
  unsigned __int64 iv_shfted; // rax
  byte *data_plus_37688; // rdx
  char data_3; // al
  byte result; // al

  countDown = 8;
  data_1 = *(_QWORD *)data;
  iv_1 = *(_QWORD *)crc_and_ivNo2;
  iv_minus_21569 = crc_and_ivNo2 - 21569;
  iv_shfted = __ROL8__(iv_1, 3);
  do
  {
    iv_shfted = (iv_shfted << 8) | crc_and_ivNo2[(HIBYTE(iv_shfted) | 0x221300) - 2233056];
    // HIBYTE(iv_shfted) 一定小于 0xff
    // 所以(HIBYTE(iv_shfted) | 0x221300) - 0x2212E0
    // =HIBYTE(iv_shfted)+0x221300-0x2212E0
    // =HIBYTE(iv_shfted)+0x20
    // =HIBYTE(iv_shfted)+32d
    // =...->tbl[HIBYTE(iv_shfted)]
    // 
    --countDown;
  }
  while ( countDown );
  *(_QWORD *)crc_and_ivNo2 = iv_shfted;
  data_plus_37688 = data + 37688;
  // i=0
  do
  {
    data_3 = *(data_plus_37688 - 37688);
    // data_3=data[i]
    ++iv_minus_21569;
    ++data_plus_37688;
    // ++i
    result = iv_minus_21569[21568] ^ iv_minus_21569[21584] ^ data_3;
    // =crc_and_ivNo2[i]^crc_and_ivNo2[16+i]^data_3
    // =crc[i]^iv[i]^data_3
    *(data_plus_37688 - 37689) = result;
    // data[i]=result
  }
  while ( iv_minus_21569 != crc_and_ivNo2 - 21561 );
  // while(i!=8) // only 8 bytes used
  *((_QWORD *)crc_and_ivNo2 + 2) = data_1;
  return result;
}

仔细分析后发现,这个函数中唯一的解密运算就是那行result = iv_minus_21569[21568] ^ iv_minus_21569[21584] ^ data_3;看来确实不是aes,不然不会这么简单。又想到题目中反复强调被加密的文件是一个png图片,于是开始考虑用png文件已知的文件头数据来破解这个算法。起先我以为png顶多只有头4个字节是固定的,一搜,居然有8个字节。这时我才焕然大悟,第一轮解密的明文是已知的,也就是result=crc[i]^iv[i]^data_3中result、iv、data_3都是已知的。于是用python脚本计算第一轮正确的crc值:

import argparse

ap = argparse.ArgumentParser()
ap.add_argument("data",  help="encrypted data file")

args = ap.parse_args()

with open(args.data,'rb') as d:
    file_hdr_and_checksum=d.read(8)
    file_iv=d.read(8)
    enc_data=d.read()

PNG=0xA1A0A0D474E5089
# 取 enc_data 的前 8 字节作为整数(与 PNG 头大小匹配)
ENC = int.from_bytes(enc_data[0:8], "little")
IV = int.from_bytes(file_iv[0:8], "little")
print("{0:016X}".format(PNG^ENC^IV))

算出来结果为95CC2E27B526E487。然后在*(_QWORD *)crc_and_ivNo2 = iv_shfted;上打上断点,断点触发后修改rax的值为前面计算出的正确的crc值,禁用断点再运行,解密成功

之前搞错方向时找到了一个检查png图片隐写的工具StegSolve,现在用它打开解密的图片,发现其中包含的tEXt区块就是flag

第八题 哈吉米

没解出来。

我用了多台安卓设备来尝试,都无法运行这道题的apk,安卓10、9、8、6都有尝试,即使是干净的安卓设备也是如此。要么安装后运行时闪退要么安装包解析失败,我也并未通过adb连接来安装。不知道是题目本身如此还是安卓系统版本太低导致的。总之没法像第二题那样当普通游戏来玩了

然后用jeb分析。在dex中找到了与解析flag有关的字符串,相关的代码疑似用的是kotlin的协程来实现的,但是主要的验证逻辑都指向了libhajimi.so里。

用ida分析libhajimi.so,一开始选择的是x86架构的so,结果ida识别不出入口函数JNI_OnLoad的JNIEnv结构体,原因是加载不出android ARM的类型库,换成arm64才正常。再后面发现有些代码是经过混淆和加密的,而且有个函数名称里就包含有"decrypt",可能需要动态调试才能理清逻辑。但是我的设备又运行不了,最后放弃了。

第九题 web语音验证码

之前没接触过wasm,所以这题是连蒙带猜的。

首先分析assets里js代码的初始化部分

………………
    function gen(uid, voice) {
        const ptr0 = passStringToWasm0(voice, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
        const len0 = WASM_VECTOR_LEN;
        const ret = wasm.gen(uid, ptr0, len0);
        if (ret[2]) {
            throw takeFromExternrefTable0(ret[1]);
        }
        return takeFromExternrefTable0(ret[0]);
    }
    __exports.gen = gen;
………………
async function checkCode(code, expectedHash) {
    ………………

    for (let i = 0; i < 0x2026; i++) {
        current = await crypto.subtle.digest('SHA-256', current)
    }

    ………………
    return hashHex === expectedHash
}

async function init() {
        ………………

            try {
                const challenge = wasm_bindgen.gen(uid, voice)
                currentHash = challenge.h
        ………………
        document.getElementById('verifyBtn').addEventListener('click', async () => {
            …………

            const code = input.value.slice(5, -1)
            const isValid = await checkCode(code, currentHash)

            if (isValid) {
                msgArea.textContent = "验证码正确,快去网站提交 flag 吧!"
                msgArea.className = "success-text"
            } else {
                input.focus()
                msgArea.textContent = "验证码与语音不匹配。"
                …………
}
init()

可以看到验证码通过wasm_bindgen.gen方法生成,而且还需要输入自己的uid,检验是通过对输入内容掉去除flag{}后的0x2026次sha-256哈希与gen方法返回的challenge对象的h的值比较实现的。而gen方法来自开头闭包加载的wasm中。

所以,这道题需要逆向WebAssembly。我之前从来没有逆向过wasm,去网上搜索逆向方法,搜到的说法有f12、wabt、ida、jeb。先打开html,在f12的sources中把wasm导出出来

然后开始摸索分析wasm的方法。

  • f12可以直接调试,但是只能看见汇编码,而且诸如交叉引用之类浏览代码的功能也一点没有。

  • 然后我想还是用jeb或ida会方便点,但是比较低的版本识别不了wasm,所以就又下载来了前几个月刚出的新版来尝试

    • jeb5.36能加载出wasm的汇编码,但是反汇编直接失败,而且交叉引用貌似也用不了,因为是demo版吗?
    • ida 9.3列出了wasm加载器,但是一加载就报错param type不正确,原因不明。
  • 最后老实用网上大佬前些年介绍的传统方法:先用wabt2c转换为c,再编译为o。也就是论坛里发布的wasm一键转c这个工具。分析完后再用f12动态调试吧。。


先看一下f12给出的汇编代码的结构:

  (func $gen (;50;) (export "gen") (param $var0 i32) (param $var1 i32) (param $var2 i32) (result i32) (result i32) (result i32)
    (local $var3 i32)
    ……
    (local $var31 i64)
    (local $var32 externref)
    global.get $global0
    i32.const 16
    i32.sub
    local.tee $var15
    global.set $global0
    local.get $var1
    local.set $var17
    local.get $var2
    local.set $var16
    i32.const 0
    local.set $var1
    global.get $global0
    i32.const 640
    i32.sub
    local.tee $var3
    global.set $global0
    local.get $var3
    i32.const 96
    i32.add
    i32.const 0
    i32.store8
    local.get $var3
    i32.const 88
    i32.add
    i64.const 0
    i64.store
    local.get $var3
    i64.const 0
    i64.store offset=80
    local.get $var3
    i32.const 80
    i32.add
    i32.const 17
    call $wbg.__wbg_getRandomValues_1c61fac11405ffdc
    i32.const 5412468
    i32.load
    local.set $var2
    i32.const 5412472
    i32.load
    local.set $var4
    i32.const 5412468
    i64.const 0
    i64.store align=4

还好,输入输出的变量名至少给出来了。分析js代码可知输入的是uid, ptr0, len0输出的分别是处理结果、异常、是否出现异常。

现场学习一下wasm的汇编,其指令能读写三个内存区域:

  1. 线性内存(Linear Memory)  

    • 说明: WebAssembly 的线性内存是一块连续的、可增长的字节数组,模块通过内存索引访问。  
    • 典型指令: load/store 系列(i32.load、i64.store、f32.load 等)。  
    • 访问方式: 使用基址(来自栈或局部/全局变量)加上立即数偏移和对齐参数进行读写。  
    • 用途示例: 存放堆数据、数组、字符串、与 JS 共享的大块缓冲区。
  2. 表(Table)/函数表(Function Table)  

    • 说明: 表主要用于间接函数调用,存储函数引用(或在引用类型启用时存储引用类型值);也可在某些扩展中存放引用对象。  
    • 典型指令: table.get、table.set、call_indirect(或在新版规范中 call_ref/等)。  
    • 访问方式: 通过索引读写表项,索引越界会触发运行时错误(或陷阱)。  
    • 用途示例: 支持动态/多态调用、实现语言运行时的虚函数表。
  3. 全局变量(Globals)与本地变量(Locals)/栈(Stack)  

    • 说明: 虽然本地变量和操作数栈不是“内存”区域的传统意义,但在汇编层面它们是指令直接读写的存储位置;全局变量是模块级的可导出/导入存储单元。  
    • 典型指令: get_local / set_local(在二进制/文本新语法中为 local.get/local.set)、global.get、global.set。  
    • 访问方式: 通过变量索引读写;全局可分为可变(var)和不可变(const)。  
    • 用途示例: 存放函数局部状态、中间计算值、模块范围配置或状态。

也就是说,flag可能藏在这三个地方的其中一个。

  • 起先我从表入手,直接在f12的汇编中搜索table下断点,调试找到了给h这个键赋值的部分,但是这时候哈希已经计算好了。。倒着找回去代码又太杂。。

  • 然后分析全局和本地变量。全局变量似乎只有一个g0,值起初为一个固定值而程序执行期间时不时会进行加减法。。

    • 然后试图从gen的第一个输入变量uid正向分析,结果发现开头几行gen的汇编代码量巨大,在f12中难以翻阅。。线索再次断掉。。

(虽然线索断了,但是先前学的内存区域的知识后期会用到)


最后决定先用ida进行整体上的分析。用ida 9.3分析o,与汇编相比似乎更难分析了,gen函数大到f5拒绝分析,说明gen这个函数一定把很多不同的逻辑都塞在里面了。强行修改f5插件的函数大小限制后,gen的参数个数和类型甚至都与f12的汇编对不上了。

但是好歹交叉引用功能能用了,就用graph功能将函数关系直接图形化

可见,排除掉wasm内置函数的情况下,gen引用的函数有孤立的也有相互引用的。从js的代码可以看出这个wasm的基本功能是提供flag、flag哈希和音频,我就猜测引用复杂的那部分大概与哈希和音频有关,而孤立的那些函数可能与flag有关(我知道这么分析很勉强,但是没别的办法了)

孤立函数中,func9与gen一样大到f5拒绝分析,所以我就从func9入手再次在f12中进行动态调试


接下来就是纯纯胡猜了。简单看了看,先从var0指向的线性内存地址读取了一系列数据,再进入一个大循环然后出现了一系列异或移位?尾部又是写入了一系列数据,同时也没有引用其他函数,应该是什么加解密相关的函数。

shift+f11离开func9,发现func9调用完成后紧接着就是一个大循环,循环的代码太多,而且操作大多是读写内存,所以我选择查看内存。

再次学习wasm的汇编。读取(load)和写入(store)指令的目标内存地址在栈当中储存,栈在f12中可以直接查看。

然后就是把func9调用结束后访问的内存全部查看一遍了

第一次调用处在循环起点,好像没什么东西

第二次在循环中部,也没有。

第三次是在某个块的尾部被调用。这次后续的写入出现了offset参数,也就是说操作位置要在目标地址的基础上增加一些偏移

第四次同第二次。。

到了第五次开始出现转机了。虽然汇编代码操作的内存位置对不上,但是还是在内存中看见了这一串字符

我盲猜这个就是flag,用控制台输入代码new TextDecoder().decode(new Uint8Array(memories.$memory.buffer,0x000FFE98,50))导出出来(flag每次生成的都不同,这里的截图是另一轮生成的验证码flag,所以与前面的截图对不上)

禁用断点,继续运行,然后验证,还真是flag

第十 windows 高级题

没解出来。

经过ida分析,似乎是对所有常量字符串都进行了加密,而且大部分函数中间还穿插了大量花指令和动态跳转,以致ida的f5都无法完整分析。可能需要一些特定技巧才能解除这些干扰,但是我不会。。故放弃。

第十一 MCP

我对ai的认知还停留在只能web端上交互,像MCP、openclaw那样需要花费api配额的用法都是富哥才敢玩我只能敬而远之的想法上。所以这一题我没像题目md里描述的那样通过ai配置连接,而是找了一个官方发布的调试工具MCP Inspector来手动分析,然后用python脚本得到flag。。。(现在想来可能还不如用ai分析,自找苦吃了)

这款调试工具基于nodejs开发,通过npm install下载和部署,然后用npm run来运行。填写mcp地址和类型(Streamable HTTP)就可以连接了

题目的提示没补全的时候我都是在瞎转。后面把复核这条说明补上后,我以为还有初核需要探索,就依然在瞎转。。。最后看见有许多人突然都做出来了,才发现说明里又加入了审计的提示,而且把流程也明写出来了。(既然有人能做出来,就说明肯定有解了)

翻阅mcp的协议规范,规范上指出mcp可以提供Prompts、Resources、Tools几种服务,而用调试工具查看,这个mcp上提供的只有tools,分别是:

start_session
list_documents
get_document
search_documents
review_document
log_sensitive_access
get_audit_log
execute_tool_chain
update_tool_chain
list_all_documents
get_system_config

除了start_session以外,其他都需要start_session返回的会话id作为参数(调试工具上操作时每次调用都需要自行复制粘贴会话id)。继续尝试瞎转,list_documents、get_document、search_documents都能直接使用,而后面的会报permission_denied。

通过list_documents和get_document得到mcp中包含的文本资源如下:

{
  "id": "public:/docs/annual_report",
  "title": "云台岁终告谕·新岁启卷",
  "content": "云台司岁终告谕:凡尘与灵脉同庆,诸峰钟鼓齐鸣。请诸弟子于新岁晨钟后赴山门广场,恭听掌门示诫。偈语首字:玄霄。",
  "access_level": 1,
  "department": "云台司"
}
{
  "id": "public:/docs/employee_handbook",
  "title": "门规行仪录",
  "content": "行走尘寰,当守门规:不得擅离山门,不得窥探禁阁。新岁值守次序已悬于功德堂。偈语末字:密令。",
  "access_level": 1,
  "department": "功德堂"
}
{
  "id": "public:/docs/false_scroll",
  "title": "伪卷疑章",
  "content": "残页妄载:偈语首字作“玄冥”,末字作“密符”。此卷断简残缺,疑点甚多,务必慎辨真伪。",
  "access_level": 1,
  "department": "杂录坊"
}
{
  "id": "public:/docs/challenge_hint",
  "title": "玄堂复核提示",
  "content": "复核字段:\nctf_request=<passphrase>\naudit_log_id=<audit_log_id>\nseal=<seal>\nseal 公式:sha256(audit_log_id + passphrase),取前8位小写十六进制。\n常见误区:\n- 复核目标文档:confidential:/audit/logs。\n- ctf_request 填的是复核口令本体,不是 access_token;口令线索散在公开卷宗中。\n- audit_log_id 要用触发拒绝后返回的完整审计编号,截断或改写都会失配。\n- 复核仅认可同一会话中最近一次拒绝访问返回的审计编号;跨会话或旧编号均会失效。\n- 审计链路讲究单一职责:复核阶段的链路配置越纯粹,越不容易断链。\n- 复核凭据具时效与一次性,校验失败后请重新触发拒绝并获取新编号。\n- 复核阶段拿到的凭据用于打通审计流程,最终读取密卷仍需后续凭据。",
  "access_level": 1,
  "department": "玄堂"
}
{
  "id": "public:/docs/board_route_hint",
  "title": "议会卷宗路引",
  "content": "终卷路径由三段拼接:前缀 secret:/,域 board,条目 resolutions。按此组合即可得到最终文档 URI。",
  "access_level": 1,
  "department": "玄堂"
}

这里面,玄堂复核提示和议会卷宗路引是后期才被加上的。我一开始看着开头那三个文本摸不着头脑,丢给ai,不出意料地ai也看不懂。还以为“偈语”是指某个特定的加密算法的密钥。。但是又看不见密文在哪。。

到后期我才反应过来,会报permission_denied的那些工具可能才是分析的重点。update_tool_chain貌似是构建工具链的,只能运行list_documents、get_document、search_documents和get_audit_log这几个工具,而且只能在参数中填写工具名称,没有填写工具自身的参数的地方。将链参数设置为get_audit_log,会在输出中出现ctf的字眼。

玄堂复核提示也更新了好几次。更新的最终版本增加了复核目标文档,给出了审核文件的地址,经过瞎摸,这个地址应使用review_document这个工具。设置doc_id为confidential:/audit/logs,输出中的guide字段直接把解法写出来了:

{
  "error": "ctf_missing_field",
  "missing_field": "review_notes",
  "guide": {
    "review_doc_id": "confidential:/audit/logs",
    "audit_log_source_doc": "secret:/board/resolutions",
    "review_template": "ctf_request=<passphrase>\naudit_log_id=<audit_log_id>\nseal=<seal>",
    "seal_formula": "sha256(audit_log_id + passphrase) 前8位小写十六进制",
    "chain_config_required": [
      "get_audit_log"
    ],
    "token_flow_hint": "tool_access_token -> update_tool_chain/execute_tool_chain -> _audit_token",
    "decoy_warning": "compliance_blob_b64 为干扰信息,不作为判题依据",
    "required_public_clues": [
      "public:/docs/annual_report",
      "public:/docs/employee_handbook",
      "public:/docs/challenge_hint",
      "public:/docs/board_route_hint"
    ],
    "unread_public_clues": [],
    "target_doc_hint_doc": "public:/docs/board_route_hint"
  }
}

输出的error中也包含ctf的字样,说明解题路径至少打通了(但是这个guide字段有时在调试工具中看不见,不知为何)

整理一下这里面的信息,并再次经过一轮的瞎摸可知,解题需要经过token_flow_hint描述的三个流程,所以我直接借这个提示列出我采用的过程:

  1. tool_access_token
    • 先根据“玄堂复核提示”,以required_public_clues中列出的所有文本id和secret:/board/resolutions为doc_id参数执行get_document(注意这里有坑:对于每个新的会话id,不能只访问secret:/board/resolutions,并且访问有顺序要求,应该最后访问它),拿到拒绝访问的提示和audit日志编号
      • 然后获取audit的访问权限。在get_audit_log和update_tool_chain之间,只有update_tool_chain才会在报错时出现“ctf”字样,但是update_tool_chain有一参数正是还没找到的access_token,所以还需从其他工具入手
    • 经过瞎摸发现有个工具叫review_document,它需要review_notes这个参数
      • guide提示给出了review_template,正是对应review_notes
      • doc_id使用review_doc_id所给的confidential:/audit/logs
      • 正确执行后就得到access_token
        • 同时也出现了compliance_blob_b64以及compliance_note: 玄堂复核戒律:chain_config 仅提交 get_audit_log;混入其他工具会触发 invalid_chain 且当前凭据作废之类的字段,忽略不管
  2. update_tool_chain/execute_tool_chain
    • 填写了update_tool_chain中的access_token参数后再执行,达成“玄堂复核提示”说的“打通审计流程”
      • 但是执行update_tool_chain后再执行execute_tool_chain依旧会返回失败,而已有提示中没有提及相关信息
        • 我的做法是转而执行get_audit_log,成功了,拿到了_audit_token
  3. _audit_token
    • 以_audit_token为access_token,再以secret:/board/resolutions为doc_id,执行get_document就得到flag了

review_template中的seal需要计算sha256,而且输入所需的audit编号又是动态获取,手工计算很麻烦,所以我最后将整个过程以脚本执行的方式进行。脚本中除了上述过程外还附加了第零步:获取mcp-session-id,用于完成mcp协议握手。先让chatGPT写出握手的代码,再照猫画虎手工补全后续步骤,调试工具会将输入输出的json结构体展示出来,因此可以直接复制粘贴(但是工具展示得不全,比如没有jsonrpc和id,所以代码脚本中需要补上)。服务器响应中除了json还包含其他东西,因此还需要截断响应体

import argparse
import requests,hashlib
import time
import json

# 用法:xxx.py tools 玄霄密令
# 其他参数作废

ap = argparse.ArgumentParser()
apsp=ap.add_subparsers(required=True,dest='command')
apspt=apsp.add_parser("tools",  help="tools compromising")
apspt.add_argument("phrase",nargs="?",default="111")
apsp.add_parser("res",  help="resource")

n=ap.parse_args()

url = "https://9863968daeea51ea32f40575dd41dd113.52pojie.cn:3000/mcp"
headers = {
    "Content-Type": "application/json; charset=utf-8",
    "Accept": "application/json, text/event-stream",
    "User-Agent": "curl/8.13.0",
    "MCP-Protocol-Version": "2024-11-05",
    # 'mcp-session-id': '761814da-f993-0182-0156-c4d97b6314e0',
    # "Authorization": "Bearer YOUR_TOKEN",  # 如需认证则取消注释
}
payload = {
    "jsonrpc": "2.0",
    "method": "initialize",
    "params": {
        "protocolVersion": "2024-11-05",
        "clientInfo": {"name": "test-client", "version": "1.0.0"},
        "capabilities": {}
    },
    "id": 1
}

def presp(resp):
    """
    打印服务器响应
    """
    print("Status:", resp.status_code)
    print("Response headers:", resp.headers)
    print(resp.text)
resp = requests.post(url, json=payload, headers=headers, stream=False)
presp(resp)
uid=resp.headers["mcp-session-id"]
presp(resp)
resp.close()
headers.update({'mcp-session-id':(uid)})

if n.command=="tools":
    sespayload = {
        "jsonrpc": "2.0",
      "method": "tools/call",
      "params": {
        "name": "start_session",
        "arguments": {},
        "_meta": {
          "progressToken": 1
        }
      },
        "id": 1
    }
    syspayload = {
        "jsonrpc": "2.0",
        "method": "tools/call",
        "params": {
          "name": "get_system_config",
          "arguments": {
            "session_id": None,
          },
          "_meta": {
            "progressToken": 2
          }
        },
        "id": 1
    }
    resp = requests.post(url, json=sespayload, headers=headers, stream=False)
    presp(resp)
    idx=resp.text.index("{") # 截断响应体
    sid=json.loads(json.loads(resp.text[idx:])["result"]["content"][0]['text'])['session_id']
    for doc in [
        "public:/docs/challenge_hint",
        "public:/docs/annual_report",
        "public:/docs/board_route_hint",
        "public:/docs/employee_handbook",
        "secret:/board/resolutions",
    ]:
        docpayload = {
            "jsonrpc": "2.0",
            "method": "tools/call",
            "params": {
              "name": "get_document",
              "arguments": {
                    "session_id": sid,
                    "doc_id": doc
                },
              "_meta": {
                "progressToken": 3
              }
            },
            "id": 1
        }
        resp = requests.post(url, json=docpayload, headers=headers, stream=False)
    presp(resp)
    idx=resp.text.index("{")
    aid=json.loads(json.loads(resp.text[idx:])["result"]["content"][0]['text'])['audit_log_id']
    ph=n.phrase
    revpayload = {
        "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "review_document",
    "arguments": {
      "session_id": sid,
      "doc_id": "confidential:/audit/logs",
      "review_notes":f"""ctf_request={ph}
      audit_log_id={aid}
      seal={hashlib.sha256((aid+ph).encode('utf-8')).hexdigest()[:8].lower()}
      """
    },
    "_meta": {
      "progressToken": 4
    }
  },
        "id": 1
    }

    print(revpayload:=json.dumps(revpayload, ensure_ascii=False))
    resp = requests.post(url, data=revpayload.encode("utf-8"), headers=headers, stream=False)
    resp.encoding = 'utf-8'
    idx=resp.text.index("{")
    acid=json.loads(json.loads(resp.text[idx:])["result"]["content"][0]['text'])["tool_access_token"]
    presp(resp)
    # assert False
    chainpayload = {
        "jsonrpc": "2.0",
        "method": "tools/call",
        "params": {
            "name": "update_tool_chain",
            "arguments": {
              "session_id": sid,
              "chain_config": ["get_audit_log"],
              "access_token": acid,
              "audit_log_id": aid
            },
            "_meta":
            {
                "progressToken":5
            }
        },
        "id": 1
    }
    print(chainpayload:=json.dumps(chainpayload, ensure_ascii=False))
    # assert False
    resp = requests.post(url, data=chainpayload.encode("utf-8"), headers=headers, stream=False)
    idx=resp.text.index("{")
    presp(resp)

    # 这一条报错log_not_found,不知是否必要
    exechainpayload = {
        "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "execute_tool_chain",
    "arguments": {
      "session_id": sid
    },
    "_meta": {
      "progressToken": 6
    }
  },
        "id": 1
    }
    print(exechainpayload:=json.dumps(exechainpayload, ensure_ascii=False))
    # assert False
    resp = requests.post(url, data=exechainpayload.encode("utf-8"), headers=headers, stream=False)
    idx=resp.text.index("{")
    presp(resp)

    getlogpayload = {
        "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "get_audit_log",
    "arguments": {
      "session_id": sid,
      "log_id": aid,
      "access_token": acid
    },
    "_meta": {
      "progressToken": 1
    }
  },
        "id": 1
    }
    print(getlogpayload:=json.dumps(getlogpayload, ensure_ascii=False))
    # assert False
    resp = requests.post(url, data=getlogpayload.encode("utf-8"), headers=headers, stream=False)
    idx=resp.text.index("{")
    presp(resp)
    resp.encoding = 'utf-8'
    idx=resp.text.index("{")
    audid=json.loads(json.loads(resp.text[idx:])["result"]["content"][0]['text'])["_audit_token"]
    print("_audit_token is",audid)
    # 最后一步!
    docpayload = {
        "jsonrpc": "2.0",
        "method": "tools/call",
        "params": {
          "name": "get_document",
          "arguments": {
                "session_id": sid,
                "doc_id": "secret:/board/resolutions",
                "access_token":audid
            },
          "_meta": {
            "progressToken": 3
          }
        },
        "id": 1
    }
    resp = requests.post(url, json=docpayload, headers=headers, stream=False)
    resp.encoding = 'utf-8'
    presp(resp) # flag输出
elif n.command=="res":

    respayload = {
        "jsonrpc": "2.0",
        "method": "resources/list",
        "id": 1
    }
    resp=requests.post(url, json=respayload, headers=headers, stream=False)
    presp(resp)

本来还在纠结口令到底是玄霄还是玄冥,结果直接用真卷给的口令就试出来了。。脚本最后一行的输出就是secret:/board/resolutions的内容,也就是flag了

data: {"result":{"content":[{"type":"text","text":"{\n  \"id\": \"secret:/board/resolutions\",\n  \"title\": \"至高议会密议\",\n  \"content\": \"flag{new_year_2026_keep_warm}\",\n  \"access_level\": 4,\n  \"department\": \"玄霄议会\"\n}"}]},"jsonrpc":"2.0","id":1}

当然调试工具中也可得到

免费评分

参与人数 4吾爱币 +6 热心值 +3 收起 理由
beyond1994 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
BrutusScipio + 1 + 1 用心讨论,共获提升!
max2012 + 1 + 1 用心讨论,共获提升!
Coxxs + 3 + 1 用心讨论,共获提升!

查看全部评分

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

沙发
ZhangYixiSuccee 发表于 2026-3-6 09:48
大佬还是强啊,纯手工分析
3#
hdbb 发表于 2026-3-6 10:36
4#
 楼主| hhxxhg 发表于 2026-3-6 11:22 |楼主
5#
beyond1994 发表于 2026-3-10 22:23
看得我头大
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2026-3-11 03:44

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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