记一次luac分析

前言

某mips架构的路由器上的lua脚本a.lua,被编译成了luac二进制格式。初步分析发现指定lua使用固件中的共享库liblua.so.5.1.5可运行该luac文件,同架构的官方openwrt则不能运行该luac,所以判断出lialua.so.5.1.5被魔改了。

1
2
3
4
5
6
7
8
9
10
11
[email protected]****:~# export LD_LIBRARY_PATH=./
[email protected]****:~# lua ./a.luac
lua: module '****' not found:
....
stack traceback:
[C]: in function 'require'
?: in main chunk
[C]: ?
[email protected]****:~# unset LD_LIBRARY_PATH
[email protected]****:~# lua ./a.luac
lua: ./a.luac: bad code in precompiled chunk

静态分析

ida静态分析一下修改后的liblua.so.5.1.5,对照lua源码,发现LOADK字节码的名称字符串不见了。

GDB调试

静态太难分析,难以找到哪些地方被修改,还是在x86上编译一个32位带调试符号的lua加载a.luac吧。。首先根据这篇文章,把openwrt对lua修改的patch打上。

使用make MYCFLAGS='-fPIC -m32 -g3' MYLDFLAGS='-m32' PKG_VERSION='5.1.5' linux出错,在链接时(gcc -o lua -L. -llua -m32 lua.o -lm -Wl,-E -ldl),报错符号未定义lua.c:(.text+0x20): undefined reference to 'lua_sethook'。看了下目标文件都是32位的,怎么会找不到符号?没时间搞,看了这个repo,把目标文件lua.o移到-L指定库路径前(草?),也就是Makefile生成lua和luac的规则改成:

1
2
3
4
5
6
7
8
$(LUA_T): $(LUA_O) $(LUA_SO)
$(CC) -o [email protected] $(LUA_O) -L. -llua $(MYLDFLAGS) $(LIBS)

$(LUAC_T): $(LUAC_O) $(LUA_SO)
$(CC) -o [email protected] $(LUAC_O) -L. -llua $(MYLDFLAGS) $(LIBS)

$(LUAC_T)-host: $(LUAC_O) $(LUA_A)
$(CC) -o [email protected] $(LUAC_O) $(MYLDFLAGS) $(LUA_A) $(LIBS)

就可以编译32位带调试符号的lua了。

然后gdb挂上去,断点打在error,看下是怎么回事。

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
gdb-peda$ bt
#0 error (why=0xf7fb712d "bad code", [email protected]=0xeff4b12d <error: Cannot access memory at address 0xeff4b12d>,
S=<optimized out>, S=<optimized out>) at lundump.c:39
#1 0xf7fa7773 in LoadFunction ([email protected]=0xffffaec0, p=<optimized out>) at lundump.c:230
#2 0xf7fa746b in LoadConstants (f=0x56562660, S=0xffffaec0) at lundump.c:187
#3 LoadFunction ([email protected]=0xffffaec0, p=<optimized out>) at lundump.c:228
#4 0xf7fa78a3 in luaU_undump (L=0x5655b1a0, Z=0xffffb0d8, buff=0xffffb09c, name=0x565614d0 "@../../../agent.lua")
at lundump.c:262
#5 0xf7f9e1de in f_parser (L=0x5655b1a0, ud=0xffffb098) at ldo.c:498
#6 0xf7f9ddc8 in luaD_rawrunprotected (L=0x5655b1a0, f=0xf7f9e180 <f_parser>, ud=0xffffb098) at ldo.c:116
#7 0xf7f9ec4d in luaD_pcall (L=0x5655b1a0, func=0xf7f9e180 <f_parser>, u=0xffffb098, old_top=0x30, ef=0x0) at ldo.c:464
#8 0xf7f9ed51 in luaD_protectedparser (L=0x5655b1a0, z=0xffffb0d8, name=0x565614d0 "@../../../agent.lua") at ldo.c:514
#9 0xf7f9a542 in lua_load (L=0x5655b1a0, reader=0xf7faaf90 <getF>, data=0xffffb134,
chunkname=0x565614d0 "@../../../agent.lua") at lapi.c:929
#10 0xf7fac224 in luaL_loadfile (L=0x5655b1a0, filename=0xffffd63e "../../../agent.lua") at lauxlib.c:591
#11 0x56557432 in handle_script (n=<optimized out>, argv=<optimized out>, L=<optimized out>) at lua.c:247
#12 pmain (L=<optimized out>) at lua.c:362
#13 0xf7f9e53d in luaD_precall (L=<optimized out>, func=<optimized out>, nresults=0x0) at ldo.c:320
#14 0xf7f9ea56 in luaD_call (L=0x5655b1a0, func=0x5655b3ec, nResults=0x0) at ldo.c:377
#15 0xf7f98ded in f_Ccall (L=0x5655b1a0, ud=0xffffd394) at lapi.c:906
#16 0xf7f9ddc8 in luaD_rawrunprotected (L=0x5655b1a0, f=0xf7f98d90 <f_Ccall>, ud=0xffffd394) at ldo.c:116
#17 0xf7f9ec4d in luaD_pcall (L=0x5655b1a0, func=0xf7f98d90 <f_Ccall>, u=0xffffd394, old_top=0xc, ef=0x0) at ldo.c:464
#18 0xf7f9a4ce in lua_cpcall (L=0x5655b1a0, func=0x56556f80 <pmain>, ud=0xffffd3d0) at lapi.c:916
#19 0x5655662e in main (argc=0x2, argv=0xffffd4a4) at lua.c:396
#20 0xf7caeed5 in __libc_start_main () from /lib32/libc.so.6
#21 0x565566b5 in _start () at lua.c:383

