目录
●
前言 一 跟踪,脱壳,解包,解密 二 标准化 luac 三 反汇编,阅读,指令级修改 四 反编译,函数级修改 五 luac 函数替换
前言
当今的手游,十个有九个是 lua 做的,其中七个是 coco2d-x 的。很多其他程序嵌入的脚本也直接用了 lua。为了保密,资源文件中 lua 脚本基本都是已加密的字节码文件,还有许多是非标准字节码。此时破解便是难题,本文便为此提供一些破解思路。
本文适用于 lua5.1,大部分适用于 5.0 5.2 (部分指令与格式细节除外),对 luajit 1.x 和 2.x 不太适用。
目前最广泛使用的也应该是 lua5.1 版本。coco2d-x 用的也是 5.1的语法,可选官方版本和luajit版本虚拟机。
程序编译时的处理流程如下:
lua源文件--obfuscate-->lua源文件(混淆后)--compile-->luac文件(带调试用的变量名和行号)--strip-->luac文件(不带调试信息)
lua/luac--加密/打包-->资源文件
obfuscate/混淆:变量名函数名替换,不可还原,只能算小困难,而且目前用的人不多。
compile/编译:lua源文件编译为字节码文件,此文以后称之为 luac文件。而且很多是非标准的字节码格式。即使是标准的字节码,也只能保证反汇编成功,反编译技术不够成熟。基本上所有用 lua 的手游都做了这一步。
strip:去除调试信息,删除所有 local upval 名,作用范围,调试用的指令对应源文件行号。对反编译是巨大的障碍。目前用的人也不多。
程序运行时流程很简单:
资源文件--解包/解密-->lua/luac-->lua虚拟机
用到的源码与工具:
lua-5.1 源码
ChunkSpy
LuaAssemblyTools
luadec51
一 跟踪,解包,解密
目的是得到 luac文件,不管是不是标准格式。
这里我只能提供思路。我见过 简单的异或加密,密码加密zip,AES加密。
追 luaL_loadbuffer , luaL_loadfile , lua_load 这三个函数,应该可以找到解密函数。
hook 这3个函数,可以直接导出 luac 文件。如果没编译甚至是 lua源文件。
hook luaV_execute 可以拿到 Proto* ,如果能用 luaU_dump 导出的话,就是 luac文件。
hook 抓数据效率比较低,还很难所有脚本都被执行。最好是找到解密函数,然后自己做解包程序,甚至打包程序。
例如:
aes加密,gdb内存抓key
Angry Birds Rio encrypts settings/highscores with AES
xor加密
【求助】某热门游戏 android so部分解密算法
hook luaV_execute
【原创】用LuaDec逆回LUA脚本内容
二 标准化 luac
目的是将非标准 luac 转化为标准格式。
有两种方案来处理得到的 luac
得到一个能够执行上述非标 luac 的 lua 版本 找到 lua51.dll liblua51.so 之类,编程直接调用。只能用于执行几个关键lua函数。或者导出 luac 对比文件格式。
改 lua 源文件,编译出一个。这样做什么都可以,还能编译出对应的 luadec 。
这就必须要搞清楚非标 lua 是如何修改的。
将非标 luac 转换成标准格式 luac 就可以用标准 lua 工具了,反汇编,汇编,反编译,都能用。
常用 lua 修改方法
#define LUA_SIGNATURE "\033Lua"
改数字类型 改后的luac文件,用标准 lua 程序载入时显示“ bad header ”
lua 只有一种数字类型,可以在编译时更改。默认是 8位 double, 一般可以改成 4位 float, 4位 int 和 8位 long long
chunkspy 程序对此方法基本免疫。
例如:前面提到的Angry Birds Rio,数字类型就改成 float了
在 luaconf.h 中修改
#define LUA_NUMBER_DOUBLE
#define LUA_NUMBER double
其他的一些格式化和转化用函数最好也修改。例如,改 float 时
LUA_NUMBER to float
LUA_NUMBER_SCAN to "%f"
LUA_NUMBER_FMT to "%.7g"
l_mathop(x) to (x##f)
lua_str2number to use strtof
OpCode 重定义 用标准 lua 程序载入时显示“ bad code ”
破解非常麻烦
例如:
简单交换4跳指令
记一款宽带拨号器加密算法逆向实例
某雷 B**T ui 基本每条指令都改了
J*2+1 早期版本用了 cnumber 补丁,加了2条指令,结果导致指令非标准
改 lopcodes.h 和 lopcodes.c 的这些地方,可以调换指令的顺序,也可以插入几个没用的占位指令,注意指令最多 64 个
// 改有 "ORDER OP" 的地方
/* grep "ORDER OP" if you change these enums */
enum OpCode; // lopcodes.h
const lu_byte luaP_opmodes[]; // lopcodes.c
const char *const luaP_opnames[]; // lopcodes.c
那么,怎么知道非标 lua 是如何修改的呢?
一是观察法,二进制编辑器打开一个luac文件,结合 lua 源代码人工分析。
二是对比法,对比下面的 lua 源文件,allopcodes.lua,在标准lua编译和非标lua编译后的差别。
local u1,u2,u3
function f1(a1,a2,...)
local l0 = a1; -- move
local l1 = 1 -- loadk
local l2 = true -- loadbool
local l3 = nil -- loadnil
local l4 = u1[g1] -- gettupval, getglobal, gettable
g1 = l1 -- setglobal
u2 = l2 -- setupval
l3[l2] = l1 -- settable
local l5 = { -- newtable
l1, l2; -- move, setlist
x = l2 -- settable
}
local l6 = l5:x() -- self, call
local l7 = -((l0+l1-l2)*l3/l4%l5)^l6 -- add, sub, mul, div, mod, pow, unm
local l8 = #(not l7) -- not, len
local l9 = l7..l8 -- concat
if l1==l2 and l2<l3 or l3<=l4 then -- eq, lt, le, jmp
for i = 1, 10, 2 do -- forprep
l0 = l0 and l2 -- test
end -- forloop
else -- jmp
for k,v in ipairs(l5) do
l4 = l5 or l6 -- testset
end -- tforloop
end
do
local l21, l22 = ... -- vararg
local function f2() -- closure
return l21, l22
end
f2(k,v) -- call
end --close
return f1() -- return, tailcall
end
如何让被破解程序中的lua载入上面的程序,并导出呢?
1. 如果你有办法让程序执行下面的 C 代码的话,参考 lua 源代码里面的 luac.c 写入文件,或者 lstrlib.c 的 str_dump 写入内存
lua_State* L = luaL_newstate();
luaL_loadbuffer or luaL_loadstring or luaL_loadfile
Closure* c=(Closure*)lua_topointer(L, -1);
Proto* f = c->l.p;//Closure如果改了偏移会不同
luaU_dump(L, f, writer, D, 0);//非导出函数, string.dump
lua_close(L);
2. 让程序执行 lua 代码 导出到文件或者string
可以 hook luaL_loadbuffer 然后偷天换日
-- luaopen_string 此库必须打开
allopcodes() allopcodes.lua文件内容 end
chunk = string.dump(allopcodes)
if luaopen_io then --如果 io 可用,则可以直接写文件
file:write(chunk)
else
output(chunk,to_any_where) --想办法把 chunk 搞出来,直接读内存也行
return chunk
end
下面是一段很长的 luac 标准格式分析,列出修改 lua虚拟机后可能变化的地方。用 chunkspy 生成,加了中文注释
Pos Hex Data Description or Code
------------------------------------------------------------------------
0000 ** source chunk: localetest.lua
** global header start **
0000 1B4C7561 header signature: "\27Lua" 改文件头就是改这里 #define LUA_SIGNATURE "\033Lua"
0004 51 version (major:minor hex digits) 版本号,显眼的 LuaQ,5.2就是 LuaR
0005 00 format (0=official)
0006 01 endianness (1=little endian) 这4个字节应该不会改,可以作为定位依据
0007 04 size of int (bytes) 如果改了这边,就是放弃了完整的校验
0008 04 size of size_t (bytes)
0009 04 size of Instruction (bytes)
000A 08 size of number (bytes) 改数字类型就是这两个字节,double 0800, float 0400, int 0401, longlong 0801
000B 00 integral (1=integral) 是否整数
* number type: double
* x86 standard (32-bit, little endian, doubles)
** global header end **
000C ** function [0] definition (level 1) 顶层有且只有一个函数
** start of function **
000C 0F000000 string size (15) 函数对应文件名,标准字符串格式,前4个字节是长度,可以看出是大头big endian的
0010 6C6F63616C657465+ "localete" 虽然 lua 字符串可以包含 \0 也不要求以 \0 结尾
0018 73742E6C756100 "st.lua\0" 但 dump 时会加一个 \0结尾, load时会忽略
source name: localetest.lua
001F 00000000 line defined (0)
0023 00000000 last line defined (0)
0027 00 nups (0)
0028 00 numparams (0)
0029 02 is_vararg (2)
002A 02 maxstacksize (2)
* code: 指令区,当做过 OpCode 重定义 后,chunkspy 很可能在这里失败
002B 03000000 sizecode (3) 指令数量
002F 01000000 [1] loadk 0 0 ; R0 := "a"
0033 64000000 [2] closure 1 0 ; R1 := closure(function[0]) 0 upvalues
0037 1E008000 [3] return 0 1 ; return
* constants: 常量区开始
003B 01000000 sizek (1) 常量数量
003F 04 const type 4 一个字节的常量类型,理论上有4种,luac只会出现 number=3 ,string=4,nil 和 boolean 不会出现
0040 02000000 string size (2) 标准字符串格式
0044 6100 "a\0"
const [0]: "a"
* functions: 子函数区,递归
0046 01000000 sizep (1) 子函数数量
004A ** function [0] definition (level 2) 第一个子函数
** start of function **
004A 00000000 string size (0) 函数对应文件名,空串,长度为0,只有长度,没有对应数据
source name: (none)
004E 03000000 line defined (3)
0052 05000000 last line defined (5)
0056 00 nups (0)
0057 00 numparams (0)
0058 03 is_vararg (3)
0059 02 maxstacksize (2)
* code:
005A 03000000 sizecode (3)
005E 65000000 [1] vararg 1 0 ; R1 to top := ...
0062 5E000000 [2] return 1 0 ; return R1 to top
0066 1E008000 [3] return 0 1 ; return
* constants: 常量区
006A 00000000 sizek (0) 只有长度0,没有数据
* functions: 子函数区
006E 00000000 sizep (0) 只有长度0,没有数据
* lines: 指令地址对应源文件行号, strip 时会清空,只留下数量0
0072 03000000 sizelineinfo (3)
[pc] (line)
0076 04000000 [1] (4)
007A 04000000 [2] (4)
007E 05000000 [3] (5)
* locals: local 变量名, strip 时会清空,只留下数量0
0082 01000000 sizelocvars (1)
0086 04000000 string size (4) 标准字符串
008A 61726700 "arg\0"
local [0]: arg
008E 00000000 startpc (0) 跟着2个字节的变量作用范围
0092 02000000 endpc (2)
* upvalues:
0096 00000000 sizeupvalues (0) upval 变量名, strip 时会清空,只留下数量0
** end of function [0] definition (level 2) ** 一个函数结束了
* lines: 顶层函数的其他数据,格式见上面
009A 03000000 sizelineinfo (3)
[pc] (line)
009E 01000000 [1] (1)
00A2 05000000 [2] (5)
00A6 05000000 [3] (5)
* locals:
00AA 02000000 sizelocvars (2)
00AE 02000000 string size (2)
00B2 6100 "a\0"
local [0]: a
00B4 01000000 startpc (1)
00B8 02000000 endpc (2)
00BC 02000000 string size (2)
00C0 6600 "f\0"
local [1]: f
00C2 02000000 startpc (2)
00C6 02000000 endpc (2)
* upvalues:
00CA 00000000 sizeupvalues (0)
** end of function [0] definition (level 1) ** 顶层函数结束,没了
00CE ** end of chunk **
luac格式,也可以参考
【原创】Lua脚本反编译入门之一 ,这篇主要分析的是5.2版,不过跟5.1差别不大。
格式分析里我们可以看见 “改文件头”和“改数字类型”的影响。“OpCode 重定义”就不太容易看出了。所以,前面我们想方设法导出了 allopcodes.lua 的编译后数据,要拿来跟标准 lua 编译后的对比。
allopcodes.lua 这个程序包含3个函数,顶层的一个是主函数,主函数包含的第一个函数,是我们要对比的对象。
标准 lua 格式是这样的,66条指令,包含 lua 5.1 所有的38种指令。
然后我们知道,lua指令格式是 4个字节,对 big endian/大头 ,标准lua就是这种,第1个字节的低6位,是操作符。
那么,这样也就能对比出“OpCode 重定义”到底是怎么做的了。
0063 42000000 sizecode (66)
0067 C0000000 [01] move 3 0 ; R3 := R0
006B 01010000 [02] loadk 4 0 ; R4 := 1
006F 42018000 [03] loadbool 5 1 0 ; R5 := true
0073 83010003 [04] loadnil 6 6 ; R6, := nil
0077 C4010000 [05] getupval 7 0 ; R7 := U0 , u1
007B 05420000 [06] getglobal 8 1 ; R8 := g1
007F C6018203 [07] gettable 7 7 8 ; R7 := R7[R8]
0083 07410000 [08] setglobal 4 1 ; g1 := R4
0087 48018000 [09] setupval 5 1 ; U1 := R5 , u2
008B 89018102 [10] settable 6 5 4 ; R6[R5] := R4
008F 0A420001 [11] newtable 8 2 1 ; R8 := {} , array=2, hash=1
0093 40020002 [12] move 9 4 ; R9 := R4
0097 80028002 [13] move 10 5 ; R10 := R5
009B 09420181 [14] settable 8 258 5 ; R8["x"] := R5
009F 22420001 [15] setlist 8 2 1 ; R8[1 to 2] := R9 to R10
00A3 4B824004 [16] self 9 8 258 ; R10 := R8; R9 := R8["x"]
00A7 5C820001 [17] call 9 2 2 ; R9 := R9(R10)
00AB 8C028101 [18] add 10 3 4 ; R10 := R3 + R4
00AF 8D420105 [19] sub 10 10 5 ; R10 := R10 - R5
00B3 8E820105 [20] mul 10 10 6 ; R10 := R10 * R6
00B7 8FC20105 [21] div 10 10 7 ; R10 := R10 / R7
00BB 90020205 [22] mod 10 10 8 ; R10 := R10 % R8
00BF 91420205 [23] pow 10 10 9 ; R10 := R10 ^ R9
00C3 92020005 [24] unm 10 10 ; R10 := -R10
00C7 D3020005 [25] not 11 10 ; R11 := not R10
00CB D4028005 [26] len 11 11 ; R11 := #R11
00CF 00030005 [27] move 12 10 ; R12 := R10
00D3 40038005 [28] move 13 11 ; R13 := R11
00D7 15430306 [29] concat 12 12 13 ; R12 := R12..R13
00DB 17400102 [30] eq 0 4 5 ; R4 == R5, to [32] if true
00DF 16400080 [31] jmp 2 ; to [34]
00E3 58808102 [32] lt 1 5 6 ; R5 < R6, to [34] if false
00E7 16400080 [33] jmp 2 ; to [36]
00EB 19C00103 [34] le 0 6 7 ; R6 <= R7, to [36] if true
00EF 16000280 [35] jmp 9 ; to [45]
00F3 41030000 [36] loadk 13 0 ; R13 := 1
00F7 81C30000 [37] loadk 14 3 ; R14 := 10
00FB C1030100 [38] loadk 15 4 ; R15 := 2
00FF 60830080 [39] forprep 13 3 ; R13 -= R15; PC := 43
0103 DA000000 [40] test 3 0 ; if R3 then to [42]
0107 16000080 [41] jmp 1 ; to [43]
010B C0008002 [42] move 3 5 ; R3 := R5
010F 5FC3FE7F [43] forloop 13 -4 ; R13 += R15; if R13 <= R1
4 then begin PC := 40; R16 := R13 end
0113 16000280 [44] jmp 9 ; to [54]
0117 45430100 [45] getglobal 13 5 ; R13 := ipairs
011B 80030004 [46] move 14 8 ; R14 := R8
011F 5C030101 [47] call 13 2 4 ; R13 to R15 := R13(R14)
0123 16800080 [48] jmp 3 ; to [52]
0127 DB410004 [49] testset 7 8 1 ; if R8 then R7 = R8 else to [51]
012B 16000080 [50] jmp 1 ; to [52]
012F C0018004 [51] move 7 9 ; R7 := R9
0133 61830000 [52] tforloop 13 2 ; R16, R17 := R13(R14,R15); if R16 ~= nil then R15 := R16 else PC := 54
0137 1680FE7F [53] jmp -5 ; to [49]
013B 65038001 [54] vararg 13 3 ; R13, R14 := ...
013F E4030000 [55] closure 15 0 ; R15 := closure(function[0]) 2 upvalues
0143 00008006 [56] move 0 13 ; R0 := R13
0147 00000007 [57] move 0 14 ; R0 := R14
014B 00048007 [58] move 16 15 ; R16 := R15
014F 45840100 [59] getglobal 17 6 ; R17 := k
0153 85C40100 [60] getglobal 18 7 ; R18 := v
0157 1C448001 [61] call 16 3 1 ; := R16(R17, R18)
015B 63030000 [62] close 13 ; SAVE all upvalues from R13 to top
015F 45030200 [63] getglobal 13 8 ; R13 := f1
0163 5D038000 [64] tailcall 13 1 0 ; R13 to top := R13()
0167 5E030000 [65] return 13 0 ; return R13 to top
016B 1E008000 [66] return 0 1 ; return
通过上面做的文件分析对比,我们已经可以编译出 能够执行非标准luac的 lua 虚拟机了。
但是如果你还想做 luac 格式转换的话,对于只用了“OpCode重定义”的,你可以载入,修改指令,写出。
具体实现参考一下 luac.c ,里实现的 载入 和 写出。修改指令参考
【原创】Lua脚本反编译入门之一 , 记得递归修改所有的函数。
还有载入时一定要跳过指令检查,也就是输出 bad code 的地方。这样改:
lundump.c 的 LoadFunction , 注释下面一句
IF (!luaG_checkcode(f), "bad code");
“二 标准化 luac ”结束。现在应该能做到:
编译出 能够执行非标准luac的 lua 虚拟机
标准 luac 和 非标准 luac 之间格式转换的程序,应该能写了
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课