Anode是一道类型非常经典的逆向题,请求用户输入flag,判断是否正确:
从IDA给出的信息来判断,这题是由NodeJS语言进行编写,然后NEXE打包成可执行文件:
NEXE会将JS源码直接打包到EXE中,不经过任何加密或者压缩,所以很容易使用十六进制编辑工具将源码提取出来,搜索特征字符串“Enter flag”,就能快速定位到源码位置:
JS代码如下所示,分析代码可知,程序接受用户的输入flag,经过了一个非常长的switch运算最后和加密后的flag进行比较,解题必须将加密的flag逆推回去。
readline.question(`Enter flag: `, flag => {
readline.close();
if (flag.length !== 44) {
console.log("Try again.");
process.exit(0);
}
var b = [];
for (var i = 0; i < flag.length; i++) {
b.push(flag.charCodeAt(i));
}
// something strange is happening...
if (1n) {
console.log("uh-oh, math is too correct...");
process.exit(0);
}
var state = 1337;
while (true) {
state ^= Math.floor(Math.random() * (2**30));
switch (state) {
case 306211:
if (Math.random() < 0.5) {
b[30] -= b[34] + b[23] + b[5] + b[37] + b[33] + b[12] + Math.floor(Math.random() * 256);
b[30] &= 0xFF;
} else {
b[26] -= b[24] + b[41] + b[13] + b[43] + b[6] + b[30] + 225;
b[26] &= 0xFF;
}
state = 868071080;
continue;
case 311489:
if (Math.random() < 0.5) {
b[10] -= b[32] + b[1] + b[20] + b[30] + b[23] + b[9] + 115;
b[10] &= 0xFF;
} else {
b[7] ^= (b[18] + b[14] + b[11] + b[25] + b[31] + b[21] + 19) & 0xFF;
}
state = 22167546;
continue;
case 755154:
if (93909087n) {
b[4] -= b[42] + b[6] + b[26] + b[39] + b[35] + b[16] + 80;
b[4] &= 0xFF;
} else {
b[16] += b[36] + b[2] + b[29] + b[10] + b[12] + b[18] + 202;
b[16] &= 0xFF;
}
state = 857247560;
continue;
case 1045388446:
if (Math.random() < 0.5) {
b[33] += b[40] + b[17] + b[43] + b[21] + b[36] + b[23] + 76;
b[33] &= 0xFF;
} else {
b[20] -= b[37] + b[30] + b[12] + b[15] + b[6] + b[7] + 88;
b[20] &= 0xFF;
}
state = 204284567;
continue;
//......省略,代码非常长
//......省略,代码非常长
//......省略,代码非常长
case 1071767211:
if (Math.random() < 0.5) {
b[30] ^= (b[42] + b[9] + b[2] + b[36] + b[12] + b[16] + 241) & 0xFF;
} else {
b[20] ^= (b[41] + b[2] + b[40] + b[21] + b[36] + b[17] + 37) & 0xFF;
}
state = 109621765;
continue;
default:
console.log("uh-oh, math.random() is too random...");
process.exit(0);
}
break;
}
var target = [106, 196, 106, 178, 174, 102, 31, 91, 66, 255, 86, 196, 74, 139, 219, 166, 106, 4, 211, 68, 227, 72, 156, 38, 239, 153, 223, 225, 73, 171, 51, 4, 234, 50, 207, 82, 18, 111, 180, 212, 81, 189, 73, 76];
if (b.every((x,i) => x === target[i])) {
console.log('Congrats!');
} else {
console.log('Try again.');
}
});
这题有埋坑的地方,题目源码很明显给了提示,正常情况下,if块将会被执行,但是从实际运行结果来看,这个字符串并没有输出,程序也没有退出,很明显,这题修改了V8引擎。
不仅bigint类型的真假值判断被修改了,随机数生成也被修改了,因为程序每次都会走到相同的switch分支:
正常的解题思路是分析V8源码修改的地方,使用bindiff和对比源码的方法去分析,不过由于时间有限,我决定另辟蹊径,通过黑盒的方法去把这些关键信息收集到,即使我对它如何修改V8引擎细节一无所知。
通过修改exe中js源码的方法,将随机数值以及每个if判断数值为真或为假直接打印出来,通过收集到的信息修复JS代码,使得它在正常的JS引擎也能运行起来。如何修改呢?方法非常简单,只需要对ReadFile下断点,在它把JS数据读入内存中后进行修改:
直接修改js源码,插入两段代码,一个获取随机值:
一个获取if判断的值,为真还是为假:
知道以上信息就可以着手修复JS代码了,将if的值直接替换成true或者false:
import subprocess
import right
import re
import right_judge
all_case_num=[]
def fix_js():
count=1
n_c=0
fix_flag=False
out_line=''
with open('test_no_input.js','rt') as fd:
line=fd.readline()
while line:
if 'case' in line:
fix_flag=True
if fix_flag and 'if' in line and '>' not in line and '<' not in line:
p = re.compile(r'[(](.*?)[)]', re.S)
all_case_num.append(re.findall(p, line)[0])
if right_judge.all_case_judge[n_c]:
line=' if (true) {n'
else:
line=' if (false) {n'
fix_flag=False
n_c+=1
out_line+=line
line=fd.readline()
print(out_line)
with open('fixed_test.js','wt+') as fdo:
fdo.write(out_line)
if __name__ == '__main__':
fix_js()
得到JS内容如下:
为了进一步提高可读性,编写脚本进行进一步化简,将switch结构修改成顺序执行,并将随机值的运算结果进行求解。
import subprocess
import right
import re
import flare_random
import math
all_case_num=[]
end_num=185078700
random_magic_str='getrand(pos++)'
def fix_js():
random_pos=0
rcn=right.right_case
rand_num=flare_random.randnum
all_const_num=''
out_line=[]
with open('fixed_test.js','rt') as fd:
data=fd.read()
for case_num in rcn:
random_pos+=1
if case_num==end_num:
break
case_pos=data.find(str(case_num), 0)
cond_block_start=data.find('(',case_pos)
cond_block_end=data.find('{',case_pos)
cond_block=data[cond_block_start:cond_block_end]
if_true_block_start=data.find('{',case_pos)
if_true_block_end=data.find('}', case_pos)
if_true_block=data[if_true_block_start+1:if_true_block_end]
if_false_block_start=data.find('{', if_true_block_end)
if_false_block_end=data.find('}', if_true_block_end+1)
cond_bool=None
if random_magic_str in cond_block:
if_rand_num=rand_num[random_pos]
random_pos+=1
sol_cond=cond_block.replace(random_magic_str,str(if_rand_num))
cond_bool=eval(sol_cond)
if 'true' in cond_block:
cond_bool=True
if 'false' in cond_block:
cond_bool=False
assert cond_bool!=None
if_false_block=data[if_false_block_start+1:if_false_block_end]
assert if_true_block.count(random_magic_str)<=1
assert if_false_block.count(random_magic_str)<=1
if cond_bool:
exec_block=if_true_block
else:
exec_block=if_false_block
#print(exec_block)
line=exec_block
if 'Math.floor(' in exec_block:
m_start=exec_block.find('Math.floor',0)
m_end=exec_block.find(')',m_start)
m_end=exec_block.find(')',m_end+1)
math_semt=exec_block[m_start:m_end+1]
#print(math_semt)
assert random_magic_str in math_semt
math_semt_rand_num=rand_num[random_pos]
sol_semt=math_semt.replace(random_magic_str,str(math_semt_rand_num)).replace('Math','math')
#print(eval(sol_semt))
random_pos+=1
line=exec_block.replace(math_semt,str(eval(sol_semt)))
if '&=' in line:
line_list=line.split('n')
formatline=line_list[1].replace(' ','')
else:
line_list=line.split('n')
formatline=line_list[1].replace(' ','').replace('&0xFF','').replace('(','').replace(')','')
#formatline)
formatline_list=formatline.split('=')
left_v=formatline_list[0][0:-1]
op=formatline[len(left_v):len(left_v)+2]
right_v=formatline_list[1].strip(';')
add_nums=right_v.split('+')
assert len(add_nums)==7
const_num=add_nums[6]
#获取所有下标,并且排序
p = re.compile(r'[[](.*?)[]]', re.S)
all_add_num=[]
for i in range(6):
all_add_num.append(int(re.findall(p, add_nums[i])[0]))
all_add_num.sort()
#print(all_add_num)
fix_line=left_v+op
for i in all_add_num:
fix_line+='b[%d]+'%i
fix_line+=const_num
fix_line+='n'
out_line.append(fix_line)
print(''.join(out_line))
if __name__ == '__main__':
fix_js()
最后得到结果如下所示:
观察这个得到化简的结果,只有三种运算,分别是:+= 、^=、,-= ,为了可以将密文逆推回去,直接将 -= 替换成 += ,将 += 替换成 -= ,运算顺序进行反转就可以解密flag:Flag为:n0t_ju5t_A_j4vaSCriP7_ch4l1eng3@flare-on.com
这个题目虽然不难,但是十分考验细节,比如会考察选手是否能够快速发现V8引擎被修改过的痕迹,考察选手对算法的一些观察能力,总的来说,细节决定成败,尤其是逆向分析一些算法的时候,一点点修改就可能导致运算结果并不相同。
本文作者:ChaMd5安全团队
本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/192610.html