MTCTF 2021 Inject Writeup

美团 CTF 上一道有趣的逆向题目

主要涉及:代码解密,远程注入,函数劫持

分析题目

题目给了一个 inject.exe 和 notepad2.exe

首先在 inject.exe 中输入正确的 key1 和 key2 后,inject.exe 会使用 key1 和 key2 来解密程序中的资源文件 yyy.a,并将 yyy.a 作为动态库注入到 notepad2.exe 进程中

接着在启动后的 notepad2.exe 中输入正确的 key3,进程中的 yyy.a 会对 key3 做校验,如果结果正确则将 flag 写入到 flag.txt

解密动态库

其中 key1 和 key2 的约束条件如下

M=0x75D05803
t=dword(key1/key2)%M
p1=pow(t,8)%M
p2=pow(t,2)%M
if ((2*t-p2-42+p1)%M==1 && gcd(key1,key2)==1) goto right

考虑到这个约束条件不太好解决,用 z3 也没跑出来,所以可以考虑从后面解密 yyy.a 的部分入手

解密生成 yyy.a 的代码如下

  v9 = fopen(v8, "wb");
  v10 = FindResourceA(0i64, (LPCSTR)0x65, "code");
  v11 = SizeofResource(0i64, v10);
  v12 = LoadResource(0i64, v10);
  v13 = LockResource(v12);
  fwrite(v13, 1ui64, v11, v9);
  v14 = FindResourceA(0i64, (LPCSTR)0x66, "code");
  v15 = SizeofResource(0i64, v14);
  v16 = LoadResource(0i64, v14);
  v17 = LockResource(v16);
  v18 = malloc((unsigned int)v15);
  v19 = 0;
  if ( (_DWORD)v15 )
  {
    v20 = v18;
    v21 = v17 - v18;
    do
    {
      *v20 = v20[v21] ^ *((_BYTE *)&dword_7FF6F30F66B8 + (v19++ & 7));
      ++v20;
    }
    while ( v19 < (unsigned int)v15 );
  }
  fwrite(v18, 1ui64, v15, v9);
  v22 = FindResourceA(0i64, (LPCSTR)0x67, "code");
  v23 = SizeofResource(0i64, v22);
  v24 = LoadResource(0i64, v22);
  v25 = LockResource(v24);
  fwrite(v25, 1ui64, v23, v9);
  fclose(v9);

生成的 yyy.a 内容由 Resource(101) + Decrypt(Resource(102), key1_2) + Resource(103) 三部分组成

其中 key1_2 是根据 key1 和 key2 生成的 8 字节密钥

Decrypt 使用 key1_2 的 8 个字节来循环异或 Resource(102) 进行解密

下断点查看第三部分开头的内容都是 0,所以可以合理猜测解密后第二部分结尾的内容也是 0

下断点看一下解密前第二部分结尾的几个 8 字节序列确实是相同的,证实了上边的猜测,这样我们就得到了 key1_2 的内容

key1_2 = [0x17, 0xe3, 0xe3, 0x37, 0x17, 0xe3, 0xe3, 0x00]

改 ip 跳过前边的约束检查,直接动态修改内存中的 key1_2,继续运行就可以得到解密后的 yyy.a 了

分析动态库

简单看一下发现 yyy.a 套了个 upx 壳,直接 upx -d 脱掉

主函数如下,可以看出是 hook 了 notepad2.exe 的 WriteFile 函数来实现对 key3 的校验操作

  MessageBoxA(
    0i64,
    "Now input key3 in notepad like `key3:abcdef`.
Save it.Reopen the file, you will get the flag.
",
    "Message",
    0);
  v1 = GetModuleHandleA("kernel32.dll");
  WriteFile = GetProcAddress(v1, "WriteFile");
  v3 = WriteFile + *(WriteFile + 2);
  qword_180004630 = *(v3 + 6);
  VirtualProtect(v3 + 6, 8ui64, 4u, &flOldProtect);
  qword_180004628 = sub_1800011A0;
  *(v3 + 6) = sub_1800011A0;
  VirtualProtect(v3 + 6, 8ui64, flOldProtect, &flOldProtect);

新的函数入口在 sub_1800011A0,里面有校验 key3 与生成 flag 的代码

校验 key3 的代码如下

