CISCN 2021 preliminary competition little evil (Ruby + interpreted language obfuscation + side channel attack)

Learn from wp:

  1. [Original]2021CISCN reverse-little_evil-CTF confrontation-kanxue-security community|security recruitment|kanxue.com
  2. CISCN Little_evil – Pandaos’s blog (panda0s.top)
  3. CISCN2021 RE writeup (s0uthwood.github.io)

Get flag:M5Ya7

Knowledge learned:

Question type:
[\] 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:

  1. 21 functions defined
  2. Use the send function to execute the code
  3. 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)