hgame_final by RogerThat

​ 对于这道题目有好两个思路可以进行,其一是直接进入程序,在逆向过程中会遇到一个接管几乎全部程序的 switch 结构。很典型的直冲直撞逆向手(在没有 hint 的情况下)。

在我这边的环境下这个函数在 sub_7FF6A2994F44

virtual_machain

可以看出,这个代码很难看懂,但是可以看出他的分支有 46 种,一般出现这么长的 switch 结构是很有问题的,而且根据我的分析,调用不同的 switch 他会调用函数等等,输出出

Welcome to HGAME final!

what is your flag?

在一开始不知道这个和 lua 的情况下焦头烂额,因为想分析这个虚拟机难度非常高,后来我也只是在源码的帮助下才勉强理解。一开始通过 x64_dbg 自带的条件断点,我让他输出出了 siwtch 的分支。

switch_case

具体的 OPCODE 我就不展示了,很长很长,而且大部分都是 case 1。

但是当我知道了它和 lua 有关,那就不一样了,内置的虚拟机必然会去执行 lua 代码,或者 lua 2 luac.out 的字节码。这个代码其实就是在 pe 文件里面,其实 mezone 给的 out 文件多了 debug信息的,反而更容易知道逻辑。

关于我 lua 学习整个过程,我丢在了 mind_strom.md 中了,还挺有纪念价值。

need_dump

后面还有好多,而且没有 debug 的信息,只好对照着源码去寻找节区。

在 pe 中有这么一串典型的 luac.out 文件,而且可以在 exe 中找到 version 5.3.6 其实这里 mezone 说的 luadec 其实有一点问题,对于没有 debug 信息的 .out 不是很支持,而且配置环境有点麻烦,这里推荐我刚找到的新工具,unluac 支持 5.1 - 5.4 ,unluac_2021_03_11.jar

java -jar .\unluac_2021_03_11.jar .\MyByte.out > .\myOut.lua

生成的 myOut.lua 就先不妨这了,太长了,我放在一个压缩包里面好了。

unluac

不是很好阅读,毕竟没有 “符号文件” “debug 信息”。不过也还好,逻辑也能知道。

知道了这个文件之后会发现有一个加密函数 hgame.enc 。很陌生,我决定分析一下 OPCODE 一探究竟。附件:opcode.txt 很多都是手打的,毕竟要分析虚拟机行为。

关键部分

{OP_EQ}         in case 31
{OP_MOVE}       in case 0
{OP_LOADK}      in case 1
{OP_CONCAT}     in case 29
{OP_LOADK}      in case 1
{OP_GETTABUP}   in case 6
{OP_LOADK}      in case 1

{OP_GETTABLE}   in case 7
{OP_MOVE}       in case 0
############################# hgame.enc
{OP_CALL}       in case 36
Under CASE 24,I jmp to small case 16
##############################

{OP_NEWTABLE}   in case 11

{OP_LOADK}      in case 1
{OP_LOADK}      in case 1
{OP_LOADK}      in case 1
{OP_FORPREP}    in case 40
{OP_FORLOOP}    in case 39
{OP_LOADK}      in case 1
{OP_SELF}       in case 12
{OP_MOVE}        in case 0
{OP_MOVE}       in case 0
{OP_CALL}        in case 36
Under CASE 24,I jmp to small case 16
if L2_2 ~= 64 then
  L2_2 = print
  L3_2 = "NO!"
  L2_2(L3_2)
  return
end
L2_2 = L0_2
L3_2 = "0000000000000000"
L0_2 = L2_2 .. L3_2
L2_2 = "hgame"
L2_2 = _ENV[L2_2]
L3_2 = "enc"
L2_2 = L2_2[L3_2]
L3_2 = L0_2
L2_2(L3_2)
L2_2 = {}
L3_2 = 1
L4_2 = 80
L5_2 = 1
for L6_2 = L3_2, L4_2, L5_2 do
  L9_2 = "byte"
  L8_2 = L0_2
  --......--

可以对比 unluac 得到的代码,发现 hgame.enc 这个函数脱离了这个虚拟机而运作,加密了之后再进行下去。这里可以看看源码。

