Learn from wp:
- [Original]2021CISCN reverse-little_evil-CTF confrontation-kanxue-security community|security recruitment|kanxue.com
- CISCN Little_evil – Pandaos’s blog (panda0s.top)
- CISCN2021 RE writeup (s0uthwood.github.io)
Get flag:M5Ya7
Knowledge learned:
Question type:
[\[ruby language reverse engineering\]] Ruby program packaged with Ruby packer
side channel blasting
Use of subprocess library python
VM reverse engineering (virtual machine reverse engineering or code interpreter reverse engineering)
Interpreted language code obfuscation uses itself to encrypt itself, and then parses itself at runtime.
This VM is actually the Brainfuck interpreter
__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);
}
When you see this code, you can guess that it must have been obfuscated. You can tell it is written in ruby when you see ruby, but I don’t know how it was obfuscated!
Only after looking at other people’s wp did I know that it was packaged by Ruby packer. Go to github to find the source code! !
Source code: github Ruby packer packager source code
ruby_sysinit(&argc, &argv);
{
RUBY_INIT_STACK;
ruby_init();
return ruby_run_node(ruby_options(argc, argv));
}
Found it to be the same! ! If it’s packed, then we need to unpack it!
It is a ruby program packaged with Ruby packer. Ruby packer will package the source program into a squashfs file system and insert it into the ruby interpreter.
ruby packer uses a memory virtual file system enclose_io_memfs
0. The first layer of confusion ruby packer packaging
squashfs can be extracted directly using binwalk
binwalk -Me little_evil
binwalk is a tool commonly used in digital forensics and binary analysis. The command binwalk -Me little_evil you provided is used to perform specific operations of binwalk. The following explains the meaning of each part of the command:
- binwalk: This is the command itself, indicating that you want to use the binwalk tool.
- -Me: These are options or flags that modify the behavior of binwalk. Specifically, -M tells binwalk to automatically extract files as they are discovered, and -e tells it to recursively extract files from other files.
- little_evil: This is the name of the file or directory you want binwalk to analyze and extract files from.
┌──(kali㉿kali)-[~/Desktop/Competition question/Previous national competitionsRE/CISCN 2021preliminary round little evil]
└─$ binwalk -Me little_evil
Scan Time: 2024-05-16 15:27:58
Target File: /home/kali/Desktop/Competition question/Previous national competitionsRE/CISCN 2021preliminary round 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 "
Extracted successfully! String obtained according to ida: "/__enclose_io_memfs__/local/out.rb"
After the execution is completed, there is a folder _little_evil.extracted, where we extract the main program, path:
_little_evil.extracted\squashfs-root\enclose_io_memfs\local\out.rb
The source code is obfuscated:
Beautify Ruby files
This should be a lesson in ruby syntax: https://www.runoob.com/ruby/ruby-syntax.html
It is found that there is only the key string, and the others are repeated variable names:
$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;
Go to the website to beautify it, and then ask chatgpt:
Or directly use the beautification scripts of the big guys for free:
data = open("d:/CTF_Study/Reverse/ciscnPast questions/[CISCN 2021preliminary round]little evil/out.rb", "r").read()
data = data.replace(";", ";\n")
data = data.replace("()", "()\n\t")
data = data.replace("end;", "end;\n")
count = 1 # The number of function names
names = {}
for line in data.splitlines(): # Get a newline character after\nor\tA line of equal characters
if line.startswith("def"): # Determine whether the beginning of a line isdef
idx = line.find("(") # The subscript after the function name itself
original_name = line[4:idx] # Get the original function name
names[original_name] = "func" + str(count) # Create dictionary and assign values
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/ciscnPast questions/[CISCN 2021preliminary round]little evil/out2.rb", "w").write(data)
Out!
1. The second level of confusion, through send (“eval”, “source code”), ruby syntax confusion
Analyzing the ruby function found:
- 21 functions defined
- Use the send function to execute the code
- Combine the real source code ruby by calling the func function, and then execute the code through func21
De-obfuscation logic:
First write a test code to understand the send function:
$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
You can know that the first parameter of the send function is the function to be called, and the second parameter is the parameter of the calling function! !
By adding a print function above the send function:
def func21()
print l1Il[0,4] +"\n"
send(l1Il[0,4], l1Il[4,l1Il.length]);
end;
The output function name is: eval
So $l1Il[4,$l1Il.length]
is the source code to be called! ! !
Just add the output function directly!
def func21()
print l1Il[4,l1Il.length]
send(l1Il[0,4],l1Il[4,$l1Il.length]);
end;
The source code was successfully leaked to deobfuscate, and the solution is still this obfuscation:
Let’s de-obfuscate:
Source code:
begin $_=/;@_=_+_;-_=_-@_
__=->_{_==[]||_==''?.:_+__[_[_..-_]]}
@__=->_,&__{_==[]?[]:[__[_[.]]]+@__[_[_..-_],&__]}_____=->_{@__[[*_],&->__{__[.]}]}
@_____=->_{@__[[*_],&->__{__[-_]}]}
______=->_{___,______=_____[_],@_____[_];_____=__[___];____={};__=.;(_=->{
____[______[__]]=___[__];(__+=_)==_____ ?____:_[]})[]}
@______=->_,__{_=[*_]+[*__];____=__[_];___={};__=.;(_____=->{
___[_[__][.]]=_[__][_];(__+=_)==____ ?___:_____[]})[]}
_______=->_{___=[];@___=__[_];__=___=____=.;____,@____={},[]
(_____=->{
_[____]=='5'?(@____<<____):.
_[____]=='6'?(____[@____[-_]]=____;@____=@____[....-@_]):.
(____+=_)==@___?.:_____[]})[]____=____=={}?{}:@______[____,______[____]]
(______=->{_[__]==
'0'?(___[___]||=.;___[___]+=_):_[__]==
'1'?(___[___]||=.;___[___]-=_):_[__]==
'2'?(___[___]||=.;___[___]=STDIN.getc.ord):_[__]==
'3'?(___+=_):_[__]==
'4'?(___-=_):_[__]==
'5'?(__=(___[___]||.)==.?____[__]:__):_[__]==
'6'?(__=(___[___]||.)!=.?____[__]:__):_[__]==
'7'?(><<(''<<___[___])):.
(__+=_)==@___?_:______[]})[]}_______['3351635164300000000540000000003164073000000540000003164070070000071730000000541111111131641175160343516445163530440316354031643451634235163516000000054000000000003164344354131645335163435164444516333530444403331635403164344451665163423516351600000054000000000316413443541316453351634351644445163335304444033316354031643444516651634235163516000000005400000000000316403443541316453351634351644445163335304444033316354031643444516651634235163516000000005400000000000031640344354131645335163435164444516333530444403331635403164344451665163423516351600000540000000000031643443541316453351634351644445163335304444033316354031643444516651635164453030441633544033164533516351643000000005400000000003164171111744516644'];rescue Exception;end
2. The third level of confusion is to obfuscate Ruby syntax by modifying the variable name and function name.
This is very difficult to solve. For a novice in Ruby language, it is impossible to understand it! !
When I looked at other people’s wp, I found that I needed to use a particularly complicated demixing script. It was too complicated and I couldn’t understand it at all, so I had to find a way to solve it myself!
0. With the benefit of hindsight, directly solve the problem by adding the side channel leakage flag and source code instrumentation
a. Source code instrumentation
According to the wp of other big guys, we can know that this is a simple VM, so we can think of the characteristics of a VM. When checking the flag, it must be checked byte by byte, that is, the more correct the input string is, the better the VM will be. The more cycles there are, the more!
First find the VM parsing switch:
(______=->{_[__]==
'0'?(___[___]||=.;___[___]+=_):_[__]==
'1'?(___[___]||=.;___[___]-=_):_[__]==
'2'?(___[___]||=.;___[___]=STDIN.getc.ord):_[__]==
'3'?(___+=_):_[__]==
'4'?(___-=_):_[__]==
'5'?(__=(___[___]||.)==.?____[__]:__):_[__]==
'6'?(__=(___[___]||.)!=.?____[__]:__):_[__]==
'7'?(><<(''<<___[___])):.
(__+=$_)==@___?_:______[]})[]}
Optimize chatgpt and insert a variable on each branch to implement program instrumentation, var0~7:
(______=->{
_[__]=='0'?(___[___]||=.;
___[___]+=_;var0 += 1):
_[__]=='1'?(___[___]||=.;
___[___]-=_;var1 += 1):
_[__]=='2'?(___[___]||=.;
___[___]=STDIN.getc.ord;var2 += 1): #Enter location!
_[__]=='3'?(___+=_;var3 += 1):
_[__]=='4'?(___-=_;var4 += 1):
_[__]=='5'?(__=(___[___]||.)==.?____[__]:__;var5 += 1):
_[__]=='6'?(__=(___[___]||.)!=.?____[__]:__;var6 += 1):
_[__]=='7'?(><<(''<<___[___]);var7 += 1):.
(__+=$_)==@___?_:______[]})[]
}
Let’s sort it out again and get an instrumented version of the ruby script:
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}"
Start testing the role of instrumentation:
Enter: 1234569, M5, M5Y respectively. You can clearly know whether var2 is in the correct position even if it is used to compare the flag!
Now that we know which variable is used to compare the correctness of the data, we can start to leak the side channel flag!
b. Side channel leakage flag
import re
import subprocess
def AnalysisOutput(outputstr): #Parse out the charactersvar2value
# Define regular expression patterns to match variable names and values
pattern = r'var(\d+): (\d+)'
# Find matching variable names and values using regular expressions
matches = re.findall(pattern, outputstr)
# Store the results in a dictionary
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): # from 0 arrive 10
input_str = tmpstr + b"\n"
process = subprocess.Popen(["ruby", "fin1.rb"], # Run my instrumentedruby Script
stdin=subprocess.PIPE,stdout=subprocess.PIPE)
output, _ = process.communicate(input_str) # Enter numbers and get output
#print(output)
if b"OK" in output: #If there isOKIt indicates that the blasting is successful!
return tmpstr,-1
tmpvalue = AnalysisOutput(output.decode('utf-8'))
if tmpvalue != value:
return tmpstr,0
tmpstr[idx] += 1
flag = bytearray(b"!"*10) #Because I don't knowflaglength,So it is directly defined as10try
idx = 0
while idx < 10:
keystr,a = burp(flag,idx,idx+1)
if a == -1:
print("flagyes:",flag)
break
print(keystr)
#flag = keystr
idx += 1
The flag was successfully exploded. The correct flag is M5Ya7:
1. The big guy’s deobfuscation script, then source code analysis, and then VM analysis, the flag appears:
This VM is actually the Brainfuck interpreter
Deobfuscated source code:
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
Here’s what the boss does! :CISCN Little_evil – Pandaos’s blog (panda0s.top)