hook

主函数代码如下

int __cdecl main(int argc, const char **argv, const char **envp)
{
  HANDLE FileA;
  DWORD NumberOfBytesWritten;
  char Buffer[32];

  sub_401370(aPleaseInputFla);
  scanf("%31s", Buffer);
  if ( strlen(Buffer) == 19 )
  {
    sub_401220();
    FileA = CreateFileA(FileName, 0x40000000u, 0, nullptr, 2u, 0x80u, nullptr);
    WriteFile(FileA, Buffer, 0x13u, &NumberOfBytesWritten, nullptr);
    sub_401240(Buffer, &NumberOfBytesWritten);
    if ( NumberOfBytesWritten == 1 )
      sub_401370(aRightFlagIsYou);
    else
      sub_401370(aWrong);
    system(Command);
    return 0;
  }
  else
  {
    sub_401370(aWrong);
    system(Command);
    return 0;
  }
}

从主函数看起来 关键逻辑像是在 WriteFile 和 sub\401240 里面 程序先调用 sub\401220 然后创建一个文件 接着调用 WriteFile 把输入写入文件 最后调用 sub\401240 表面上看 sub\401240 好像是最终校验函数 但是实际真正的校验发生在 WriteFile 被 hook 之后的函数里面

先看 sub\401220

int sub_401220()
{
  HMODULE LibraryA;
  DWORD CurrentProcessId;

  CurrentProcessId = GetCurrentProcessId();
  hProcess = OpenProcess(0x1F0FFFu, 0, CurrentProcessId);
  LibraryA = LoadLibraryA(LibFileName);
  WriteFile_0 = (BOOL (__stdcall *)(HANDLE, LPCVOID, DWORD, LPDWORD, LPOVERLAPPED))GetProcAddress(LibraryA, ProcName);
  lpAddress = WriteFile_0;
  if ( !WriteFile_0 )
    return sub_401370((int)&unk_40A044);
  unk_40C9B4 = *(_DWORD *)lpAddress;
  unk_40C9B8 = *((_BYTE *)lpAddress + 4);
  byte_40C9BC = -23;
  dword_40C9BD = (char *)sub_401080 - (char *)lpAddress - 5;
  return sub_4010D0();
}

GetCurrentProcessId 用来获取当前进程 ID

OpenProcess 用 0x1F0FFF 权限打开当前进程

LoadLibraryA 加载 DLL

这里的 LibFileName 大概率是 kernel32 dll

GetProcAddress 根据 ProcName 获取函数地址

这里 ProcName 大概率是 WriteFile

获取到 WriteFile 地址之后 程序把地址保存到 lpAddress

接着保存 WriteFile 入口的前 5 个字节

unk_40C9B4 = *(_DWORD *)lpAddress;
unk_40C9B8 = *((_BYTE *)lpAddress + 4);

这两句一共保存了 5 个字节 然后程序构造 hook 补丁

byte_40C9BC = -23;
dword_40C9BD = (char *)sub_401080 - (char *)lpAddress - 5;

这里 byte\40C9BC 是 signed char 所以 -23 对应的无符号字节是 0xE9 0xE9 是 x86 里面的 near jmp 指令 dword\40C9BD 是 jmp 的相对偏移

也就是

目标地址 - 当前地址 - 5

所以这里构造出来的 5 字节补丁是

E9 xx xx xx xx

含义是

jmp sub_401080

也就是说 程序准备把 WriteFile 的入口改成跳转到 sub\401080

然后进入 sub\4010D0

BOOL sub_4010D0()
{
  DWORD v1;
  DWORD flOldProtect;

  v1 = 0;
  VirtualProtectEx(hProcess, lpAddress, 5u, 4u, &flOldProtect);
  WriteProcessMemory(hProcess, lpAddress, &byte_40C9BC, 5u, nullptr);
  return VirtualProtectEx(hProcess, lpAddress, 5u, flOldProtect, &v1);
}

sub\4010D0 负责真正写入 hook

VirtualProtectEx 把 WriteFile 入口所在内存页改成可读写执行

这里的 4 对应 PAGE\EXECUTE\READWRITE

