ho_0k

127次阅读
没有评论

ho_0k

打开IDA,看到主入口里面就是一个平平无奇的输入与预期字符串比较,大概率是判断flag

int __fastcall main(int argc, const char **argv, const char **envp)
{
  char Str1[64]; // [rsp+20h] [rbp-40h] BYREF

  _main(argc, argv, envp);
  puts("Input your flag:");
  scanf("%64s", Str1);
  if ( !strcmp(Str1, "VIDAR{ho_0k&&h0_ok&&h0_0k&&ho_ok}") )  //很奇怪 这里使用了几个变量替换
    puts("yes!");
  else
    puts("no!");
  return 0;
}

然后看到ho_0k 大概率就是程序的核心部分

int ho_0k()
{
  int result; // eax
  HANDLE CurrentProcess; // rax
  HANDLE v2; // rax
  SIZE_T NumberOfBytesRead; // [rsp+38h] [rbp-38h] BYREF
  _BYTE v4[5]; // [rsp+46h] [rbp-2Ah] BYREF
  int Buffer; // [rsp+4Bh] [rbp-25h] BYREF
  char v6; // [rsp+4Fh] [rbp-21h]
  struct _PEB *v7; // [rsp+50h] [rbp-20h]
  int v8; // [rsp+5Ch] [rbp-14h]
  struct _PEB *v9; // [rsp+60h] [rbp-10h]
  LPCVOID lpBaseAddress; // [rsp+68h] [rbp-8h]

  result = (int)refptr_strcmp;   //这是链接器生成的对 strcmp 的函数引用(全局函数指针)。
  lpBaseAddress = refptr_strcmp;  //它实际指向真正的 strcmp() 实现地址。
  Buffer = 0;
  v6 = 0;
  memset(v4, 0, sizeof(v4));
  NumberOfBytesRead = 0LL;
  if ( refptr_strcmp )
  {
    v8 = 96;
    v7 = NtCurrentPeb();  //读取当前Peb
    v9 = v7;
    CurrentProcess = GetCurrentProcess();  //获取当前线程
    ReadProcessMemory(CurrentProcess, lpBaseAddress, &Buffer, 5uLL, &NumberOfBytesRead);  
	//从 refptr_strcmp(即 strcmp 的函数指针)读取前 5 字节
    v4[0] = -23 - v9->BeingDebugged;  //修改一个字节  反调试
    *(_DWORD *)&v4[1] = (unsigned int)strcmp - 5 - (_DWORD)lpBaseAddress; // 计算偏移
    v2 = GetCurrentProcess();
    return WriteProcessMemory(v2, (LPVOID)lpBaseAddress, v4, 5uLL, &NumberOfBytesRead);  //自修改
  }
  return result;
}

ho_0k解读

  1. refptr_strcmp
    这是链接器生成的对 strcmp​ 的函数引用(全局函数指针)。
    它实际指向真正的 strcmp() 实现地址。

  2. ReadProcessMemory WriteProcessMemory
    这意味着程序​在运行时读取并修改自己内存中的代码(自修改代码)。

  3. 修改内容

    v4[0] = -23 - v9->BeingDebugged;
    *(_DWORD*)&v4[1] = (unsigned int)strcmp - 5 - (_DWORD)lpBaseAddress;
    

    一般来说,v4[0]​ 很可能是写入一个 JMP​ 指令(0xE9)的补码形式。
    -23​ 十进制是 0xE9​ 的补码(256 – 23 = 233 = 0xE9)。
    因此如果没有调试(BeingDebugged==0),v4[0]​ = 0xE9;
    如果在调试,v4[0]​ = 0xE8 (call) 或 0xE9 – 1 之类——反调试关键点。
    所以:

    这段代码实质是:当程序没被调试时,把 strcmp 前 5 字节改写为一个 jmp 指令,跳转到真正的 strcmp 实现处。
    如果在调试器下,BeingDebugged​ = 1,会导致写入错误的字节码而破坏 strcmp,使程序失效。