vmcase(OP_CALL) {
  int b = GETARG_B(i);
  int nresults = GETARG_C(i) - 1;
  if (b != 0) L->top = ra+b;  /* else previous instruction set top */
  if (luaD_precall(L, ra, nresults)) {  /* C function? */
    if (nresults >= 0)
      L->top = ci->top;  /* adjust results */
    Protect((void)0);  /* update 'base' */
  }
  else {  /* Lua function */
    ci = L->ci;
    goto newframe;  /* restart luaV_execute over new Lua function */
  }
  vmbreak;
}

他会分析这个要调用的函数是否是 lua 程序 或者 C 程序,而如果是 lua 程序的话,下一步就是进入一个 所谓 newframe ,其实就是新的一个虚拟机。

void luaV_execute (lua_State *L) {
  CallInfo *ci = L->ci;
  LClosure *cl;
  TValue *k;
  StkId base;
  ci->callstatus |= CIST_FRESH;  /* fresh invocation of 'luaV_execute" */
------------------------------important----------------------------
 newframe:  /* reentry point when frame changes (call/return) */
--------------------------------------------------------------------
  lua_assert(ci == L->ci);
  cl = clLvalue(ci->func);  /* local reference to function's closure */
  k = cl->p->k;  /* local reference to function's constant table */
  base = ci->u.l.base;  /* local copy of function's base */
  /* main loop of interpreter */
  for (;;) {
    Instruction i;
    StkId ra;
    vmfetch();
    vmdispatch (GET_OPCODE(i)) {
    --.....--

由此可见这不是一个 lua 程序,我们应该去寻找一个 C function。为了寻找这个,我的设想是在 存储着我的字符串的地方下断,而这个就涉及 lua 的寄存器 Table TValue 等结构的存储。

尝试一下。我下断点下在 OP_GETTABLE 也就是 case 7,他的下一步就是 OP_MOVE 再下一步就是 OP_CALL 调用 hgame.enc 函数,通过这个 OP_GETTABLE 我能得到他的 input 的结构,因为 OP_MOVE 在装载参数

  vmcase(OP_GETTABLE) {
    StkId rb = RB(i);
    TValue *rc = RKC(i);
    gettableProtected(L, rb, rc, ra);
    vmbreak;
  }
###########################
typedef union Value {
  GCObject *gc;    /* collectable objects */
  void *p;         /* light userdata */
  int b;           /* booleans */
  lua_CFunction f; /* light C functions */
  lua_Integer i;   /* integer numbers */
  lua_Number n;    /* float numbers */
} Value;

#define TValuefields	Value value_; int tt_

typedef struct lua_TValue {
  TValuefields;
} TValue;
case 7u:
  v71 = 16i64 * (v56 >> 23) + v55;
  if ( ((v56 >> 14) & 0x100) != 0 )
    v4 = (_DWORD *)(16i64 * (unsigned __int8)(v56 >> 14) + v54);
  else
    v4 = (_DWORD *)(16i64 * ((v56 >> 14) & 0x1FF) + v55);

显然这个 v4 就是 Tvalue *rc;在 x64_dbg 中跟踪这个东西。

TValue

在此处 GETTABLE 下一个断在 OP_CALL 使得这个真正加载完。

out_TValue

在标蓝色的地方下一个 1byte 硬件写入断点,运行。(我这里写了加硬件写入断点,但是刚才调试的时候忘记加了。。又得重来一遍)

hardware_breakpoint

走出这个 dll 代码,肯定是哪里调用了什么修改这个字符串的函数。

发现这个函数在

out_step

回到 ida 找这个代码所在。

会发现这个两个代码,看来是加密函数无疑了。

__int64 __fastcall may_change_string(__int64 a1)
{
  char *v1; // rdi
  __int64 i; // rcx
  char v4[32]; // [rsp+0h] [rbp-30h] BYREF
  size_t Size[4]; // [rsp+38h] [rbp+8h] BYREF
  void *Src; // [rsp+58h] [rbp+28h]
  _OWORD v7[3]; // [rsp+78h] [rbp+48h] BYREF
  _OWORD v8[3]; // [rsp+A8h] [rbp+78h] BYREF
  int part_of_my_string[16]; // [rsp+E0h] [rbp+B0h] BYREF
  char used_to_output[112]; // [rsp+150h] [rbp+120h] BYREF
  char v11[80]; // [rsp+1C0h] [rbp+190h] BYREF
  int v12; // [rsp+244h] [rbp+214h]
  int v13; // [rsp+264h] [rbp+234h]

  v1 = v4;
  for ( i = 258i64; i; --i )
  {
    *(_DWORD *)v1 = -858993460;
    v1 += 4;
  }
  sub_7FF6A2941A7D((__int64)&unk_7FF6A29C40B3);
  Src = (void *)sub_7FF6A29410A0(a1, 1i64, Size);
  Size[0] = 64i64;
  v7[0] = xmmword_7FF6A29B3238;
  v8[0] = xmmword_7FF6A29B3250;
  memset(part_of_my_string, 0, 0x47ui64);
  j_memcpy(part_of_my_string, Src, sizeof(part_of_my_string));
  memset(used_to_output, 0, 0x50ui64);
  memset(v11, 0, sizeof(v11));
  v12 = 64;
  v13 = must_enc((int)v7, (int)v8, (int)part_of_my_string, 64, used_to_output);
  j_memcpy(Src, used_to_output, 0x50ui64);
  sub_7FF6A29418FC((__int64)v4, (__int64)&unk_7FF6A29B30F0);
  return 0i64;
}
__int64 __fastcall sub_7FF6A2947F04(__int64 a1, __int64 a2, const void *part_of_my_string, signed int a4_64u, void *out_put_string)
{
  char *v5; // rdi
  __int64 i; // rcx
  unsigned int v7; // edi
  char v9[32]; // [rsp+0h] [rbp-30h] BYREF
  char v10[216]; // [rsp+40h] [rbp+10h] BYREF
  void *v11; // [rsp+118h] [rbp+E8h]
  void *Block; // [rsp+138h] [rbp+108h]
  void *v13; // [rsp+158h] [rbp+128h]
  void *v14; // [rsp+178h] [rbp+148h]
  int v15; // [rsp+194h] [rbp+164h]
  int v16[8]; // [rsp+1B4h] [rbp+184h] BYREF
  int v17[16]; // [rsp+1D4h] [rbp+1A4h] BYREF
  int v18; // [rsp+214h] [rbp+1E4h]

  v5 = v9;
  for ( i = 190i64; i; --i )
  {
    *(_DWORD *)v5 = -858993460;
    v5 += 4;
  }
  sub_7FF6A2941A7D((__int64)&unk_7FF6A29C4006);
  v15 = 0;
  v16[0] = 0;
  v17[0] = 0;
  v18 = 16 - a4_64u % 16;
  v13 = malloc(v18 + a4_64u);                   // v13 = malloc(80)
  j_memcpy(v13, part_of_my_string, a4_64u);
  sub_7FF6A2941A28((__int64)v10, 1i64, a1, a2); // creat v10 byte stream
  v15 = 4 * (a4_64u / 16) + 4;                  // v15 = 20u
  v11 = malloc(4i64 * v15);                     // v11 = malloc(80)
  Block = (void *)memcpy_version2(v13, (unsigned int)a4_64u);// create Block by v13
  j_enc1((unsigned int)v10, (_DWORD)Block, a4_64u, (_DWORD)v11, (__int64)v16);// enc1
  j_enc2(v10, (char *)v11 + 4 * (v16[0] / 4), v17);// enc2
  v14 = (void *)sub_7FF6A29412A3(v11, (unsigned int)(v17[0] + v16[0]));// make v14 by v11
  j_memcpy(out_put_string, v14, v17[0] + v16[0]);
  free(Block);
  free(v14);
  free(v11);
  v7 = v17[0] + v16[0];
  sub_7FF6A29418FC((__int64)v9, (__int64)&unk_7FF6A29A6FE0);
  return v7;
}

上面的注释就是对这个 enc 的分析了。

他会创建一块 block 保存输入进的字符串,这个单看这个有点奇怪的代码其实不适合,动态调试比较方便,这也是为什么我上面的几个 x64_dbg 调试下面内存窗口名字都是这些变量的名字。

而且还自己实现了两个 memcpy 函数,我一开始差点看懵了。

可以看出前面几个函数基本都在初始化参数,在下面 enc1 和 enc2进行加密。

enc

在 enc1 中我们可以发现红色框框进行了一个判断,很奇怪,我一开始没注意,直到我跟进黄色框框才发现这个函数有多复杂,搜索后发现有一些操作和 AES 很像,但是很奇怪,AES 的 s-box,轮数,其实都对不上。我看这个 if else 好像有点互逆的样子,我试着把 block 中的字符串改成了输出,修改了这个标志位,再调用加密,结果很惊人。它恢复了 ‘a’*64 字符串。上点图。

aaa_before_enc

可见 v10 有一个标志位,然后加密 block 字符串。
经过两个加密程序后。

aaa_after_enc

把这一段 dump 下来当作 block 输入,再把 标志位改成 0(有时候就要试探一下)

aaa_before_enc

经过加密

aaa_after_enc

他居然变成了 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…. 哈哈哈哈哈,那一切迎刃而解。

#exp.py
tables = [
[109, 183, 25, 125, 100, 181, 54, 232, 77, 118, 65, 108, 51, 10, 173, 22, 95, 123, 36, 86, 236, 124, 82, 218, 167, 24, 83, 164, 140, 18, 253, 241, 62, 217, 247, 52, 230, 212, 177, 122, 67, 23, 182, 127, 145, 206, 215, 222, 66, 96, 1, 198, 146, 194, 40, 35, 55, 117, 120, 188, 136, 172, 134, 202, 152, 71, 114, 249, 37, 227, 72, 41, 15, 98, 126, 29, 9, 78, 99, 11, 186, 254, 32, 160, 231, 43, 93, 156, 169, 246, 251, 68, 33, 192, 228, 211, 150, 143, 113, 161, 81, 104, 248, 237, 151, 193, 190, 175, 238, 158, 57, 101, 7, 53, 233, 102, 142, 107, 69, 178, 187, 184, 256, 31, 163, 13, 61, 171, 56, 226, 245, 135, 210, 8, 91, 201, 234, 208, 27, 19, 85, 133, 75, 74, 141, 132, 244, 224, 203, 176, 220, 70, 195, 144, 46, 28, 155, 58, 5, 168, 128, 219, 209, 110, 148, 48, 45, 106, 250, 139, 229, 138, 225, 204, 154, 49, 112, 64, 131, 39, 21, 88, 213, 189, 103, 6, 38, 111, 223, 59, 239, 16, 119, 165, 199, 185, 242, 252, 147, 166, 4, 14, 235, 179, 196, 121, 30, 17, 20, 3, 97, 50, 47, 170, 12, 115, 90, 191, 162, 26, 89, 174, 105, 92, 129, 73, 157, 240, 153, 44, 207, 255, 180, 42, 76, 200, 197, 205, 243, 80, 214, 84, 137, 221, 63, 116, 79, 130, 149, 60, 159, 87, 34, 2, 216, 94]
, 
[37, 136, 231, 74, 42, 48, 8, 100, 233, 80, 214, 127, 170, 129, 197, 180, 140, 181, 200, 210, 61, 17, 1, 12, 51, 119, 201, 81, 46, 124, 183, 169, 87, 151, 117, 105, 145, 131, 222, 240, 90, 63, 21, 4, 203, 52, 192, 221, 19, 171, 23, 135, 134, 92, 32, 88, 24, 235, 91, 115, 103, 85, 20, 165, 245, 241, 149, 218, 208, 168, 177, 193, 26, 70, 172, 27, 97, 14, 78, 146, 250, 189, 86, 35, 243, 141, 254, 161, 237, 186, 16, 53, 154, 9, 163, 144, 69, 187, 82, 182, 2, 71, 33, 229, 79, 123, 162, 99, 25, 256, 7, 185, 102, 59, 107, 219, 220, 184, 196, 50, 3, 22, 10, 30, 155, 158, 122, 213, 98, 58, 232, 195, 173, 194, 94, 143, 120, 148, 178, 153, 152, 121, 157, 242, 207, 167, 199, 108, 49, 191, 223, 204, 198, 68, 249, 45, 29, 227, 84, 60, 73, 188, 96, 44, 36, 111, 39, 64, 190, 236, 137, 128, 175, 244, 234, 147, 126, 150, 101, 72, 93, 205, 179, 133, 224, 38, 251, 67, 209, 247, 212, 40, 125, 116, 160, 114, 230, 89, 202, 66, 109, 216, 95, 255, 62, 18, 113, 6, 206, 215, 28, 226, 106, 110, 15, 118, 228, 54, 56, 43, 55, 156, 132, 238, 248, 65, 138, 34, 57, 130, 5, 139, 252, 112, 13, 47, 75, 104, 159, 217, 11, 77, 83, 76, 31, 239, 166, 174, 142, 164, 41, 246, 176, 253, 225, 211]
, 
[203, 220, 180, 210, 96, 164, 234, 226, 145, 215, 22, 229, 132, 26, 149, 11, 128, 179, 107, 7, 17, 161, 160, 100, 129, 72, 130, 186, 55, 118, 162, 5, 20, 156, 21, 250, 237, 106, 104, 64, 75, 166, 206, 249, 200, 144, 44, 137, 63, 90, 241, 111, 34, 52, 169, 36, 78, 175, 173, 54, 218, 212, 93, 80, 189, 33, 153, 49, 236, 45, 113, 123, 58, 231, 81, 79, 138, 155, 112, 196, 253, 245, 108, 66, 122, 135, 163, 223, 165, 126, 157, 51, 67, 114, 23, 59, 91, 152, 239, 131, 194, 115, 42, 205, 188, 101, 191, 98, 227, 48, 242, 89, 201, 14, 225, 202, 251, 85, 102, 68, 99, 133, 230, 240, 222, 256, 57, 124, 140, 195, 142, 38, 110, 143, 15, 65, 87, 39, 24, 83, 8, 121, 117, 184, 199, 77, 159, 148, 181, 94, 88, 238, 248, 216, 69, 62, 46, 3, 176, 27, 185, 198, 9, 183, 228, 244, 224, 16, 43, 255, 29, 232, 40, 35, 13, 136, 150, 207, 56, 154, 61, 214, 120, 213, 25, 177, 74, 167, 109, 141, 151, 10, 71, 172, 73, 197, 6, 174, 53, 233, 146, 190, 235, 147, 76, 170, 50, 247, 47, 211, 19, 254, 82, 18, 97, 116, 41, 171, 95, 221, 158, 178, 209, 30, 103, 1, 192, 2, 208, 217, 12, 84, 134, 31, 204, 105, 28, 243, 127, 119, 125, 139, 92, 182, 32, 246, 60, 252, 219, 193, 86, 168, 187, 70, 4, 37]
]
answer = [91, 38, 6, 154, 6, 103, 145, 181, 35, 244, 97, 219, 99, 95, 68, 35, 32, 189, 197, 208, 182, 128, 242, 98, 66, 176, 190, 128, 98, 236, 157, 189, 200, 101, 50, 197, 176, 103, 27, 193, 244, 53, 249, 247, 4, 243, 116, 146, 122, 171, 101, 37, 56, 227, 202, 227, 67, 207, 66, 182, 174, 187, 87, 157, 207, 11, 250, 103, 233, 69, 23, 60, 13, 74, 79, 88, 223, 188, 234, 79]
flag = ['\x00']*80

for i in range(80):
    flag[79-i] = tables[0].index(tables[1].index(tables[2].index(answer[79-i])+1)+1)+1-1

for i in flag:
    print("%02x "%(i),end="")

这一段输出已经加密的比特流。

08 bc 48 bf 48 e7 ff 28 66 22 84 9c 74 f5 f2 66 0a 81 c0 65 dd 0f c7 a4 fa be c2 0f a4 d2 bd 81 57 b6 2c c0 be e7 98 64 22 c6 1b 76 ad 93 3f 8a 20 5b b6 a3 01 87 35 87 05 a9 fa dd e4 51 7f bd a9 86 c1 e7 8b 03 94 6a 0e 49 92 fc 80 12 bb 92  

然后再复制到 block 中 修改标志位,再解密!

result

result

大胜利,好耶。

真的学些了好多,尤其是 lua 的源代码,可读性超强。

不过有一些还不明白,怎么做到把这个 lua 虚拟机移植到程序内的,这个很有意思。应该还有别的做法,比如 hook 函数什么的,不过我还不清楚那些,或者说遇到了我也用别的方法解决了。

不过最后最后,我感觉最重要的应该还是怎么制作出这个程序,真的很惊艳。太妙了,一道题让我学习了一门语言外加一些骚操作。