HGAME-FINAL-RE
hgame_final by RogerThat
对于这道题目有好两个思路可以进行,其一是直接进入程序,在逆向过程中会遇到一个接管几乎全部程序的 switch 结构。很典型的直冲直撞逆向手(在没有 hint 的情况下)。
在我这边的环境下这个函数在 sub_7FF6A2994F44
可以看出,这个代码很难看懂,但是可以看出他的分支有 46 种,一般出现这么长的 switch 结构是很有问题的,而且根据我的分析,调用不同的 switch 他会调用函数等等,输出出
Welcome to HGAME final!
what is your flag?
在一开始不知道这个和 lua 的情况下焦头烂额,因为想分析这个虚拟机难度非常高,后来我也只是在源码的帮助下才勉强理解。一开始通过 x64_dbg 自带的条件断点,我让他输出出了 siwtch 的分支。
具体的 OPCODE 我就不展示了,很长很长,而且大部分都是 case 1。
但是当我知道了它和 lua 有关,那就不一样了,内置的虚拟机必然会去执行 lua 代码,或者 lua 2 luac.out 的字节码。这个代码其实就是在 pe 文件里面,其实 mezone 给的 out 文件多了 debug信息的,反而更容易知道逻辑。
关于我 lua 学习整个过程,我丢在了 mind_strom.md 中了,还挺有纪念价值。
后面还有好多,而且没有 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 就先不妨这了,太长了,我放在一个压缩包里面好了。
不是很好阅读,毕竟没有 “符号文件” “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 中跟踪这个东西。
在此处 GETTABLE 下一个断在 OP_CALL 使得这个真正加载完。
在标蓝色的地方下一个 1byte 硬件写入断点,运行。(我这里写了加硬件写入断点,但是刚才调试的时候忘记加了。。又得重来一遍)
走出这个 dll 代码,肯定是哪里调用了什么修改这个字符串的函数。
发现这个函数在
回到 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进行加密。
在 enc1 中我们可以发现红色框框进行了一个判断,很奇怪,我一开始没注意,直到我跟进黄色框框才发现这个函数有多复杂,搜索后发现有一些操作和 AES 很像,但是很奇怪,AES 的 s-box,轮数,其实都对不上。我看这个 if else 好像有点互逆的样子,我试着把 block 中的字符串改成了输出,修改了这个标志位,再调用加密,结果很惊人。它恢复了 ‘a’*64 字符串。上点图。
可见 v10 有一个标志位,然后加密 block 字符串。
经过两个加密程序后。
把这一段 dump 下来当作 block 输入,再把 标志位改成 0(有时候就要试探一下)
经过加密
他居然变成了 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
大胜利,好耶。
真的学些了好多,尤其是 lua 的源代码,可读性超强。
不过有一些还不明白,怎么做到把这个 lua 虚拟机移植到程序内的,这个很有意思。应该还有别的做法,比如 hook 函数什么的,不过我还不清楚那些,或者说遇到了我也用别的方法解决了。
不过最后最后,我感觉最重要的应该还是怎么制作出这个程序,真的很惊艳。太妙了,一道题让我学习了一门语言外加一些骚操作。