系列教程
样本
两个求破贴
样本 1 来源:https://www.52pojie.cn/forum.php?mod=redirect&goto=findpost&ptid=737347&pid=20098089
下载地址:https://pan.baidu.com/s/1tKLaRdyQpz6ORmYCLZ48Aw
--YIC新加密算法
print('加密由一炮、&@yic77455提供。\n如未能进入,可能密码错误!\n'..'加群免费获取')
function Yicjm(key,code)
return (code:gsub('..', function (h) return string.char((tonumber(h,16)+256-13 - key + 999999*256)%256) end))
end
local v_value=gg.prompt({'加群免费获取'},{[1]=0},{[1]='number'})
gg.toast('正在加载脚本……')
pcall(Yicjm(v_value[1], '1B1D14191FD3D2FCFCDEDEE0DBE1DCE4......'))
样本 2 来源:https://www.52pojie.cn/forum.php?mod=redirect&goto=findpost&ptid=737347&pid=20161673
下载地址:https://www.lanzouj.com/i11pd4j
--YIC新加密算法
goto aa
::aa::
gg.clearResults()
gg.searchNumber('1;1', gg.TYPE_AUTO, false, gg.SIGN_EQUAL, 0, -1)
gg.getResults(2000)
gg.toast('过保护已开启')
print('加密由饭、&@yic77455提供。\n如未能进入,可能密码错误!\n'..'友情更新刺激战场5.12最新版')
function Yicjm(key,code)
return (code:gsub('..', function (h) return string.char((tonumber(h,16)+256-13 - key + 999999*256)%256) end))
end
local v_value=gg.prompt({'友情更新刺激战场5.12最新版'},{[1]=0},{[1]='number'})
gg.toast('正在加载脚本……')
pcall(Yicjm(v_value[1], 'B3C2BBB0C1B6BCBB6DB0B5B2B0B8A1B6......'))
这两个样本都可以“YIC加密”的字样。
分析
看上去这个关键代码 string.char((tonumber(h,16)+256-13 - key + 999999*256)%256)
的解码算法不是特别麻烦,但是还是和 lua脚本解密5:RC4加密穷举秘钥 一样,没有秘钥是解不了的,必须得穷举。
因为这个解码算法比较简单,所以我们仔细分析一下。
如果你真的想学,你最好先去简单的学一下 Lua 语言。
这里假设你应该是可以看出哪段代码是我们解密的关键之处,哪段代码与解密无关。
我尽量用最通俗的语言解释。
碰到 Lua 相关语法,我会默认你已知(请自行去学习 Lua 的相关知识,自行去查阅手册)。
这个算法并不需要很高的数学水平,只要会算术就可以理解,小学生只要会加减乘除求余数就可以理解。
代码概况
关键代码如下:
function Yicjm(key,code)
return (code:gsub('..', function (h) return string.char((tonumber(h,16)+256-13 - key + 999999*256)%256) end))
end
local v_value=gg.prompt({'友情更新刺激战场5.12最新版'},{[1]=0},{[1]='number'})
gg.toast('正在加载脚本……')
pcall(Yicjm(v_value[1], 'B3C2BBB0C1B6BCBB6DB0B5B2B0B8A1B6......'))
这段代码的意思大概就是
- 定义一个解密函数
Yicjm
- gg修改器读取一个数值存到
v_value
- 提示正在加载脚本
- 然后用你刚才输入的那个数值
v_value[1]
作为 Yicjm
函数的 key
参数来解密 'B3C2BBB0C1B6BCBB6DB0B5B2B0B8A1B6......'
这个字符串,得到真正的代码,然后执行。
解码函数
接下来我们来分析这个 Yicjm
函数。
这里的 key
就是秘钥了,code
是一个字符串,code
的形式就是一个十六进制字符串表示的数据。
然后我们就要分析这个算法了。
code:gsub('..', function (h) return string.char((tonumber(h,16)+256-13 - key + 999999*256)%256) end)
code:gsub
中的冒号(:
)是 Lua 面向对象编程的一个语法糖,因为 code
是一个字符串(string
)类型的变量,所以这句话相当于
string.gsub(code, '..', return string.char((tonumber(h,16)+256-13 - key + 999999*256)%256) end)
我们去查一下 gsub
的用法
https://www.lua.org/manual/5.2/manual.html#pdf-string.gsub
string.gsub (s, pattern, repl [, n])
Returns a copy of s
in which all (or the first n
, if given) occurrences of the pattern
have been replaced by a replacement string specified by repl
, which can be a string, a table, or a function. gsub
also returns, as its second value, the total number of matches that occurred. The name gsub
comes from Global SUBstitution.
If repl
is a string, then its value is used for replacement. The character %
works as an escape character: any sequence in repl
of the form %d
, with d between 1 and 9, stands for the value of the d-th captured substring. The sequence %0
stands for the whole match. The sequence %%
stands for a single %
.
If repl
is a table, then the table is queried for every match, using the first capture as the key.
If repl
is a function, then this function is called every time a match occurs, with all captured substrings passed as arguments, in order.
In any case, if the pattern specifies no captures, then it behaves as if the whole pattern was inside a capture.
If the value returned by the table query or by the function call is a string or a number, then it is used as the replacement string; otherwise, if it is false or nil, then there is no replacement (that is, the original match is kept in the string).
Here are some examples:
x = string.gsub("hello world", "(%w+)", "%1 %1")
--> x="hello hello world world"
x = string.gsub("hello world", "%w+", "%0 %0", 1)
--> x="hello hello world"
x = string.gsub("hello world from Lua", "(%w+)%s*(%w+)", "%2 %1")
--> x="world hello Lua from"
x = string.gsub("home = $HOME, user = $USER", "%$(%w+)", os.getenv)
--> x="home = /home/roberto, user = roberto"
x = string.gsub("4+5 = $return 4+5$", "%$(.-)%$", function (s)
return load(s)()
end)
--> x="4+5 = 9"
local t = {name="lua", version="5.2"}
x = string.gsub("$name-$version.tar.gz", "%$(%w+)", t)
--> x="lua-5.2.tar.gz"
这个解码函数使用的是 “repl
is a function” 这种用法,每次匹配到样式,都会调用后面的函数,然后替换成返回值。
这里我把原文复制过来不是为了凑字数,而是为了说明如何查阅官方文档。
至于 Lua 的字符串匹配语法请自行参考官方手册 https://www.lua.org/manual/5.2/manual.html#6.4.1
这里的匹配样式就是 '..'
,两个点就是两个字符,就是两个十六进制数,或者说就是 1 个字节的十六进制表示。
替换 callback 算法分析
function (h)
return string.char((tonumber(h,16)+256-13 - key + 999999*256)%256)
end
这里的 h
就是一个 2 个字符的字符串,比如 'B3'
、'C2'
等等(以样本 2 的加密部分 'B3C2BBB0C1B6BCBB6DB0B5B2B0B8A1B6......'
为例)。
tonumber(h,16)
这个函数就是把字符串以 16 进制的形式转换为数字,'B3'
会被转换成为 179
,我们暂时把这个数值记为 b
(代表 byte)。
然后最关键的部分来了,看这段算法:
(b + 256 - 13 - key + 999999 * 256) % 256
这是一个求余数的运算。首先,它似乎等于(这个应该能看懂吧):
(b - 13 - key + 1000000 * 256) % 256
根据同余定理,它等于:
(b - 13 - key) % 256
如果你不懂的话,简单的说明一下:3 % 7 = 3, 10 % 7 = 3, 17 % 7 = 3
。
发现什么规律了吗?如果 x % 256 = y
,那么 (x + 1000000 * 256) % 256 = y
也成立。
因上面的式子是一个求除以 256 的余数,那么 key
的穷举范围只需要取 0 ~ 255 就够了,即使你想将 key
取在更大的范围,比如 key
取为 257,结果一定和 key
取 1 的时候是一样的。
同时我们也知道了,式子的结果也一定是 0 ~ 255 的范围,这个结果恰好被 string.char
转换成为字符。
解码
所以,请用一句话总结一下上面我们分析得到的结论是什么?
然后我们就写一个穷举的代码
function Yicjm(key,code)
return (code:gsub('..', function (h) return string.char((tonumber(h,16)+256-13 - key + 999999*256)%256) end))
end
for key = 0, 255 do
print(key)
local code = Yicjm(key, 'B3C2BBB0C1B6BCBB6DB0B5B2B0B8A1B6')
if (string.find(code, "gg")
and string.find(code, "if")
and string.find(code, "then")
and string.find(code, "end")) then
print(key .. " may be the key.")
file = io.open("2.lua", "w")
file:write(code)
file:close()
break
end
end
这里就是检测了解码中的代码中是否包含 gg
、if
、then
、end
这类特殊关键字,如果包含则解码成功了,输出到 2.lua
。
成果
样本 1 成果
样本 2 成果
总结
秘钥量太小,轻易就能穷举出来,总之这个加密是非常不负责任的。
样本 3
样本 3 求破贴
我下载看了一下,的确按照我的方法是无法找到秘钥的,为什么呢?
原因很简单,因为解密之后的代码不包含 gg
、if
、then
、end
这类特殊关键字。
如果你尝试判断是否包括 load
关键字,那么你可以很轻松的得到秘钥。
for key = 0, 255 do
print(key)
local code = Yicjm(key, "4E4E4E71719D8D8729CEE429F3CA2BF2")
if (string.find(code, 'load')) then
print(key .. " YIC decode OK.")
file = io.open("2.lua", "w")
file:write(code)
file:close()
break
end
end
其实是在不行你就挨个试嘛...
那么有没有什么更好的方法呢?
既然读取脚本用的是 load
函数,这个函数在读取字符串成功时会返回读取结果(也就是一个函数),失败时则返回 nil
和错误信息,那么我们可以用这个函数来判断,解密之后的函数能否被读取运行。
for key = 0, 255 do
print(key)
local code = Yicjm(key, "4E4E4E71719D8D8729CEE429F3CA2BF2")
local func, err = load(code)
if (func) then
print(key .. " YIC decode OK.")
file = io.open("2.lua", "w")
file:write(code)
file:close()
break
end
end
这回可就是“为所欲为”了,即使出现 precompiled chunk
也不怕了,load
函数会自动处理的。
注意:对于 lua 5.1 请使用 loadstring
函数
样本 3 成果
这只是解密出的第 1 层,样本 3 的这个作者硬是用同样一个算法加密了 7 层,中间每层还把密码密码给出来了。
作者似乎不知道“同余”这种东西,93987
和 93987 % 256
(也就是 35
) 得到的结果是一样的,根本用不着那么长的密码。
能破的人不在乎多几层,破不了的人一层加密就足够了,真有意思。
相关链接
附件
3 个样本,以及使用到的代码
examples.7z
(21.18 KB, 下载次数: 259)