bool check()
{
  unsigned __int64 v0; // r8
  unsigned __int64 v1; // r9
  __int64 v2; // rcx
  unsigned __int64 v3; // r11
  unsigned __int64 v4; // r10
  __int64 v5; // rcx
  __int64 v6; // rbx

  v0 = 1i64;
  v1 = 1i64;
  v2 = 4i64;
  v3 = input % 0x6440DB83ui64;
  do
  {
    v1 = v3 * v1 % 0x6440DB83;
    --v2;
  }
  while ( v2 );
  v4 = 1i64;
  v5 = 3i64;
  do
  {
    v4 = v3 * v4 % 0x6440DB83;
    --v5;
  }
  while ( v5 );
  v6 = 2i64;
  do
  {
    v0 = v3 * v0 % 0x6440DB83;
    --v6;
  }
  while ( v6 );
  return (2 * v0 - v4 + v1 - 32) % 0x6440DB83 == 1;
}

生成 flag 的代码如下

      sub_180001010(a2, "key3:%x", &dword_18000463C);
      *(float *)&input = (double)(int)dword_18000463C
                       * 1.818989403545856e-12
                       * 1.818989403545856e-12
                       * 0.00000002980232238769531;
      if ( check() )
      {
        v9 = (char *)malloc(0x2Bui64);
        idx = 5;
        v11 = byte_180003280;
        flag = v9;
        len = 0;
        idx_ = 5i64;
        qmemcpy(v9, "flag{", 5);
        v15 = 0x10842000i64;
        do
        {
          if ( idx_ <= 28 && _bittest64(&v15, idx_) )
          {
            v16 = '-';
          }
          else
          {
            v17 = len % 8;
            v18 = *v11;
            ++len;
            ++v11;
            v16 = v18 ^ *((_BYTE *)&input + v17);
          }
          flag[idx_] = v16;
          ++idx;
          ++idx_;
        }
        while ( len < 32 );
        v19 = 42i64;
        v20 = flag;
        *(_WORD *)&flag[idx] = '}';
      }
      else
      {
        v19 = 23i64;
        v20 = "your key isn't correct.";
      }

爆破密钥

爆破下面这个 key3 的中间结果,结果为 386499290

      *(float *)&input = (double)(int)dword_18000463C
                       * 1.818989403545856e-12
                       * 1.818989403545856e-12
                       * 0.00000002980232238769531;
#include <cstdio>

bool check(unsigned input)
{
  unsigned long long v0; // r8
  unsigned long long v1; // r9
  unsigned long long v2; // rcx
  unsigned long long v3; // r11
  unsigned long long v4; // r10
  unsigned long long v5; // rcx
  unsigned long long v6; // rbx

  v0 = 1;
  v1 = 1;
  v2 = 4;
  v3 = input % 0x6440DB83u;
  do
  {
    v1 = v3 * v1 % 0x6440DB83;
    --v2;
  }
  while ( v2 );
  v4 = 1;
  v5 = 3;
  do
  {
    v4 = v3 * v4 % 0x6440DB83;
    --v5;
  }
  while ( v5 );
  v6 = 2;
  do
  {
    v0 = v3 * v0 % 0x6440DB83;
    --v6;
  }
  while ( v6 );
  return (2 * v0 - v4 + v1 - 32) % 0x6440DB83 == 1;
}

int main(){
    printf("%d
",check(386499290));
    for (unsigned int i=0;i<0xffffffff;i++){
        if (check(i)) {
            printf("%u
",i);
            getchar();
        }
    }
}

使用上面的中间结果爆破 key3,结果为 4505965

#include <cstdio>

unsigned int input=0;

int main(){
    for (unsigned int i=0;i<0xffffffff;i++){
        *(float *)&input = (double)i
                       * 1.818989403545856e-12
                       * 1.818989403545856e-12
                       * 0.00000002980232238769531;
        if (input==386499290) printf("%u",i);
    }
}

调用动态库

写一个程序直接从外部调用 yyy.a 的校验函数 sub_1800011A0 (偏移量为 0x11A0),传入 "key3:44c16d" (4505965)

#include <stdio.h>
#include <windows.h>
#include <map>
using namespace std;
typedef int (*func1)(__int64,char*, __int64, __int64, __int64);
int main() {
    HMODULE hdll = NULL;
    hdll = LoadLibrary(L"yyy_unpack.dll");
    if (hdll != NULL) {
        printf("YES
");
        func1 myfunc = ((func1)((PBYTE)hdll + 0x11A0));
        for (int i = 0; i < 128; i++)
        {
            printf("%02x", *((unsigned char*)myfunc + i));
        }
        myfunc(0, (char*)"key3:44c16d", 0, 0, 0);
    }
    else {
        printf("NO
");
    }
    FreeLibrary(hdll);
    return 0;
}

在 myfunc 这里下断点跟进去,在解密完成后的地方再下断点,就可以在内存中找到 flag

原文地址:https://www.cnblogs.com/algonote/p/14802566.html