mruby字节码逆向分析
0x01 babycode
mruby字节码逆向题目,需要找到对应版本的源码编译出解释器,进而打印出字节码信息,并借此逆向程序逻辑。
题目附件是babycode.mrb
,根据题目名搜索可知.mrb
后缀文件为mruby编译出的字节码程序。
mruby是一个Ruby语言的轻量级实现,可以将ruby源码便以为中间代码,之后有基于寄存器的虚拟机解释运行。
通过010editor加载文件,查看文件头RITE0300
,对照知这是mruby最新版编译出的字节码文件。
magic number | mruby version |
---|---|
RITE0006 | mruby v2.1.0 |
RITE0200 | mruby v3.0.0 |
RITE0300 | mruby v3.1.0 |
随着版本号的增高,模数中的数值也在增大,发布版本不多,可以下载编译出字节码文件对照。
确定好mruby的版本之后,去mruby官网下载对应源码进行编译,rake编译完成后bin目录下可见如下系列的工具。
tool | |
---|---|
mruby | 解释器,解释执行字节码文件,可打印字节码信息 |
mirb | 用于评估ruby代码 |
mrbc | 编译器,将源程序编译为字节码 |
.mrb
文件可由mruby
解释器或mrb_load_irep_file()
函数执行,-b参数是表示该文件是字节码文件,-v参数可以打印出字节码信息。
所以用mrbuy程序拿到可读的字节码信息。
之后就是漫长的人脑反编译过程,在mruby/include/mruby/ops.h文件中有对mruby字节码的注解。
翻译开始前最好先仔细观察指令格式,并且对于有疑问的opcode要搜索对照或自己写测试代码解释为字节码查看,这个过程一定要沉住气,以减少不必要的错误。
观察字节码格式,发现其分块比较明显,irep 后面跟一个地址则是函数地址,之后则是关于函数所用的寄存器信息和指令长度等,其在打印字节码时按源码位置从上到下翻译,但是类和函数的只展示声明,主体放到后面,主要是函数的调用信息。
irep 0x56268619b4b0 nregs=5 nlocals=2 pools=1 syms=5 reps=2 ilen=55
local variable names:
R1:p
000 LOADNIL R2#R2=nil
002 LOADNIL R3#R3=nil
004 CLASS R2 :Crypt
007 EXEC R2 I(0:0x56268619b580)
010 TCLASSR2
012 METHODR3 I(1:0x56268619bd80)
015 DEF R2 :check
018 SSEND R2 :gets n=0 (0x00)#R2=gets()
022 SEND R2 :chomp n=0 (0x00)#R2=R2.chomp
026 MOVE R1 R2 ; R1:p
029 MOVE R3 R1 ; R1:p
032 SSEND R2 :check n=1 (0x01)
036 JMPNOTR2 050
040 STRINGR3 L(0); yes #if(R2==TRUE) PUTS('yes')
043 SSEND R2 :puts n=1 (0x01)
047 JMP 052
050 LOADNIL R2
052 RETURNR2
054 STOP# exit VM
其中定义类的模块、定义函数和函数调用模块如下
004 CLASS R2 :Crypt
007 EXEC R2 I(0:0x56268619b580)
#这块指令定义了一个Crypt类,且主体代码在地址 0x56268619b580
010 TCLASSR2
012 METHODR3 I(1:0x56268619bd80)
015 DEF R2 :check
#这类模块定义了一个名为check的函数,且主体在地址 0x56268619bd80
018 SSEND R2 :gets n=0 (0x00)#R2=gets()
022 SEND R2 :chomp n=0 (0x00)#R2=R2.chomp
#SEND和SSEND系列主要用于函数调用,其中SSEND是直接调用,而SEND是内置函数
# “:” 后面为函数名 n=0表示用到R2后连续0个寄存器
#如果n=x 这表明函数调用时需要用到额外x个R2之后的寄存器 如R2、R3、R4...注意是连续的
029 MOVE R3 R1 ; R1:p
032 SSEND R2 :check n=1 (0x01)
#上例则是n=1 SSEND 调用需要用到R2和R3 即 R2=check(R3) 而R3又是p即我们的输入 R2为函数的返回值
分析知我们的输入传入check函数,如果返回真则输出yes,故主要加密逻辑在check函数中。跟随check函数声明时的地址到函数主体,字节码如下。
可见在定义局部变量时格式如下
local variable names:
R1:p
R2:& #参数和局部变量的分界线 即R1是参数 R3-R7为局部变量
R3:i
R4:lst_ch
R5:c
R6:k
R7:cipher_text
之后根据opcode解析指令,首先是赋值
004 LOADI_0 R3 ; R3:i #i=0
006 LOADI_0 R4 ; R4:lst_ch #lst_ch=0
#OPCODE(LOADI_1,B) R[a] = mrb_int(1)
#此类loadi_0 加载立即数进寄存器 立即数为"_"的数值
循环体结构
008 MOVE R8 R3 ; R3:i
011 MOVE R9 R1 ; R1:p
014 SEND R9 :length n=0 (0x00) # R9=R9.length
018 LTR8 R9 # R8=i<R9
020 JMPNOTR8 086 # if(!R8) jmp 到 086的地址
024 MOVE R8 R1 ; R1:p
027 MOVE R9 R3 ; R3:i
030 GETIDXR8 R9 #R8=R8[R9] 即取出 R8=p[i]
032 SEND R8 :ordn=0 (0x00) #R8=ord(p[i])
036 MOVE R5 R8 ; R5:c
039 MOVE R8 R5 ; R5:c #c=R8
042 MOVE R9 R4 ; R4:lst_ch
045 SEND R8 :^ n=1 (0x01) #R8=R8^R9 R8=p[i]^lst_h
049 MOVE R9 R3 ; R3:i
052 ADDI R9 1#R9=i+1
055 SEND R8 :^ n=1 (0x01) #R8^=(i+1)
059 SEND R8 :chrn=0 (0x00) #R8=chr(R8)
063 MOVE R9 R1 ; R1:p
066 MOVE R10 R3 ; R3:i
069 MOVE R11 R8
072 SETIDXR9 R10 R11 #p[i]=R8
074 MOVE R8 R5 ; R5:c
077 MOVE R4 R8 ; R4:lst_ch #lst_ch=c
080 ADDI R3 1 ; R3:i #i+=1
083 JMP 008
之后是调用加密类的函数进行加密
089 GETCONST R8 Crypt
092 GETMCNST R8 R8::CIPHER
095 MOVE R9 R1 ; R1:p
098 MOVE R10 R6 ; R6:k
101 SEND R8 :encryptn=2 (0x02) #R8 = Crypt::CIPHER.encrypt(p,k)
105 MOVE R7 R8 ; R7:cipher_text
108 MOVE R8 R7 ; R7:cipher_text
111 STRINGR9 L(1); f469358b7f165145116e127ad6105917bce5225d6d62a714c390c5ed93b22d8b6b102a8813488fdb
114 EQR8 R9#if R8==R9
116 JMPNOTR8 124
120 LOADT R8
122 RETURNR8#R8=true
124 LOADF R8
126 RETURNR8#R9=fale
上述函数翻译后以python呈现如下
def check(p):
lst_ch=0
for i in range(len(p)):
c=ord(p[i])
p[i]=ord(p[i])^lst_ch
p[i]^=(i+1)
lst_ch=c
enc=encrypt(p)
if enc=='f469358b7f165145116e127ad6105917bce5225d6d62a714c390c5ed93b22d8b6b102a8813488fdb':
return True
else:
return False
查看Crypto类相关函数定义
irep 0x56268619b580 nregs=3 nlocals=1 pools=0 syms=1 reps=1 ilen=12
000 LOADNIL R1
002 LOADNIL R2
004 CLASS R1 :CIPHER #定义了一个子类 CIPHER
007 EXEC R1 I(0:0x56268619b650)
010 RETURNR1
转到CIPHER子类的定义
irep 0x56268619b650 nregs=3 nlocals=1 pools=0 syms=6 reps=4 ilen=55
000 LOADI32 R1 305419896#R1=305419896
006 SETCONST XX R1 #XX=305419896
009 LOADI R1 16
012 SETCONST YY R1 #YY=16
015 LOADSELF R1
017 SCLASSR1
019 METHODR2 I(0:0x56268619b790)
022 DEF R1 :encrypt #encrypt
025 TCLASSR1
027 METHODR2 I(1:0x56268619b830)
030 DEF R1 :encrypt #私有的encrypt函数
033 SSEND R1 :privaten=0 (0x00)
037 TCLASSR1
039 METHODR2 I(2:0x56268619bb50)
042 DEF R1 :to_key #to_key函数
045 TCLASSR1
047 METHODR2 I(3:0x56268619bc20)
050 DEF R1 :enc_one #enc_one函数
053 RETURNR1
其中SCLASS singleton_class
Singleton 是一种确保类只能有一个对象的设计模式。可以简单理解为该函数内可以创建本类的对象,完成一些私有函数的调用。
根据类的定义,Crypt::CIPHER.encrypt调用的是public的encrypt函数,故转到其定义。
irep 0x56268619b790 nregs=9 nlocals=5 pools=0 syms=3 reps=0 ilen=29
local variable names:
R1:t
R2:p
R3:&
R4:cip
000 ENTER 2:0:0:0:0:0:0 (0x80000)
004 GETCONST R5 CIPHER
007 SEND R5 :newn=0 (0x00)
011 MOVE R4 R5 ; R4:cip #cip=CIPHER.new()
014 MOVE R5 R4 ; R4:cip
017 MOVE R6 R1 ; R1:t
020 MOVE R7 R2 ; R2:p
023 SEND R5 :encryptn=2 (0x02) #r5=cip.encrypt(t,p) 私有函数
#这里参数容易混淆 t和p是传入的p(输入)和k调用encrypt
前面的工作主要是通过公有函数encrypt接口使用singleton_class
来创建一个CIPHER对象,进而调用私有的encrypt函数。
irep 0x56268619b830 nregs=16 nlocals=11 pools=1 syms=8 reps=1 ilen=346
local variable names:
R1:t
R2:p
R3:&
R4:key
R5:c
R6:n
R7:num1
R8:num2
R9:enum1
R10:enum2
000 ENTER 2:0:0:0:0:0:0 (0x80000)
004 MOVE R12 R2 ; R2:p
007 SSEND R11 :to_key n=1 (0x01)
011 MOVE R4 R11 ; R4:key #key=to_key(p)
014 ARRAY R5 R5 0 ; R5:c #c=[]
017 LOADI_0 R6 ; R6:n #n=0
019 MOVE R11 R6 ; R6:n
022 MOVE R12 R1 ; R1:t
025 SEND R12 :length n=0 (0x00) #R12=t.length()
029 LTR11 R12# if(n<t.length())
031 JMPNOTR11 327
035 MOVE R11 R1 ; R1:t
038 MOVE R12 R6 ; R6:n
041 GETIDXR11 R12 #R11=t[n]
043 SEND R11 :ordn=0 (0x00)
047 SEND R11 :to_i n=0 (0x00) #R11=ord(R11).to_i
051 LOADI R12 24
054 SEND R11 :<< n=1 (0x01) #R11=R11<<24
058 MOVE R7 R11 ; R7:num1
061 MOVE R11 R7 ; R7:num1
064 MOVE R12 R1 ; R1:t
067 MOVE R13 R6 ; R6:n
070 ADDI R13 1
073 GETIDXR12 R13
075 SEND R12 :ordn=0 (0x00)
079 SEND R12 :to_i n=0 (0x00)
083 LOADI R13 16
086 SEND R12 :<< n=1 (0x01)
090 ADD R11 R12
092 MOVE R7 R11 ; R7:num1
095 MOVE R11 R7 ; R7:num1
098 MOVE R12 R1 ; R1:t
101 MOVE R13 R6 ; R6:n
104 ADDI R13 2
107 GETIDXR12 R13
109 SEND R12 :ordn=0 (0x00)
113 SEND R12 :to_i n=0 (0x00)
117 LOADI R13 8
120 SEND R12 :<< n=1 (0x01)
124 ADD R11 R12
126 MOVE R7 R11 ; R7:num1
129 MOVE R11 R7 ; R7:num1
132 MOVE R12 R1 ; R1:t
135 MOVE R13 R6 ; R6:n
138 ADDI R13 3
141 GETIDXR12 R13
143 SEND R12 :ordn=0 (0x00)
147 SEND R12 :to_i n=0 (0x00)
151 ADD R11 R12
153 MOVE R7 R11 ; R7:num1 #num1=(t[n]<<24)|(t[n+1]<<16)|(t[n+2]<<8)|t[n+3]
156 MOVE R11 R1 ; R1:t
159 MOVE R12 R6 ; R6:n
162 ADDI R12 4
165 GETIDXR11 R12
167 SEND R11 :ordn=0 (0x00)
171 SEND R11 :to_i n=0 (0x00)
175 LOADI R12 24
178 SEND R11 :<< n=1 (0x01)
182 MOVE R8 R11 ; R8:num2
185 MOVE R11 R8 ; R8:num2
188 MOVE R12 R1 ; R1:t
191 MOVE R13 R6 ; R6:n
194 ADDI R13 5
197 GETIDXR12 R13
199 SEND R12 :ordn=0 (0x00)
203 SEND R12 :to_i n=0 (0x00)
207 LOADI R13 16
210 SEND R12 :<< n=1 (0x01)
214 ADD R11 R12
216 MOVE R8 R11 ; R8:num2
219 MOVE R11 R8 ; R8:num2
222 MOVE R12 R1 ; R1:t
225 MOVE R13 R6 ; R6:n
228 ADDI R13 6
231 GETIDXR12 R13
233 SEND R12 :ordn=0 (0x00)
237 SEND R12 :to_i n=0 (0x00)
241 LOADI R13 8
244 SEND R12 :<< n=1 (0x01)
248 ADD R11 R12
250 MOVE R8 R11 ; R8:num2
253 MOVE R11 R8 ; R8:num2
256 MOVE R12 R1 ; R1:t
259 MOVE R13 R6 ; R6:n
262 ADDI R13 7
265 GETIDXR12 R13
267 SEND R12 :ordn=0 (0x00)
271 SEND R12 :to_i n=0 (0x00)
275 ADD R11 R12
277 MOVE R8 R11 ; R8:num2#num2=(t[n+4]<<24)|(t[n+5]<<16)|(t[n+6]<<8)|t[n+7]
280 MOVE R12 R7 ; R7:num1
283 MOVE R13 R8 ; R8:num2
286 MOVE R14 R4 ; R4:key
289 SSEND R11 :enc_onen=3 (0x03) #R11=enc_one(num1,num2,key)
293 AREF R9 R11 0 ; R9:enum1 #enum1=R11[0]
297 AREF R10 R11 1 ; R10:enum2 #enum1=R11[1]
301 MOVE R11 R5 ; R5:c
304 MOVE R12 R9 ; R9:enum1 # << 在ruby对于数组运算类似append
307 SEND R11 :<< n=1 (0x01) #c.append(enum1)
311 MOVE R11 R5 ; R5:c
314 MOVE R12 R10 ; R10:enum2
317 SEND R11 :<< n=1 (0x01)
321 ADDI R6 8 ; R6:n #c.append(enum2)
324 JMP 019
327 MOVE R11 R5 ; R5:c
330 BLOCK R12 I(0:0x56268619ba80)
333 SENDB R11 :collectn=0 (0x00) #迭代数组 进行sprintf(" %.8x ")
337 STRINGR12 L(0);
340 SEND R11 :join n=1 (0x01) #R11.join把密文连起来
344 RETURNR11
irep 0x56268619ba80 nregs=7 nlocals=3 pools=1 syms=1 reps=0 ilen=16
local variable names:
R1:x
R2:&
000 ENTER 1:0:0:0:0:0:0 (0x40000)
004 STRINGR4 L(0); %.8x
007 MOVE R5 R1 ; R1:x
010 SSEND R3 :sprintfn=2 (0x02)
014 RETURNR3
这步函数的主要操作是将输入4个一组大端序转int,之后两个一组num1和num2和key调用enc_one函数。key有字符串unpack得到其中L表示将4个char转为一个无符号的长整型。
#函数调用方式,循环迭代
330 BLOCK R12 I(0:0x56268619ba80) #指明代码块
333 SENDB R11 :collectn=0 (0x00)#调用
#test
num=5
num.times {|i| print i," "}
005 BLOCK R3 I(0:0x55741d0c6c80) #对应{}内的代码
008 SENDB R2 :times n=0 (0x00)
#跳转到一个代码块迭代执行 可以通过GETUPVAR获取函数的局部变量
enc_one函数如下
irep 0x56268619bc20 nregs=11 nlocals=8 pools=0 syms=2 reps=1 ilen=42
local variable names:
R1:num1
R2:num2
R3:key
R4:&
R5:y
R6:z
R7:s
000 ENTER 3:0:0:0:0:0:0 (0xc0000)
004 MOVE R8 R1 ; R1:num1
007 MOVE R9 R2 ; R2:num2
010 LOADI_0 R10
012 MOVE R5 R8 ; R5:y #y=num1
015 MOVE R6 R9 ; R6:z #z=num2
018 MOVE R7 R10 ; R7:s #s=0
021 GETCONST R8 YY #R8=16
024 BLOCK R9 I(0:0x56268619bcf0)
027 SENDB R8 :times n=0 (0x00) #times(16) 即循环16次执行 0x56268619bcf0处的代码
031 MOVE R8 R5 ; R5:y
034 MOVE R9 R6 ; R6:z
037 ARRAY R8 R8 2
040 RETURNR8
irep 0x56268619bcf0 nregs=10 nlocals=3 pools=1 syms=5 reps=0 ilen=186
local variable names:
R1:i
R2:&
000 ENTER 1:0:0:0:0:0:0 (0x40000)
004 GETUPVAR R3 5 0 #获取局部变量 R3=R5:y
008 GETUPVAR R4 6 0 #R4=R6:z
012 LOADI_3 R5#R5=3
014 SEND R4 :<< n=1 (0x01)#R4=z<<3
018 GETUPVAR R5 6 0
022 LOADI_5 R6
024 SEND R5 :>> n=1 (0x01)#R5=z>>5
028 SEND R4 :^ n=1 (0x01)#R4=(z<<3)^(z>>5)
032 GETUPVAR R5 6 0
036 ADD R4 R5#R4=((z<<3)^(z>>5))+z
038 GETUPVAR R5 7 0 #R5=s
042 GETUPVAR R6 3 0 #R6=key
046 GETUPVAR R7 7 0 #R7=s
050 LOADI R8 11
053 SEND R7 :>> n=1 (0x01)#R7=s>>11
057 ADDI R7 1 #R7=(s>>11)+1
060 LOADI_3 R8
062 SEND R7 :& n=1 (0x01)#R7=((s>>11)+1)&3
066 GETIDXR6 R7#R6=key[((s>>11)+1)&3]
068 ADD R5 R6#R5=s+key[((s>>11)+1)&3]
070 SEND R4 :^ n=1 (0x01)#R4=(((z<<3)^(z>>5))+z)^(s+key[((s>>11)+1)&3])
074 ADD R3 R4#y+=(((z<<3)^(z>>5))+z)^(s+key[((s>>11)+1)&3])
076 SETUPVAR R3 5 0
080 LOADL R4 L(0); 4294967295 #hex()=0xffffffff
083 SEND R3 :& n=1 (0x01)
087 SETUPVAR R3 5 0 #y = y&0xffffffff
091 GETUPVAR R3 7 0
095 GETCONST R4 XX
098 ADD R3 R4
100 SETUPVAR R3 7 0 #s+=XX
104 GETUPVAR R3 6 0 #R3=z
108 GETUPVAR R4 5 0 #R4=y
112 LOADI_3 R5
114 SEND R4 :<< n=1 (0x01)
118 GETUPVAR R5 5 0
122 LOADI_5 R6
124 SEND R5 :>> n=1 (0x01)
128 SEND R4 :^ n=1 (0x01)
132 GETUPVAR R5 5 0
136 ADD R4 R5 #((y<<3)^(y>>5)+y)
138 GETUPVAR R5 7 0
142 GETUPVAR R6 3 0
146 GETUPVAR R7 7 0
150 ADDI R7 1
153 LOADI_3 R8
155 SEND R7 :& n=1 (0x01)
159 GETIDXR6 R7 #R6=key[(s+1)&3]
161 ADD R5 R6
163 SEND R4 :^ n=1 (0x01)
167 ADD R3 R4
169 SETUPVAR R3 6 0#z+=((y<<3)^(y>>5)+y)^(s+key[(s+1)&3])
173 LOADL R4 L(0); 4294967295 #z&=0xffffffff
176 SEND R3 :& n=1 (0x01)
180 SETUPVAR R3 6 0
184 RETURNR3
上述操作实现了一个魔改的Tea系列加密,以C的形式给出如下。
#define ut32 unsigned int
#define delta 305419896
void encrypt(ut32* src, ut32* k) {
ut32 sum = 0;
ut32 y = src[0];
ut32 z = src[1];
for (int i = 0; i < 16; i++) {
y += (((z << 3) ^ (z >> 5)) + z) ^ (sum + k[((sum >> 11) + 1) & 3]);
sum += delta;
z += (((y << 3) ^ (y >> 5)) + y) ^ (sum + k[(sum & 3)]);
}
src[0] = y;
src[1] = z;
}
综上,对输入先进行异或之后是魔改的Tea,把密文的16进制拼起来比对,解密脚本如下。
from Crypto.Util.number import *
s='f469358b7f165145116e127ad6105917bce5225d6d62a714c390c5ed93b22d8b6b102a8813488fdb'
c=[]
for i in range(0,len(s),8): #8各一组4字节
c.append(int(s[i:i+8],16))
print(c)
"""
[4100535691, 2132169029, 292426362, 3591395607, 3169133149, 1835181844, 3281044973, 2477927819, 1796221576, 323522523]
"""
k='aaaassssddddffff'
key=[]
for i in range(0,len(k),4):
key.append(bytes_to_long(k[i:i+4].encode()))
print(key)
"""
[1633771873, 1936946035, 1684300900, 1717986918]
"""
解密tea
#include<iostream>
#define ut32 unsigned int
#define delta 305419896
void decrypt(ut32* enc, ut32* k) {
ut32 sum = delta * 16;
ut32 y = enc[0];
ut32 z = enc[1];
for (int i = 0; i < 16; i++) {
z -= (((y << 3) ^ (y >> 5)) + y) ^ (sum + k[(sum + 1) & 3]);
sum -= delta;
y -= (((z << 3) ^ (z >> 5)) + z) ^ (sum + k[((sum >> 11) + 1) & 3]);
}
enc[0] = y;
enc[1] = z;
}
int main() {
ut32 enc[10] = { 4100535691, 2132169029, 292426362, 3591395607, 3169133149, 1835181844, 3281044973, 2477927819, 1796221576, 323522523 };
ut32 k[4] = { 1633771873, 1936946035, 1684300900, 1717986918 };
for (int i = 0; i < 10; i += 2)
decrypt(enc + i, k);
for (int i = 0; i < 10; i++)
printf("%08x", enc[i]);
return 0;
}
//67080e02194b500d5c585f0b5e40461511470a08154211560d47491e04031d262771217626242765
解密异或
c=bytes.fromhex('67080e02194b500d5c585f0b5e40461511470a08154211560d47491e04031d262771217626242765')
c=list(c)
c[0]^=1
for i in range(1,len(c)):
c[i]^=(i+1)
c[i]^=c[i-1]
print(bytes(c))
#flag{6ad1c70c-daa4-11ec-9d64-0242ac1200}
0x02 总结
mruby字节码整体上比较可读,并且其基于寄存器的结构让传参并不是太复杂。不过要想快速恢复程序逻辑还要熟悉字节码的指令格式以及创建函数、对象和函数调用等模块的结构。
0x03 参考
- 本文作者: Lu1u
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/1643
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!