这段 strcmp​ 伪代码正是你那道题里真正的“隐藏核心逻辑”——
原来程序不是直接调用标准 strcmp​,而是自己在运行时修补 IAT (导入表)把系统的 strcmp 替换成了它自写的版本,也就是这个。

下面给你详细拆解一下,每一步都是什么作用。


🧩 结构概览

int64 __fastcall strcmp(const char *a1)
{
    _BYTE v2[48];
    ...填充了 40 个字节常量...
    if (strlen(a1) != 40)
        return 1LL;
    v4 = 96;
    v3 = NtCurrentPeb();
    p_NtGlobalFlag = &v3->NtGlobalFlag;
    for (i = 0; i < strlen(a1); ++i)
    {
        a1[i] = *p_NtGlobalFlag + 17 * a1[i] + 35;
        if (a1[i] != v2[i])
            return 1LL;
    }
    return 0LL;
}

🧠 逐行说明

1️⃣ v2 是目标字节序列

这 40 字节(负数只是 unsigned char​ 变换)就是最终希望你输入的每一位在计算完后的结果。
也就是说,合法的输入字符串 a1​ 经过某个数学变换后应该刚好等于 v2[]


2️⃣ PEB / NtGlobalFlag 与反调试

v3 = NtCurrentPeb();
p_NtGlobalFlag = &v3->NtGlobalFlag;
  • NtCurrentPeb():取当前进程 PEB。
  • NtGlobalFlag 距离 BeingDebugged 更后一点;Windows 会在调试器存在时让这个标志的部分位(0x70, 0x2, 0x10)拉高。

题目的意图:相同的输入,如果正在被调试器附加,它读取到的 NtGlobalFlag 不同,后续计算结果全部不同。

这就是反调试机制里的另一个检测点。


3️⃣ 循环逻辑

a1[i] = *p_NtGlobalFlag + 17 * a1[i] + 35;

每个字符都会被 “加密” 成一个新数值:

new_char = 17 * old_char + NtGlobalFlag + 35

然后与 v2[i] 比较。

如果所有 40 位都相等返回 0(正确),否则返回 1(错误)。


🔍 程序的真实校验顺序

  1. 程序启动时执行 ho_0k()

    • 它把 IAT 里的 strcmp 地址替换成此处自定义版本。
    • 如果检测到 BeingDebugged = 1(由 PEB 设置),还会破坏那 5 字节,使校验一定失败。
  2. 后面调用 “strcmp” 的地方实际上调用了这段函数。

  3. 该函数再通过 NtGlobalFlag 检查当前是否被调试;若是,计算不同。


🔧 你可以怎么验证(在比赛沙盒中)

以下都只在 CTF 沙盒环境里进行,用来理解过程,不是实机利用。

  1. 脱离调试器运行程序(让 NtGlobalFlag=0​),运行输入你自己算出的结果字符串,看它是否输出 yes!

  2. 静态推算逆变换
    数学等式:

    v2[i] = 17 * plain[i] + NtGlobalFlag + 35
    

    当 NtGlobalFlag=0 时:

    plain[i] = (v2[i] - 35) * 17^-1 mod 256
    

    其中 17^-1 是 17 在 256 下的乘法逆元 (=241)。
    所以:

    plain[i] = ((v2[i] - 35) * 241) mod 256
    

    用这公式对 40 个 v2 字节解回去就能得到正确 flag。

    (你可以用 Python/C 去算,在比赛环境中自己编码即可。)

  3. 带调试器运行相当于 NtGlobalFlag​ 不为 0;此时每个结果不同,自然比较失败,输出 no!


✅ 小结