然后 WriteProcessMemory 把前面构造好的 5 字节 jmp 写到 WriteFile 入口

最后再次调用 VirtualProtectEx 恢复原来的内存权限

所以 sub\401220 执行完成以后

WriteFile 的入口已经被改成

jmp sub_401080

因此主函数里面这句

WriteFile(FileA, Buffer, 0x13u, &NumberOfBytesWritten, nullptr);

实际不会直接进入系统的 WriteFile

而是会先跳到 sub\401080

sub\401080 代码如下

int __stdcall sub_401080(
        HANDLE hFile,
        LPCVOID lpBuffer,
        DWORD nNumberOfBytesToWrite,
        LPDWORD lpNumberOfBytesWritten,
        LPOVERLAPPED lpOverlapped)
{
  int v5;

  v5 = sub_401000(lpBuffer, nNumberOfBytesToWrite);
  sub_401140();
  WriteFile(hFile, lpBuffer, nNumberOfBytesToWrite, lpNumberOfBytesWritten, lpOverlapped);
  if ( v5 )
    *lpNumberOfBytesWritten = 1;
  return 0;
}

这里才是真正的校验入口 它先调用 sub\401000 参数 lpBuffer 就是输入的 Buffer 参数 nNumberOfBytesToWrite 是 0x13 也就是 19 所以 sub\401000 会处理输入的 19 个字节

然后 sub\401080 调用 sub\401140 sub\401140 的作用是恢复 WriteFile 原始入口

代码如下

BOOL sub_401140()
{
  DWORD v1;
  DWORD flOldProtect;

  v1 = 0;
  VirtualProtectEx(hProcess, lpAddress, 5u, 4u, &flOldProtect);
  WriteProcessMemory(hProcess, lpAddress, &unk_40C9B4, 5u, nullptr);
  return VirtualProtectEx(hProcess, lpAddress, 5u, flOldProtect, &v1);
}

它同样先修改内存页权限 然后把之前保存的 WriteFile 前 5 个字节写回去 这样后面再调用 WriteFile 的时候就不会再次跳到 sub\401080 也就避免了无限递归 所以 sub\401080 的执行顺序是 先用 sub\401000 校验输入 然后恢复 WriteFile 原始字节 然后真正写文件 如果 sub\401000 返回 1 就把 lpNumberOfBytesWritten 改成 1

主函数后面判断的是

if ( NumberOfBytesWritten == 1 )

所以最终能不能输出 right 关键就看 sub\401000 是否返回 1 继续看 sub\401000

int __cdecl sub_401000(int a1, int a2)
{
  char i;
  bool v3;
  char v4;
  char v5;
  int v6;

  for ( i = 0; i < a2; ++i )
  {
    if ( i == 18 )
    {
      *(_BYTE *)(a1 + 18) ^= 0x13u;
    }
    else
    {
      v3 = (i & 0x80000001) == 0;
      if ( i < 0 )
        v3 = (((i & 1) - 1) | 0xFFFFFFFE) == -1;
      if ( v3 )
        v4 = *(_BYTE *)(i + a1 + 2);
      else
        v4 = *(_BYTE *)(i + a1) - i;
      *(_BYTE *)(i + a1) = i ^ v4;
    }
  }
  v5 = 0;
  if ( a2 <= 0 )
    return 1;
  v6 = 0;
  while ( byte_40A030[v6] == *(_BYTE *)(v6 + a1) )
  {
    v6 = ++v5;
    if ( v5 >= a2 )
      return 1;
  }
  return 0;
}

exp

target = [
    0x61, 0x6a, 0x79, 0x67, 0x6b,
    0x46, 0x6d, 0x2e, 0x7f, 0x5f,
    0x7e, 0x2d, 0x53, 0x56, 0x7b,
    0x38, 0x6d, 0x4c, 0x6e
]

flag = [0] * 19

for i in range(19):
    if i == 18:
        flag[18] = target[18] ^ 0x13
    elif i & 1:
        flag[i] = ((target[i] ^ i) + i) & 0xff
    else:
        flag[i + 2] = target[i] ^ i

flag[0] = ord('f')

print(bytes(flag).decode())

flag

flag{Ho0k_w1th_Fun}