梦幻手游部分Luac反编译失败的解决方法

6k 词

  这一篇是去年学习破解梦幻西游手游lua代码时记录的一些问题,今天将其整理并共享出来,所以不一定适合现在版本的梦幻手游,大家还是以参考为目的呗。lua相关的文章(共4篇)到此也写完了,如果以后还有新的东西会继续更新,接下来会写几篇关于2018 腾讯游戏安全竞赛的详细分析,敬请期待。

十二处bug修复

  当时反编译梦幻西游手游时遇到的问题大约有12个,修改完基本上可以完美复现lua源码,这里用的luadec5.1版本。

修复一

  问题1: 由于梦幻手游lua的opcode是被修改过的,之前的解决方案是找到梦幻的opcode,替换掉反编译工具的原opcode,并且修改opmode,再进行反编译。问题是部分测试的结果是可以的,但是当对整个手游的luac字节码反编译时,会出现各种错误,原因是luadec5.1 在很多地方都默认了opcode的顺序,并进行了特殊处理,所以需要找到这些特殊处理的地方一一修改。不过这样很麻烦,从而想到另外一种方式,不修改原来的opcode和opmode,而是在luadec解析到字节码的时候,将opcode还原成原来的opcode。

  解决1: 定位到解析code的位置在 lundump.c –> LoadFunction –> LoadCode (位置不唯一,可以看上一篇腾讯比赛的修复),当执行完LoadCode函数的时候,f变量则指向了code的结构,在这之后执行自己写的函数ConvertCode函数,如下:

1
2
3
4
5
6
7
8
9
10
11
12

void (Proto *f)
{
int pnOpTbl[] = { 3,13,18,36,27,10,20,25,34,2,32,15,30,16,31,9,26,24,29,1,6,28,4,17,33,0,7,11,5,14,8,19,35,12,21,22,23,37 };
for (int pc = 0; pc < f->sizecode; pc++)
{
Instruction i = f->code[pc];
OpCode o = GET_OPCODE(i);
SET_OPCODE(i, pnOpTbl[o]);
f->code[pc] = i;
}
}

修复二

  问题2: 在文件头部 反编译出现错误 – DECOMPILER ERROR: Overwrote pending register.

  解决2: 分析发现,原来是解析OP_VARARG错误导致的。OP_VARARG主要的作用是复制B-1个参数到A寄存器中,而反编译工具复制了B个参数,多了一个。修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...
case OP_VARARG: // Lua5.1 specific.
{
int i;
/*
* Read ... into register.
*/
if (b==0) {
TRY(Assign(F, REGISTER(a), "...", a, 0, 1));
} else {

// for(i = 0;i<b;i++) {
for(i = 0; i < b-1; i++) {
TRY(Assign(F, REGISTER(a+i), "...", a+i, 0, 1));
}
}
break;
}
...

修复三

  问题3: 在解析table出现反编译错误 – DECOMPILER ERROR: Confused about usage of 。registers!

  解决3: 分析发现,这里的OP_NEWTABLE 的c参数表示hash table中key的大小,而反编译代码中将c参数进行了错误转换,导致解析错误,修改代码如下:

1
2
3

//#define fb2int(x) (((x) & 7) << ((x) >> 3))

修复四

  问题4: 反编译工具出错并且退出。

  解决4: 跟踪发现是在AddToTable函数中,当keyed为0时会调用PrintTable,而PrintTable释放了table,下次再调用table时内存访问失败,修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void AddToTable(Function* F, DecTable * tbl, char *value, char *key)
{
DecTableItem *item;
List *type;
int index;
if (key == NULL) {
type = &(tbl->numeric);
index = tbl->topNumeric;
tbl->topNumeric++;
} else {
type = &(tbl->keyed);
tbl->used++;
index = 0;
}
item = NewTableItem(value, index, key);
AddToList(type, (ListItem *) item);
// FIXME: should work with arrays, too

// if(tbl->keyedSize == tbl->used && tbl->arraySize == 0){
if (tbl->keyedSize != 0 && tbl->keyedSize == tbl->used && tbl->arraySize == 0) {
PrintTable(F, tbl->reg, 0);
if (error)
return;
}
}

修复五

   问题5: 当函数是多值返回结果并且赋值于多个变量时反编译错误,情况如下(lua反汇编):

1
2
3
4
5
6
7
8
21 [-]: GETGLOBAL R0 K9        ; R0 := memoryStatMap
22 [-]: GETGLOBAL R1 K9 ; R1 := memoryStatMap
23 [-]: GETGLOBAL R2 K2 ; R2 := preload
24 [-]: GETTABLE R2 R2 K3 ; R2 := R2["utils"]
25 [-]: GETTABLE R2 R2 K16 ; R2 := R2["getCocosStat"]
26 [-]: CALL R2 1 3 ; R2,R3 := R2()
27 [-]: SETTABLE R1 K15 R3 ; R1["cocosTextureBytes"] := R3
28 [-]: SETTABLE R0 K14 R2 ; R0["cocosTextureCnt"] := R2

  当上面的代码解析到27行时,从寄存器去取R3时报错,原因是前面的call返回多值时,只是在F->Rcall中进行了标记,没有在寄存器中标记,编译的结果应该为:

