借鉴wp:
- [原创]2021CISCN逆向-little_evil-CTF对抗-看雪-安全社区|安全招聘|kanxue.com
- CISCN Little_evil - Pandaos’s blog (panda0s.top)
- CISCN2021 RE writeup (s0uthwood.github.io)
获得flag:M5Ya7
学到的知识:
题目类型:
[[ruby语言逆向]] 用Ruby packer打包的ruby程序
侧信道爆破
subprocess库pyhon的使用
VM逆向(虚拟机逆向 or 代码解释器逆向)
解释性语言代码混淆 使用自己加密自己,运行时再自己解析自己
这个VM其实是Brainfuck解释器
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
...
v25[0] = a1;
v24 = a2;
v3 = sub_52D704(a1, a2, a3);
v4 = 64;
if ( v3 )
goto LABEL_10;
v5 = malloc(0x1C0uLL);
qword_1415FC0 = v5;
if ( !v5 )
{
v6 = "NULL != enclose_io_fs";
v4 = 66;
goto LABEL_4;
}
v7 = v5;
for ( i = 112LL; i; --i )
*v7++ = 0;
if ( sub_52E6E6(v5, &unk_B00CA0, 0LL) )
{
v4 = 69;
LABEL_10:
v6 = "SQFS_OK == enclose_io_ret";
goto LABEL_4;
}
if ( !getenv("ENCLOSE_IO_USE_ORIGINAL_RUBY") )
{
v9 = malloc(8LL * (v25[0] + 1));
v10 = v9;
if ( !v9 )
{
v4 = 101;
v6 = &unk_819258;
goto LABEL_4;
}
v11 = v24;
v12 = v25[0];
v13 = v25[0];
*v9 = *v24;
v9[1] = "/__enclose_io_memfs__/local/out.rb";
for ( j = 1LL; j < v12; ++j )
v10[j + 1] = v11[j];
v15 = v13 + 1;
v16 = 0LL;
for ( k = 0LL; k < v15; ++k )
{
v18 = v10[k];
v16 += strlen(v18) + 1;
}
v19 = malloc(v16);
if ( !v19 )
{
v4 = 114;
v6 = "argv_memory";
goto LABEL_4;
}
for ( m = 0LL; m < v15; ++m )
{
strcpy(v19, v10[m]);
v10[m] = v19;
v19 += strlen(v19) + 1;
}
if ( v16 != &v19[-*v10] )
{
v4 = 120;
v6 = "argv_memory - new_argv[0] == total_argv_size";
LABEL_4:
__assert_fail(v6, "main.c", v4, "main");
}
v25[0] = v15;
v24 = v10;
}
v21 = getenv("RUBY_DEBUG");
ruby_set_debug_option(v21);
setlocale(0, "");
ruby_sysinit(v25, &v24);
ruby_init_stack(v26);
ruby_init();
v22 = ruby_options(v25[0], v24);
return ruby_run_node(v22);
}
看到这段代码可以猜测出一定是被混淆过了,在看见ruby就可以知道的ruby写的,但是不知道是如何混淆的!
看别人的wp才知道是Ruby packer打包的,去github找找源码!!
源码:github Ruby packer打包器源码
ruby_sysinit(&argc, &argv);
{
RUBY_INIT_STACK;
ruby_init();
return ruby_run_node(ruby_options(argc, argv));
}
发现确实一样!!打包的话那么我们就需要解包了!
是用Ruby packer打包的ruby程序,Ruby packer 会把源程序相关的打包成 squashfs 文件系统并塞进 ruby 解释器。
ruby packer 用了一个内存虚拟文件系统 enclose_io_memfs
0.第一层混淆ruby packer打包
squashfs 用 binwalk 就可以直接提取
binwalk -Me little_evil
binwalk 是一种常用于数字取证和二进制分析的工具。你提供的命令 binwalk -Me little_evil 是用来执行 binwalk 的特定操作的。下面解释一下命令中每个部分的含义:
- binwalk:这是命令本身,表示你想要使用 binwalk 工具。
- -Me:这些是修改 binwalk 行为的选项或标志。具体来说,-M 告诉 binwalk 在发现文件时自动提取它们,-e 告诉它递归地从其他文件中提取文件。
- little_evil:这是你想要 binwalk 分析并从中提取文件的文件或目录的名称。
┌──(kali㉿kali)-[~/Desktop/比赛题目/历届国赛RE/CISCN 2021初赛 little evil]
└─$ binwalk -Me little_evil
Scan Time: 2024-05-16 15:27:58
Target File: /home/kali/Desktop/比赛题目/历届国赛RE/CISCN 2021初赛 little evil/little_evil
MD5 Checksum: 0d6a007afd95126327b56b1a43c4311e
Signatures: 411
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 ELF, 64-bit LSB executable, AMD x86-64, version 1 (SYSV)
106199 0x19ED7 mcrypt 2.2 encrypted data, algorithm: blowfish-448, mode: CBC, keymode: 8bit
...
3614240 0x372620 SHA256 hash constants, little endian
3619200 0x373980 SHA256 hash constants, little endian
3619216 0x373990 SHA256 hash constants, little endian
...
13563152 0xCEF510 Copyright string: "Copyright 1995-2017 Jean-loup Gailly and Mark Adler "
13566320 0xCF0170 Copyright string: "Copyright 1995-2017 Mark Adler "
成功提取出来!根据ida获得的字符串:"/__enclose_io_memfs__/local/out.rb"
执行完了之后有一个文件夹_little_evil.extracted,我们在其中提取出主程序,路径:
_little_evil.extracted\squashfs-root\enclose_io_memfs\local\out.rb
源码经过混淆:
美化Ruby文件
这个应该是ruby的语法现学一波:https://www.runoob.com/ruby/ruby-syntax.html
发现关键字符串只有,其他的都是重复变量名:
$l1Il="";$l1lI="";def llIl()$lI1lll=$lI1lll|7;end;def l1lll()$lI1lll=10;end;def llI1l()$lI1lll=$lI1lll|4;end;def lIlI()$lI1lll=$lI1lll+3;end;def l111()$lI1lll=$lI1lll%3;end;def lI1IlI()$lI1lll=$lI1lll|3;end;def ll1l1()$lI1lll=$lI1lll*8;end;def l1lI()$lI1lll=$lI1lll-3;end;def lI1lII()$lI1lll=$lI1lll%1;end;def lIlIl()$lI1lll=$lI1lll&10;end;def lIll()$lI1lll=$lI1lll-4;end;def lII1()$lI1lll=$lI1lll%2;end;def l1III()$lI1lll=$lI1lll|1;end;def l1l111()$lI1lll=$lI1lll|5;end;def l1IIII()$lI1lll=$lI1lll%10;end;def l11I()$l1Il=$l1Il+$lI1lll.chr;end;def lIlll()$lI1lll=$lI1lll*9;end;def l11IlI()$lI1lll=$lI1lll-8;end;def lI1I1()$lI1lll=$lI1lll+5;end;def ll11lI()$lI1lll=$lI1lll&9;end;def lII1l1()send($l1Il[0,4], $l1Il[4,$l1Il.length]);end;
去网站美化一下,再去问问chatgpt:
或者直接白嫖大佬们的美化脚本:
data = open("d:/CTF_Study/Reverse/ciscn历年题目/[CISCN 2021初赛]little evil/out.rb", "r").read()
data = data.replace(";", ";\n")
data = data.replace("()", "()\n\t")
data = data.replace("end;", "end;\n")
count = 1 # 函数名的个数
names = {}
for line in data.splitlines(): # 得到后面有换行符\n或\t等字符的一行
if line.startswith("def"): # 判断一行开头是否为def
idx = line.find("(") # 本身函数名的后面一个位置的下标
original_name = line[4:idx] # 得到原先的函数名
names[original_name] = "func" + str(count) # 创建字典并赋值
count += 1
print(names)
for element in names:
data = data.replace(element+"()", names[element]+"()", -1)
data = data.replace(element+";", names[element]+";", -1)
open("d:/CTF_Study/Reverse/ciscn历年题目/[CISCN 2021初赛]little evil/out2.rb", "w").write(data)
出来了!
1.第二层混淆,通过send(“eval”,“源码”),ruby语法混淆
分析ruby函数发现了:
- 定义了21个函数
- 使用send函数来执行代码
- 通过调用func函数来组合出真实的源码ruby,再通过func21进行执行代码
解混淆逻辑:
先写个测试代码了解一下send函数:
$llll = "examWorld"
def exam(param)
puts "Hello, #{param}!"
end
def func21()
print $llll[4, $llll.length]
send($llll[0, 4], $llll[4, $llll.length])
end
func21
可以知道send函数的第一个参数是要调用的函数,第二个是调用函数的参数!!
通过添加一个print函数在send函数上面:
def func21()
print $l1Il[0,4] +"\n"
send($l1Il[0,4], $l1Il[4,$l1Il.length]);
end;
输出的函数名是:eval
所以$l1Il[4,$l1Il.length]
就是要调用的源码!!!
直接添加输出函数就可以了!
def func21()
print $l1Il[4,$l1Il.length]
send($l1Il[0,4], $l1Il[4,$l1Il.length]);
end;
成功泄露出源码解混淆,解出来的依旧是这种混淆:
在来一次解混淆:
源码:
begin $_=$$/$$;@_=$_+$_;$-_=$_-@_
$__=->_{_==[]||_==''?$.:$_+$__[_[$_..$-_]]}
@__=->_,&__{_==[]?[]:[__[_[$.]]]+@__[_[$_..$-_],&__]}
$_____=->_{@__[[*_],&->__{__[$.]}]}
@_____=->_{@__[[*_],&->__{__[$-_]}]}
$______=->_{___,______=$_____[_],@_____[_];_____=$__[___];____={};__=$.;(_=->{
____[______[__]]=___[__];(__+=$_)==_____ ?____:_[]})[]}
@______=->_,__{_=[*_]+[*__];____=$__[_];___={};__=$.;(_____=->{
___[_[__][$.]]=_[__][$_];(__+=$_)==____ ?___:_____[]})[]}
$_______=->_{$___=[];@___=$__[_];__=___=____=$.;$____,@____={},[]
(_____=->{
_[____]=='5'?(@____<<____):$.
_[____]=='6'?($____[@____[$-_]]=____;@____=@____[$...$.-@_]):$.
(____+=$_)==@___?$.:_____[]})[]
$____=$____=={}?{}:@______[$____,$______[$____]]
(______=->{_[__]==
'0'?($___[___]||=$.;$___[___]+=$_):_[__]==
'1'?($___[___]||=$.;$___[___]-=$_):_[__]==
'2'?($___[___]||=$.;$___[___]=STDIN.getc.ord):_[__]==
'3'?(___+=$_):_[__]==
'4'?(___-=$_):_[__]==
'5'?(__=($___[___]||$.)==$.?$____[__]:__):_[__]==
'6'?(__=($___[___]||$.)!=$.?$____[__]:__):_[__]==
'7'?($><<(''<<$___[___])):$.
(__+=$_)==@___?_:______[]})[]}
$_______['3351635164300000000540000000003164073000000540000003164070070000071730000000541111111131641175160343516445163530440316354031643451634235163516000000054000000000003164344354131645335163435164444516333530444403331635403164344451665163423516351600000054000000000316413443541316453351634351644445163335304444033316354031643444516651634235163516000000005400000000000316403443541316453351634351644445163335304444033316354031643444516651634235163516000000005400000000000031640344354131645335163435164444516333530444403331635403164344451665163423516351600000540000000000031643443541316453351634351644445163335304444033316354031643444516651635164453030441633544033164533516351643000000005400000000003164171111744516644'];rescue Exception;end
2.第三层混淆,通过修改变量名函数名进行Ruby语法混淆
这个解决起来非常困难,对于一个ruby语言的新手来说,根本看不懂!!
去看别人的wp,发现需要使用特别复杂的解混小脚本,太复杂了,看不懂一点所以得自己想办法解决!
0.事后诸葛亮,直接侧信道泄露flag加源码插桩来解决
a.源码插桩
根据别的大佬的wp可以知道这是一个简易的VM,所以就可以想到一个VM的特征,检查flag的时候一定是一个个字节的检查,也就是输入的字符串正确的越多,VM循环的次数也就越多!
先找到VM解析switch:
(______=->{_[__]==
'0'?($___[___]||=$.;$___[___]+=$_):_[__]==
'1'?($___[___]||=$.;$___[___]-=$_):_[__]==
'2'?($___[___]||=$.;$___[___]=STDIN.getc.ord):_[__]==
'3'?(___+=$_):_[__]==
'4'?(___-=$_):_[__]==
'5'?(__=($___[___]||$.)==$.?$____[__]:__):_[__]==
'6'?(__=($___[___]||$.)!=$.?$____[__]:__):_[__]==
'7'?($><<(''<<$___[___])):$.
(__+=$_)==@___?_:______[]})[]}
chatgpt优化一下,而且在每个分支上插入一个变量来实现程序插桩,var0~7:
(______=->{
_[__]=='0'?($___[___]||=$.;
$___[___]+=$_;var0 += 1):
_[__]=='1'?($___[___]||=$.;
$___[___]-=$_;var1 += 1):
_[__]=='2'?($___[___]||=$.;
$___[___]=STDIN.getc.ord;var2 += 1): #输入位置!
_[__]=='3'?(___+=$_;var3 += 1):
_[__]=='4'?(___-=$_;var4 += 1):
_[__]=='5'?(__=($___[___]||$.)==$.?$____[__]:__;var5 += 1):
_[__]=='6'?(__=($___[___]||$.)!=$.?$____[__]:__;var6 += 1):
_[__]=='7'?($><<(''<<$___[___]);var7 += 1):$.
(__+=$_)==@___?_:______[]})[]
}
再整理一下得出一个插桩版本的ruby脚本:
begin
var0 = 0;
var1 = 0;
var2 = 0;
var3 = 0;
var4 = 0;
var5 = 0;
var6 = 0;
var7 = 0;
$_=$$/$$;
@_=$_+$_;
$-_=$_-@_
$__=->_{_==[]||_==''?$.:$_+$__[_[$_..$-_]]}
@__=->_,&__{_==[]?[]:[__[_[$.]]]+@__[_[$_..$-_],&__]}
$_____=->_{@__[[*_],&->__{__[$.]}]}
@_____=->_{@__[[*_],&->__{__[$-_]}]}
$______=->_{___,______=$_____[_],@_____[_];
_____=$__[___];
____={};
__=$.;
(_=->{
____[______[__]]=___[__];
(__+=$_)==_____ ?____:_[]})[]}
@______=->_,__{_=[*_]+[*__];
____=$__[_];
___={};__=$.;
(_____=->{
___[_[__][$.]]=_[__][$_];
(__+=$_)==____ ?___:_____[]})[]}
$_______=->_{$___=[];@___=$__[_];
__=___=____=$.;
$____,@____={},[]
(_____=->{
_[____]=='5'?(@____<<____):$.
_[____]=='6'?($____[@____[$-_]]=____;
@____=@____[$...$.-@_]):$.
(____+=$_)==@___?$.:_____[]})[]
$____=$____=={}?{}:@______[$____,$______[$____]]
(______=->{
_[__]=='0'?($___[___]||=$.;
$___[___]+=$_;var0 += 1):
_[__]=='1'?($___[___]||=$.;
$___[___]-=$_;var1 += 1):
_[__]=='2'?($___[___]||=$.;
$___[___]=STDIN.getc.ord;var2 += 1):
_[__]=='3'?(___+=$_;var3 += 1):
_[__]=='4'?(___-=$_;var4 += 1):
_[__]=='5'?(__=($___[___]||$.)==$.?$____[__]:__;var5 += 1):
_[__]=='6'?(__=($___[___]||$.)!=$.?$____[__]:__;var6 += 1):
_[__]=='7'?($><<(''<<$___[___]);var7 += 1):$.
(__+=$_)==@___?_:______[]})[]
}
$_______['3351635164300000000540000000003164073000000540000003164070070000071730000000541111111131641175160343516445163530440316354031643451634235163516000000054000000000003164344354131645335163435164444516333530444403331635403164344451665163423516351600000054000000000316413443541316453351634351644445163335304444033316354031643444516651634235163516000000005400000000000316403443541316453351634351644445163335304444033316354031643444516651634235163516000000005400000000000031640344354131645335163435164444516333530444403331635403164344451665163423516351600000540000000000031643443541316453351634351644445163335304444033316354031643444516651635164453030441633544033164533516351643000000005400000000003164171111744516644'];rescue Exception;end
puts "\n"
puts "var0: #{var0}"
puts "var1: #{var1}"
puts "var2: #{var2}"
puts "var3: #{var3}"
puts "var4: #{var4}"
puts "var5: #{var5}"
puts "var6: #{var6}"
puts "var7: #{var7}"
开始测试插桩的作用:
分别输入:1234569、M5、M5Y,可以很明显知道var2就算用来对比flag是否正确的位置!
既然知道了是哪个变量来对比数据的正确性,那么就可以开始侧信道泄露flag了!
b.侧信道泄露flag
import re
import subprocess
def AnalysisOutput(outputstr): #解析出字符中var2的值
# 定义正则表达式模式来匹配变量名和值
pattern = r'var(\d+): (\d+)'
# 使用正则表达式找到匹配的变量名和值
matches = re.findall(pattern, outputstr)
# 将结果存储在字典中
variables = {}
for match in matches:
var_name = 'var' + match[0]
var_value = int(match[1])
variables[var_name] = var_value
if match[0] == "2":
return int(match[1])
def burp(tmpstr,idx,value):
tmpvalue = value
for i in range(33,127): # 从 0 到 10
input_str = tmpstr + b"\n"
process = subprocess.Popen(["ruby", "fin1.rb"], # 运行我插桩的ruby 脚本
stdin=subprocess.PIPE,stdout=subprocess.PIPE)
output, _ = process.communicate(input_str) # 输入数字并获取输出
#print(output)
if b"OK" in output: #如果有OK则表明爆破成功!
return tmpstr,-1
tmpvalue = AnalysisOutput(output.decode('utf-8'))
if tmpvalue != value:
return tmpstr,0
tmpstr[idx] += 1
flag = bytearray(b"!"*10) #由于不知道flag的长度,所以直接定义为10试试
idx = 0
while idx < 10:
keystr,a = burp(flag,idx,idx+1)
if a == -1:
print("flag是:",flag)
break
print(keystr)
#flag = keystr
idx += 1
成功爆破出flag,正确的flag是M5Ya7:
1.大佬的解混淆脚本,再源码分析,再分析VM,就出flag了:
这个VM其实是Brainfuck解释器
import re
data = open("3.rb", "r").read()
target = '1'
for i in re.findall(r"\$_[^_]", data, re.M | re.S):
if i[-1] != "=":
print(i)
data = data.replace(i, target + i[-1])
# @_
target = '2'
for i in re.findall(r"@_[^_]", data, re.M | re.S):
if i[-1] != "=":
print(i)
data = data.replace(i, target + i[-1])
# $-_ => -1
target = '-1'
for i in re.findall(r"\$-_[^_]", data, re.M | re.S):
if i[-1] != "=":
print(i)
data = data.replace(i, target + i[-1])
# $__ -> proc1
target = '$proc1'
for i in re.findall(r"\$__[^_]", data, re.M | re.S):
print(i)
data = data.replace(i, target + i[-1])
# @__
target = '@proc2'
for i in re.findall(r"@__[^_]", data, re.M | re.S):
print(i)
data = data.replace(i, target + i[-1])
# $_____
target = '$proc3'
for i in re.findall(r"\$_____[^_]", data, re.M | re.S):
print(i)
data = data.replace(i, target + i[-1])
# @_____
target = '@proc4'
for i in re.findall(r"@_____[^_]", data, re.M | re.S):
print(i)
data = data.replace(i, target + i[-1])
# $______
target = '$proc5'
for i in re.findall(r"\$______[^_]", data, re.M | re.S):
print(i)
data = data.replace(i, target + i[-1])
# @______
target = '@proc6'
for i in re.findall(r"@______[^_]", data, re.M | re.S):
print(i)
data = data.replace(i, target + i[-1])
target = '@varX'
for i in re.findall(r"@___[^_]", data, re.M | re.S):
print(i)
data = data.replace(i, target + i[-1])
# $_______
target = '$proc7'
for i in re.findall(r"\$_______[^_]", data, re.M | re.S):
print(i)
data = data.replace(i, target + i[-1])
# $___
target = '$arr1'
for i in re.findall(r"\$___[^_]", data, re.M | re.S):
print(i)
data = data.replace(i, target + i[-1])
# $____
target = '$var3'
for i in re.findall(r"\$____[^_]", data, re.M | re.S):
print(i)
data = data.replace(i, target + i[-1])
# @____
target = '@var4'
for i in re.findall(r"@____[^_]", data, re.M | re.S):
print(i)
data = data.replace(i, target + i[-1])
# [^_]____[^_]
target = 'var5'
for i in re.findall(r"[^_]____[^_]", data, re.M | re.S):
print(i)
data = data.replace(i, i[0] + target + i[-1])
# [^_]___[^_]
target = 'var6'
for i in re.findall(r"[^_]___[^_]", data, re.M | re.S):
print(i)
data = data.replace(i, i[0] + target + i[-1])
# ______
target = 'var7'
for i in re.findall(r"[^_]______[^_]", data, re.M | re.S):
print(i)
data = data.replace(i, i[0] + target + i[-1])
# _____
target = 'var8'
for i in re.findall(r"[^_]_____[^_]", data, re.M | re.S):
print(i)
data = data.replace(i, i[0] + target + i[-1])
data = data.replace(i, i[0] + target + i[-1])
data = data.replace(";", ";\n", -1)
try:
open("out4.rb","w").write(data)
except Exception as e:
print(e)
解混淆后的源码:
begin $_=$$/$$;
@_=1+1;
$-_=1-2;
$proc1=->_{_==[]||_==''?$.:1+$proc1[_[1..-1]]}
@proc2=->_,&__{_==[]?[]:[__[_[$.]]]+@proc2[_[1..-1],&__]}
$proc3=->_{@proc2[[*_],&->__{__[$.]}]}
@proc4=->_{@proc2[[*_],&->__{__[-1]}]}
$proc5=->_{var6,var7=$proc3[_],@proc4[_];
var8=$proc1[var6];
var5={};
__=$.;
(_=->{
var5[var7[__]]=var6[__];
(__+=1)==var8 ?var5:_[]})[]}
@proc6=->_,__{_=[*_]+[*__];
var5=$proc1[_];
var6={};
__=$.;
(var8=->{
var6[_[__][$.]]=_[__][1];
(__+=1)==var5 ?var6:var8[]})[]}
$proc7=->_{$arr1=[];
@varX=$proc1[_];
__=var6=var5=$.;
$var3,@var4={},[]
(var8=->{
_[var5]=='5'?(@var4<<var5):$.
_[var5]=='6'?($var3[@var4[-1]]=var5;
@var4=@var4[$...$.-2]):$.
(var5+=1)==@varX?$.:var8[]})[]
$var3=$var3=={}?{}:@proc6[$var3,$proc5[$var3]]
(var7=->{_[__]==
'0'?($arr1[var6]||=$.;
$arr1[var6]+=1):_[__]==
'1'?($arr1[var6]||=$.;
$arr1[var6]-=1):_[__]==
'2'?($arr1[var6]||=$.;
$arr1[var6]=STDIN.getc.ord):_[__]==
'3'?(var6+=1):_[__]==
'4'?(var6-=1):_[__]==
'5'?(__=($arr1[var6]||$.)==$.?$var3[__]:__):_[__]==
'6'?(__=($arr1[var6]||$.)!=$.?$var3[__]:__):_[__]==
'7'?($><<(''<<$arr1[var6])):$.
(__+=1)==@varX?_:var7[]})[]}
$proc7['3351635164300000000540000000003164073000000540000003164070070000071730000000541111111131641175160343516445163530440316354031643451634235163516000000054000000000003164344354131645335163435164444516333530444403331635403164344451665163423516351600000054000000000316413443541316453351634351644445163335304444033316354031643444516651634235163516000000005400000000000316403443541316453351634351644445163335304444033316354031643444516651634235163516000000005400000000000031640344354131645335163435164444516333530444403331635403164344451665163423516351600000540000000000031643443541316453351634351644445163335304444033316354031643444516651635164453030441633544033164533516351643000000005400000000003164171111744516644'];
rescue Exception;
end
下面就是大佬的操作了!:CISCN Little_evil - Pandaos’s blog (panda0s.top)