game
IDA打开main函数啥有用的信息都看不到
运行一下发现是个八数码问题,要求解10000000次
根据提示符去IDA的strings窗口找可以发现内存中存在这部分数据,但是没有交叉引用
说明该字符串在汇编层面是没有引用的
一般来说,用户添加的信息会放在一起,所以可以在提示字符串上下翻一翻,果然找到一点有意思的东西
这个\x1BLuaQ很引入注目,大概率就是lua逆向了
去查一下1B 4C 75 61
,可以看到是Lua的字节码文件头
那么再用Lua搜一下
可以获得版本号
或者通过文件解析也可以知道后一个字节51
代表版本号
由此可见这个程序是lua打包生成的exe,实际上就是自带一个Lua解释器和脚本
那么把脚本抓下来然后反编译即可
稍微搜了一下,这个脚本没有指示长度的字节,是类似于pyc那样递归解析的。
因此需要尽可能多的dump,我抓了0x1500字节时即可解析了
反编译工具
使用方法:
java -jar unluac.jar xxx.lua
require("bit")
borad = {
{
1,
2,
3
},
{
4,
5,
6
},
{
7,
8,
0
}
}
sx = 3
sy = 3
function swap_chess(x, y, xx, yy)
local t = borad[x][y]
borad[x][y] = borad[xx][yy]
borad[xx][yy] = t
end
function move_chess(d)
if d == "S" and sx == 1 or d == "W" and sx == 3 or d == "D" and sy == 1 or d == "A" and sy == 3 then
return
end
if d == "S" then
swap_chess(sx, sy, sx - 1, sy)
sx = sx - 1
elseif d == "W" then
swap_chess(sx, sy, sx + 1, sy)
sx = sx + 1
elseif d == "D" then
swap_chess(sx, sy, sx, sy - 1)
sy = sy - 1
elseif d == "A" then
swap_chess(sx, sy, sx, sy + 1)
sy = sy + 1
end
end
function randomize()
local d = {
"W",
"S",
"A",
"D"
}
math.randomseed(os.time())
for i = 1, 1000 do
move_chess(d[math.random(4)])
end
end
function display()
local s = ""
for x = 1, 3 do
for y = 1, 3 do
s = s .. "| " .. borad[x][y] .. " "
end
s = s .. "|\n"
if x ~= 3 then
s = s .. "-------------\n"
end
end
s = s .. "\n"
io.write(s)
end
secret = {
171,
201,
244,
200,
118,
100,
138,
190,
170,
159,
94,
91,
42,
184,
8,
98,
198,
134,
110,
165,
108,
219,
117,
179,
180,
179,
221,
144,
167,
155
}
print("i want to play a game with u")
io.read()
print("finish this game 10000000 times and i'll show u the flag, trust me")
print("use WSAD/wsad to move, ctrl+z to quit")
io.read()
times = 0
total = 10000000
while times < total do
randomize()
f = false
os.execute("cls")
print("times: " .. times .. "/" .. total)
display()
repeat
io.write("> ")
s = io.read()
if s == nil then
break
end
for i = 1, string.len(s) do
move_chess(string.upper(string.sub(s, i, i)))
end
os.execute("cls")
print("times: " .. times .. "/" .. total)
display()
f = true
for i = 0, 7 do
if borad[math.floor(i / 3) + 1][i % 3 + 1] ~= i + 1 then
f = false
break
end
end
f = f and borad[3][3] == 0
until f
if f then
times = times + 1
math.randomseed(times)
for i = 1, #secret do
secret[i] = bit.bxor(secret[i], math.random(255))
end
else
os.execute("cls")
break
end
end
if times == total then
os.execute("cls")
print("congrats!")
flag = ""
for i, v in ipairs(secret) do
flag = flag .. string.char(v)
end
print(flag)
end
找一下flag,可以发现它是将secret与随机数逐字节异或,循环了10000000次,这个数字有点夸张……估计是出题人怕有算法大佬直接硬解题目吧23333
到了这里,由于用到了巨量随机数,因此必须使用lua来跑了
试了一下online,果然时间不够orz
于是去官网下个lua,很小,但是编译有点困难orz
找到的编译好的binary,但是不带bit库,于是又去github上找了xor的lua实现拿来跑
刚开始下的latest,lua5.3,怎么跑都不对,后来发现各个版本的随机数都不一样,我就惊他喵了个呆了
于是根据之前收集到的信息,找来lua5.1跑即可
function xor(a,b)
local r = 0
local f = math.floor
for i = 0, 31 do
local x = a / 2 + b / 2
if x ~= f(x) then
r = r + 2^i
end
a = f(a / 2)
b = f(b / 2)
end
return r
end
secret = {
171,
201,
244,
200,
118,
100,
138,
190,
170,
159,
94,
91,
42,
184,
8,
98,
198,
134,
110,
165,
108,
219,
117,
179,
180,
179,
221,
144,
167,
155
}
for times=1, 10000000 do
print(times)
times = times + 1
math.randomseed(times)
for i = 1, #secret do
secret[i] = xor(secret[i], math.random(255))
end
end
if times == total then
print("congrats!")
flag = ""
for i, v in ipairs(secret) do
-- print(v)
flag = flag .. string.char(v)
end
print(flag)
end
估计是由于xor的实现问题非常慢,大概需要三四十分钟才能跑出flag
如果是自己编译的windows-lua的话,会自带bit模块,那个bxor应该会很快
只需要在头部添加
require("bit")
将xor改为bit.bxor即可
后来找到了带库的luaforwin安装包,bit模块确实肉眼可见的快,不过感觉也得跑个好几十分钟吧……orz
Rua!
反编译exe,按照那些函数来看是GO语言编译出的
IDA对GO语言的支持相对而言比较差,对参数、结构的解析都比较差强人意
比方说首先自己识别的main函数就是错的,仍然是Go的Runtime初始化部分
我没有学过go语言,所以不清楚它的结构
不过从函数命名来看,应该是把方法所属的类或者是package体现在名称上了
稍微搜索一下就可以知道,go语言的入口package为main
因此可以直接搜索main
很显然这个main_main就是入口方法了
这里符号似乎都保留了,从Read和Write就能猜到意思了,不过这里的参数都没有识别出来----读写总要有句柄或是文件名吧~
从这个函数的声明也可以看出来第一个参数应该就是文件名
void __cdecl main_ReadAll(string *filePath1, __uint8 *_r1, error_0 *_r2)
那么查看汇编可以发现确实有把字符串放到寄存器里的
只是IDA没有识别出来
对于这题而言,看到这里当然就差不多了---不过如果是大一些的程序,还是需要Hex-rays的辅助的,因此最好还是修正咯
以Read为例
main_ReadAll(v1, v2, v0);
的v1、v2、v0从栈声明可以看出分别对应的是rdi, rsi, rdx寄存器
error_0 v0; // rdx
string v1; // rdi
__uint8 v2; // rsi
error_0 _r2; // [rsp+28h] [rbp-18h]
void *retaddr; // [rsp+40h] [rbp+0h]
而从上面的汇编可以看出来,它的参数应该在栈里
也就是说,是传参的分析错误
我们知道传参是由调用约定规定的
这个程序是x64的,默认情况下x64会使用__fastcall--前4个参数使用寄存器来传
知道问题在哪就很好改了,不过查了一下x64下的stdcall、cdecl、fastcall实质上都是一样的……
还好IDA自己定义了一个usercall,没有受这些影响
于是对着函数按y,将其的声明改为__usercall即可
可以看到参数已经识别出来了....虽然好像有点长2333
这个实际上也是因为Go语言的不同
我们知道在C中以\0作为字符串结尾,因此IDA也是这么识别的
而Go中
可以看到,字符串直接是无缝衔接的
引用的时候根据地址直接取
那么程序是怎么知道到哪里结束的呢?
注意看ReadALL的第二个参数,0xD就是第一个参的长度了
解决参数传递的问题后,还有一个返回值的问题……
从直接的汇编可以看出来,ReadAll调用完以后返回值是从栈里取出来的---这就让IDA头大了
目前的IDA认定返回值存在寄存器中,对于这种栈中返回值就没法识别了
因此..还是只能猜啦╮(╯_╰)╭
本题里很明显InfoIntergration对读到的内容进行处理
往下翻一翻发现这里有个exit
关键判断在于v17的值
Hex-rays里面它又是无头变量了,不过大概能猜出来是LazyProc的返回值吧
关于LazyProc查一下可以知道它是通过模块名和函数名来调用函数的方法
于是再往上看,可以发现用runtime_writeBarrier
做了两次写入
比较类似这个形式
fmtp, _ := syscall.BytePtrFromString("%d %d %d")
a, _, _ := GetDLL(t, "user32.dll").Proc("wsprintfA").Call(
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(fmtp)),
1000, 2000, 3000)
两次写入分别是模块和函数名
然而直接看比较困难,还是要把调用约定改回来,或者就要硬怼汇编啦
可以看到第一次的参数是main_kernel
查看交叉引用可知是Kernel32.dll
的句柄
第二次则是GetTickCount的字符串
然后通过LazyCall调用
研究了一下可以通过脚本把调用约定跑回来,这样看起来就比较舒服了
v2和srca是同一个变量,通过改变结构体的类型还可以让IDA把Name、l(应该是library的意思?)、Name.len都识别出来
脚本如下,注意仅对汇编界面光标所在函数内调用过的函数有效
def find_all_callees(start_ea):
callees = {}
fname = GetFunctionName(start_ea)
end_ea = FindFuncEnd(start_ea)
all_refs = set([])
# For each ins in the function.
addr = start_ea
while(addr<end_ea):
# We are only interested in call
if GetDisasm(addr).find("call")==-1 :
addr += idaapi.decode_insn(addr)
continue
refs = CodeRefsFrom(addr, 0)
for ref in refs:
callees[ref] = GetFunctionName(ref)
addr += idaapi.decode_insn(addr)
return callees
# main
ea = ScreenEA()
callees = find_all_callees(ea)
print"===========func========="
for ea in callees:
print ' 0x%08X: %s' % (ea, callees[ea])
for ea in callees:
type = get_type(ea)
#print(type),
type = type.replace("__cdecl", "__usercall"+" "+ GetFunctionName(ea).replace(".","_")) + ";"
print(type),
print(idc.SetType(ea, type))
这个函数是获取程序运行时间,与我们的输入没有关系
貌似是个反调试?
不管他,继续往下看找输入数据流
上面那一大堆,其实核心部分就这一行异或加密
于是根据题目给出的rua文件解密,得到一个binary
解密脚本:
with open("rua", "rb") as f:
data = f.read()
decode_data = [data[0] for i in range(len(data))]
# print(len(decode_data))
for i in range(len(data)-1,0, -1):
# print(i)
decode_data[i] = (((data[i-1]^i^data[i])&0xff))
print(data)
# print(decode_data)
str = b""
for i in decode_data:
str += bytes((i,))
print(str)
with open("rua_decode", "wb") as f:
f.write(str)
搜一下文件头1B 4C 4A
,又是lua~不过是luajit编译的字节码了,反编译起来没有lua那么舒服,不过还是有轮子的
反编译器
-- BYTECODE -- test.lua:0-0
function someFunc0()
var_0_1 = "bit" --var_0_1 STRING-STRING
require(var_0_1)
var_0_0 = io.read()
str = var_0_0
var_0_0 = "gs2mx}t>{-v<pcp>" --var_0_0 STRING-STRING
unk = var_0_0
var_0_0 = "" --var_0_0 STRING-STRING
ret = var_0_0
var_0_0 = {}
Barray = var_0_0
var_0_1 = 0 --var_0_1 NUMBER-NUMBER
math.randomseed(var_0_1)
var_0_0 = 0 --var_0_0 NUMBER-NUMBER
var_0_1 = string.len(str)
var_0_1 = var_0_1 - 1 --var_0_1 NUMBER-NUMBER
var_0_2 = 1 --var_0_2 NUMBER-NUMBER
for var_0_3 = var_0_0,var_0_1,var_0_2 do --location 0025, loop ends at 0054-1
var_0_8 = str
var_0_9 = var_0_3 + 1 --var_0_9 NUMBER-NUMBER
var_0_7 = str.byte(var_0_8, var_0_9)
var_0_9 = 128 --var_0_9 NUMBER-NUMBER
var_0_8 = math.random(var_0_9) --var_0_8 REPLACE-REPLACE
var_0_6 = bit.bxor(var_0_7, var_0_8)
var_0_7 = 95 --var_0_7 NUMBER-NUMBER
var_0_5 = bit.band(var_0_6, var_0_7)
var_0_5 = var_0_5 + 32 --var_0_5 NUMBER-NUMBER
Barray[var_0_3] = var_0_5
var_0_5 = string.char(unknown0)
var_0_4 = var_0_4 .. var_0_5
ret = var_0_4
end --location 0053, loops back to 0026-1
if ret == unk then
--jump to 0062 (if previous if statement is false) --0062 JMP-JMP
var_0_1 = "Bingo" --var_0_1 STRING-STRING
print(var_0_1)
--jump to 0065 (if previous if statement is false) --0065 JMP-JMP
--location 0062--0062 LOCATION-LOCATION
var_0_1 = "GG" --var_0_1 STRING-STRING
print(var_0_1)
--location 0065--0065 LOCATION-LOCATION
return
end
反编译效果不是很好,不过几个函数都能拿到,再加上本身逻辑不是很复杂,还是不太难的
核心算法其实就几句
math.randomseed(0)
for i=1 , string.len(input) do
r[i] = input[i]^math.random(128)&95+32
end
最后要求r与给定字符串相等,值得注意的是给定字符串无论是反编译的结果还是官方luajit.exe反汇编出的结果,都是截断长度不够的
只能自己去binary中扒字符串,而输入长度也未知╮(╯_╰)╭所以只能往长了取
然后反运算即可
这里还有同样长度的随机数需要拿出,我刚开始用luajit生成随机数死活算不对,后来又搞个lua51跑了一下随机数发现居然不一样,再算就对了
PS: lua51 52 53 luajit 每个的随机数都不一样,我真是惊了个呆了……
s = """gs2mx}t>{-v<pcp>"+`v>19*%j=|g ;p{/w="tdg?*!!#%$)j*}."""
l = [1, 31, 83, 10, 35, 47, 33, 127, 41, 120, 23, 110, 5, 2, 81, 63, 35, 103, 12, 58, 82, 125, 85, 102, 73, 6, 113, 9, 38, 127, 10, 3, 4, 73, 67, 83, 93, 50, 49, 4, 116, 27, 111, 70, 124, 81, 48, 122, 30, 67, 30, 115, 48, 53, 96, 46, 49, 29, 10, 76, 87, 56, 106, 5, 85, 33, 96, 109, 22, 97, 56, 52, 19, 66, 60, 63, 11, 107, 113, 71, 66, 13, 47, 92, 1, 17, 11, 114, 12, 41, 81, 84, 63, 46, 102, 55, 3, 28, 108, 51]
test1 = ""
test2 = ""
for i in range(len(s)):
r = (ord(s[i])-32)
r1 = r | 0b100000
r = r^l[i]
r1 = r1^l[i]
if(r<127 and r>32):
print(chr(r),end='')
elif(r1<127 and r1>32):
print(chr(r1),end='')
else:
print('**', end='')
反运算的时候注意原运算中有一个&,因此解码的时候会出现两个可能
还有更简单的方法就是逐字节爆破23333虽然同样会有多解,但是省事的多
n = 0
while(n<len(s)):
for i in range(32, 127):
if(((i^l[n])&95)+32==ord(s[n])):
print(chr(i),end='')
# n += 1
# break
print()
n += 1