1
memoryStatMap.cocosTextureCnt, memoryStatMap.cocosTextureBytes = preload.utils.getCocosStat()

   解决5: 当reg为空时并且Rcall不为空,增加一个return more的标记,修改2个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
char *RegisterOrConstant(Function * F, int r)
{
if (IS_CONSTANT(r)) {
return DecompileConstant(F->f, r - 256); // TODO: Lua5.1 specific. Should change to MSR!!!
} else {
char *copy;
char *reg = GetR(F, r);
if (error)
return NULL;


// if(){}
if (reg == NULL && F->Rcall[r] != 0)
{
reg = "return more";
}

copy = malloc(strlen(reg) + 1);
strcpy(copy, reg);
return copy;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void OutputAssignments(Function * F)
{
int i, srcs, size;
StringBuffer *vars;
StringBuffer *exps;
if (!SET_IS_EMPTY(F->tpend))
return;
vars = StringBuffer_new(NULL);
exps = StringBuffer_new(NULL);
size = SET_CTR(F->vpend);
srcs = 0;
for (i = 0; i < size; i++) {
int r = F->vpend->regs[i];
if (!(r == -1 || PENDING(r))) {
SET_ERROR(F,"Attempted to generate an assignment, but got confused about usage of registers");
return;
}

if (i > 0)
StringBuffer_prepend(vars, ", ");
StringBuffer_prepend(vars, F->vpend->dests[i]);

if (F->vpend->srcs[i] && (srcs > 0 || (srcs == 0 && strcmp(F->vpend->srcs[i], "nil") != 0) || i == size-1)) {

// if()
if (strcmp(F->vpend->srcs[i], "return more") != 0)
{
if (srcs > 0)
StringBuffer_prepend(exps, ", ");
StringBuffer_prepend(exps, F->vpend->srcs[i]);
srcs++;
}
}

}
...
}

修复六

  问题6: 当函数只有一个renturn的时候会反编译错误。

  解决6:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 case OP_RETURN:
{
...

// 新增的if
if (pc != 0)
{
for (i = a; i < limit; i++) {
char* istr;
if (i > a)
StringBuffer_add(str, ", ");
istr = GetR(F, i);
TRY(StringBuffer_add(str, istr));
}
TRY(AddStatement(F, str));
}
break;
}

修复七

  问题7: 部分table初始化会出错。

  解决7:

1
2
3
4
5
6
7
8
9
10
char *GetR(Function * F, int r)
{
if (IS_TABLE(r)) {

return "{ }";
// PrintTable(F, r, 0);
// if (error) return NULL;
}
...
}

修复八

  问题8: 可变参数部分解析出错,但是工具反编译时是不报错误的。

  解决8: is_vararg为7时,F->freeLocal多加了一次:

1
2
3
4
5
6
7
8
9
10

if (f->is_vararg==7) {
TRY(DeclareVariable(F, "arg", F->freeLocal));
F->freeLocal++;
}

// 修改if为else if
else if ((f->is_vararg&2) && (functionnum!=0)) {
F->freeLocal++;
}

修复九

  问题9: 反编译工具输出的中文为url类型的字符(类似 “230176148231150151230156175”),不是中文。

  解决9: 在proto.c文件中的DecompileString函数中,注释掉default 转换字符串的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
char *DecompileString(const Proto * f, int n)
{
...
default:
//add by littleNA
// if (*s < 32 || *s > 127) {
// char* pos = &(ret[p]);
// sprintf(pos, "\%d", *s);
// p += strlen(pos);
// } else {
ret[p++] = *s;
// }
break;
...
}

  然后再下面3处增加判断的约束条件,因为中文字符的话,char字节是负数,这样isalpha和isalnum函数就会出错,所以增加约束条件,小于等于127:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void MakeIndex(Function * F, StringBuffer * str, char* rstr, int self)
{
...
int dot = 0;
/*
* see if index can be expressed without quotes
*/
if (rstr[0] == '"') {


// (unsigned char)(rstr[1]) <= 127 &&
if ((unsigned char)(rstr[1]) <= 127 && isalpha(rstr[1]) || rstr[1] == '_') {
char *at = rstr + 1;
dot = 1;
while (*at != '"') {

// add by littleNA
// *(unsigned char*)at <= 127 &&
if (*(unsigned char*)at <= 127 && !isalnum(*at) && *at != '_') {
dot = 0;
break;
}
at++;
}
}
}
....
}


...
case OP_TAILCALL:
{
// add by littleNA
// (unsigned char)(*at) <= 127 &&
while (at > astr && ((unsigned char)(*at) <= 127 && isalpha(*at) || *at == '_')) {
at--;
}
}
...

修复十

  问题10: 反汇编失败。因为一些文件中含有很长的字符串,导致sprintf函数调用失败。

  解决10: 增加缓存的大小:

1
2
3
4
5
6
7
void luaU_disassemble(const Proto* fwork,