项目 说明
函数来源 程序自定义的伪 strcmp
功能 校验 flag 是否正确
检查机制 每个字符经线性变换 (17x+35+NtGlobalFlag) 与固定表比对
反调试点 依赖 NtGlobalFlag 值变化,调试器附加时必错
逆向要点 关闭调试、或模拟 NtGlobalFlag=0,再对表 v2 求逆变换恢复原 flag 

所以一句话总结:

这段 “strcmp” 是程序在反调试保护下实现的自定义校验函数,用来检测输入的 flag。

很好!你找到了被修改后的 strcmp​ 函数。这是一个加密比较逻辑,让我们分析一下:

代码分析

关键逻辑:

  1. 检查输入长度是否为 40
  2. 使用 NtGlobalFlag 作为密钥的一部分
  3. 对每个字符进行变换:a1[i] = *p_NtGlobalFlag + 17 * a1[i] + 35
  4. 与硬编码的 v2 数组比较

解题步骤

第一步:获取 NtGlobalFlag 值

在调试器中:

  1. 在修改后的 strcmp 函数中设断点
  2. 运行程序,输入任意40字符的字符串
  3. 当断点命中时,查看 p_NtGlobalFlag 指向的值

或者直接计算

// 在正常情况下,NtGlobalFlag 的值通常是:
// - 非调试状态:0x0
// - 调试状态:0x70

第二步:逆向加密算法

加密公式:

encrypted[i] = NtGlobalFlag + 17 * input[i] + 35

解密公式:

input[i] = (encrypted[i] - NtGlobalFlag - 35) * inverse(17) mod 256

第三步:编写解密脚本

# 加密数据
v2 = [
    -39, -4, -89, 116, -107, 78, -106, -126, 113, -6,
    -75, -105, -41, -24, 79, -108, -41, 100, -126, 113,
    -58, 114, -4, 113, 47, 28, 113, -40, -21, 83,
    -126, 62, 114, 96, -108, -58, -41, -40, -75, 112
]

# 转换为无符号字节
encrypted = [b & 0xFF for b in v2]

# 假设 NtGlobalFlag 值(需要调试确认)
nt_global_flag = 0x0  # 或者 0x70,需要实际调试确定

# 计算模逆元(17在模256下的逆元)
# 17 * 逆元 ≡ 1 mod 256
def mod_inverse(a, m=256):
    for i in range(m):
        if (a * i) % m == 1:
            return i
    return None

inverse_17 = mod_inverse(17)

# 解密函数
def decrypt(encrypted, nt_global_flag):
    result = []
    for enc_byte in encrypted:
        # input[i] = (encrypted[i] - nt_global_flag - 35) * inverse(17) mod 256
        decrypted = ((enc_byte - nt_global_flag - 35) * inverse_17) % 256
        result.append(decrypted)
    return bytes(result)

# 尝试两种可能的 NtGlobalFlag 值
for flag_val in [0x00, 0x70]:
    try:
        decrypted = decrypt(encrypted, flag_val)
        print(f"NtGlobalFlag=0x{flag_val:02x}: {decrypted}")
        # 检查是否可打印
        if all(32 <= b < 127 for b in decrypted):
            print(f"可读字符串: {decrypted.decode('ascii')}")
    except:
        pass

第四步:在调试器中确认 NtGlobalFlag

在 x64dbg 中:

  1. 在修改后的 strcmp 中设断点

  2. 运行并输入40个字符(如 "A"*40)

  3. 当断点命中时:

    • 查看 p_NtGlobalFlag 指向的值
    • 或者在寄存器/内存窗口中找这个值

第五步:获取真实 Flag

运行解密脚本后,你会得到真正的 flag。

快速验证:先尝试 NtGlobalFlag = 0​,如果结果是乱码,再试 NtGlobalFlag = 0x70

你现在可以在调试器中查看 p_NtGlobalFlag 的实际值吗?这样我们就能得到确切的 flag 了。

VIDAR{Congr4tulat1ons_InLineH0ok_master}

正文完
 0
评论(没有评论)