还是难以判断,看起来是lua操作码(字节码)无法识别,那去openwrt上opkg安装一下luac吧,指定不同动态库(一个原版,一个魔改),编译同一个.lua成.luac文件,这里用的是luadec里的allopcodes-5.1.lua,尽可能的在二进制里包含所有40个OpCode,然后直接010对比allopcodes.luac。

二进制对比

可以看到,有那么几个字节不同,这里的字节&0x3F就能得到OpCode,很多字节都比原版的少了1,再结合ida静态分析,LOADK操作码名消失了。可以合理猜测删除了LOADK操作码名称字符串(在OpName数组),但这无法解释为什么一部分字节往前移了1(操作码名称字符串删除不代表LOADK操作码被删了,如果删了LOADK,lua虚拟机应该无法正常运行和解释字节码),所以我写了个脚本把不同的字节比对了一遍:

1
2
3
4
5
6
7
8
9
10
OpCode = ["MOVE", "LOADK", "LOADBOOL", "LOADNIL", "GETUPVAL", "GETGLOBAL", "GETTABLE", "SETGLOBAL", "SETUPVAL", "SETTABLE", "NEWTABLE", "SELF", "ADD", "SUB", "MUL", "DIV", "MOD", "POW",
"UNM", "NOT", "LEN", "CONCAT", "JMP", "EQ", "LT", "LE", "TEST", "TESTSET", "CALL", "TAILCALL", "RETURN", "FORLOOP", "FORPREP", "TFORLOOP", "SETLIST", "CLOSE", "CLOSURE", "VARARG", ]

mod = [0xc6,0x14,0x41,0x82,0xc3,0x04,0xc5,0x06,0x47,0x88,0x09,0x08,0x4a,0x8b,0x8c,0x8d,0x8e,0x8f,0x90,0x91,0xd2,0xd3,0x54,0x94,0xd4,0x44,0x44,0x84,0x44,0x03,0x43]

ori = [0xc7,0x01,0x42,0x83,0xc4,0x05,0xc6,0x07,0x48,0x89,0x0a,0x09,0x4b,0x8c,0x8d,0x8e,0x8f,0x90,0x91,0x92,0xd3,0xd4,0x41,0x81,0xc1,0x45,0x45,0x85,0x45,0x04,0x44]

print('ori\tmod\tOpCode')
for i in range(len(mod)):
print(ori[i]&0x3f,'\t', mod[i]&0x3f,'\t', OpCode[ori[i]&0x3f])
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
原版    修改版      OpCode
ori mod OpCode
7 6 SETGLOBAL
1 20 LOADK
2 1 LOADBOOL
3 2 LOADNIL
4 3 GETUPVAL
5 4 GETGLOBAL
6 5 GETTABLE
7 6 SETGLOBAL
8 7 SETUPVAL
9 8 SETTABLE
10 9 NEWTABLE
9 8 SETTABLE
11 10 SELF
12 11 ADD
13 12 SUB
14 13 MUL
15 14 DIV
16 15 MOD
17 16 POW
18 17 UNM
19 18 NOT
20 19 LEN
1 20 LOADK
1 20 LOADK
1 20 LOADK
5 4 GETGLOBAL
5 4 GETGLOBAL
5 4 GETGLOBAL
5 4 GETGLOBAL
4 3 GETUPVAL
4 3 GETUPVAL

一目了然,OpName删除了LOADK,所以ida里面无这个字符串,然后调整了OpCode顺序,LOADK被移到20去了,所以1-20全部前移1,之所以要删掉LOADK的OpName,是因为如果OpName和OpCode保持一致的话很容易被人看出来。

因此修改lopcodes.h里的OpCode枚举,lua开发者还温馨的提示要修改两个地方:

1
** grep "ORDER OP" if you change these enums

也就是说OpName也需要调换顺序,我尝试了OpName数组直接删除”LOADK”,然后lua就无法运行了,报错./luac: error while loading shared libraries: liblua.so.5.1.5: wrong ELF class: ELFCLASS64,但为啥他们的so里面没有”LOADK”这个字符串?先不管了。

接着全局搜索LOADK,全部调换顺序,修改后重新编译lua,luadec就可以用了。。。

总结

最后得出结论,这种简单的调换OpCode顺序还是没啥用。收工。

Author

lyq1996

Posted on

2022-03-19

Updated on

2022-09-09

Licensed under

Comments