这篇文章诞生于10月11日,这主要是怕11月比赛结束后自己啥也不记得了,就先写出来放着吧,当作是解题的纪念。

01 - Flaredle

Welcome to Flare-On 9!

You probably won’t win. Maybe you’re like us and spent the year playing Wordle. We made our own version that is too hard to beat without cheating.

Play it live at: Flaredle

附件下载

游戏界面

字谜游戏,但这不关键,直接从script.js中就能推断出正确答案的位置:

1
2
const CORRECT_GUESS = 57;
let rightGuessString = WORDS[CORRECT_GUESS];

flareonisallaboutcats@flare-on.com

02 - Pixel Poker

I said you wouldn’t win that last one. I lied. The last challenge was basically a captcha. Now the real work begins. Shall we play another game?

附件下载

点开是一个窗口,题意为点击正确的像素。IDA打开能找到wWinMain,应该是一个典型的Win32程序。这种程序找关键逻辑一般是找到注册窗口类(RegisterClassEx)前的WNDCLASSEX结构体,结构体中的lpfnWndProc字段即为主窗口循环函数,里面大概率就有你想要的了。

点开WndProc函数,第二个参数是消息代码,512对应光标移动事件(WM_MOVE),513是鼠标左键按下事件(WM_LBUTTONDOWN),514是鼠标左键释放事件(WM_LBUTTONUP)。观察得判断逻辑如下:

验证逻辑

sub_4015D0会用RC4将图片逐像素解密。但那不重要,我们只要把40148640149D处的跳转nop掉即可。随后打开游戏,随便点一个地方,此时窗口会卡一下(因为正在解密),随后就显示出了flag:

解密后的图片

w1nN3r_W!NneR_cHick3n_d1nNer@flare-on.com

03 - Magic 8 Ball

You got a question? Ask the 8 ball!

附件下载

游戏没太看懂,所以直接看代码。经过一番查找,在sub_4024E0末尾发现了一堆if。这堆if应该是在逐字符验证方向键(LLURULDUL),然后判断输入是否为gimme flag pls?,最后调用sub_851A10把代表方向的字符串当作key对一些数据进行RC4解密:

RC4解密数据

于是可以推测出,如果我们输入那些字符串,然后再按那些方向键,最后回车确定,即可得到正确的flag。这里直接用代码求flag:

1
2
3
4
from Crypto.Cipher import ARC4

flag = bytes((53, 42, 18, 51, 135, 87, 100, 178, 0, 239, 166, 52, 1, 224, 222, 62, 1, 33, 236, 64, 38, 29, 105, 176, 176, 105, 178, 123, 86, 34, 235, 6, 190, 242, 93, 203, 121, 15, 43, 81, 85))
print(ARC4.new(b'LLURULDUL').decrypt(flag).decode('ascii'))

U_cRackeD_th1$_maG1cBaLL_!!_@flare-on.com

04 - darn_mice

“If it crashes its user error.” -Flare Team

附件下载

程序接受一个参数input,然后创建一页可读可写可执行的内存,如果flag的每一位与已知值相加后填入此内存执行能不崩溃,则程序认证成功。那么容易想到,此内存应该填ret指令,它的机器码是0xC3,那么反过来求input就行:

1
2
shellcode = [80, 94, 94, 163, 79, 91, 81, 94, 94, 151, 163, 128, 144, 163, 128, 144, 163, 128, 144, 163, 128, 144, 163, 128, 144, 163, 128, 144, 163, 128, 144, 162, 163, 107, 127]
print(bytes(map(lambda n: (195 - n) & 255, shellcode)).decode('ascii'))

得到see three, C3 C3 C3 C3 C3 C3 C3! XD。把它们作为命令行的参数(记得带上双引号),即可获取flag。

i_w0uld_l1k3_to_RETurn_this_joke@flare-on.com

05 - T8

FLARE FACT #823: Studies show that C++ Reversers have fewer friends on average than normal people do. That’s why you’re here, reversing this, instead of with them, because they don’t exist.

We’ve found an unknown executable on one of our hosts. The file has been there for a while, but our networking logs only show suspicious traffic on one day. Can you tell us what happened?

附件下载

点开程序分析没反应,遂IDA查看。程序是C++编译的,所以代码显得很凌乱。接下来就是长时间的枯燥分析,这里只说分析结果,即程序大概干了什么(注意下文中字符串大多是UTF-16LE编码的)。

  • 在程序初始化阶段(main执行前),程序读取当前时间,以此获取一个随机数R,并令字符串S = "FO9"

  • 程序进入main,检测现在时间是否为某月15号,否则Sleep12小时后再继续。

  • 初始化CClientSock对象。其中域名是hostname = "flare-on.com",请求方式是POST。随后创建字符串K = S + wstring(R),把它的MD5的hex值作为此对象的key。这个key在后面向网站POST时会用到。

  • 向网站发送字符串ahoy,发送时会先用key对数据进行RC4加密,然后再base64编码,随后收到数据D1

  • 调用sub_F84200解密D1,得到字符串F,把它加上"@" + hostname(这个就是flag了),并把F的MD5的hex值作为新的key。

    向网站发送字符串sce(发送方法同上),得到数据D2。调用sub_F843F0(其中有base64解码和RC4)解密数据,得到一段shellcode,接着程序执行这段shellcode,弹出这个:

    远程代码执行

  • shellcode分析结果:从fs寄存器读取PEB,从中获取PEB_LDR_DATA结构体,进而获取动态链接库加载列表。遍历此列表,找到名称中第7个字符为3的(实际上就是kernel32.dll),得到此库的基址。再从库的输入表中模糊寻找Fata????Exit*(最终找到FatalAppExitA),最后设置合适的参数调用它。

  • 至此程序结束,main函数后面还有一些代码,但是它们是C++自动生成的析构过程,实际上程序运行shellcode后根本走不到那里。

对于详细的数据结构和虚拟函数的定义,可以下载此IDB文件查看:

IDB文件下载

这个程序应该改编自病毒,由于题目把网站换成了自己的,而且那段shellcode也人畜无害,所以我们不用担心中病毒,大胆运行就好。最后放上key的破解过程和流量包中两段数据的解密代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from Crypto.Cipher import ARC4
from Crypto.Hash import MD5
from base64 import b64decode
from ctypes import cdll

def getkey(i):
cdll.msvcrt.srand(i)
key = MD5.new(('FO9%d' % cdll.msvcrt.rand()).encode('utf-16le'))
return key.hexdigest().encode('utf-16le')

data = 'ydN8BXq16RE='
for i in range(32768):
try:
raw = ARC4.new(getkey(i)).decrypt(b64decode(data))
if raw.decode('utf-16le') == 'ahoy': break
except UnicodeDecodeError:
pass
else: assert 0

cdll.msvcrt.srand(i)
print(cdll.msvcrt.rand())

key = getkey(i)
data = 'TdQdBRa1nxGU06dbB27E7SQ7TJ2+cd7zstLXRQcLbmh2nTvDm1p5IfT/Cu0JxShk6tHQBRWwPlo9zA1dISfslkLgGDs41WK12ibWIflqLE4Yq3OYIEnLNjwVHrjL2U4Lu3ms+HQc4nfMWXPgcOHb4fhokk93/AJd5GTuC5z+4YsmgRh1Z90yinLBKB+fmGUyagT6gon/KHmJdvAOQ8nAnl8K/0XG+8zYQbZRwgY6tHvvpfyn9OXCyuct5/cOi8KWgALvVHQWafrp8qB/JtT+t5zmnezQlp3zPL4sj2CJfcUTK5copbZCyHexVD4jJN+LezJEtrDXP1DJNg=='
raw = ARC4.new(key).decrypt(b64decode(data))

key = MD5.new('i_s33_you_m00n@flare-on.com'.encode('utf-16le')).hexdigest().encode('utf-16le')
data = 'F1KFlZbNGuKQxrTD/ORwudM8S8kKiL5F906YlR8TKd8XrKPeDYZ0HouiBamyQf9/Ns7u3C2UEMLoCA0B8EuZp1FpwnedVjPSdZFjkieYqWzKA7up+LYe9B4dmAUM2lYkmBSqPJYT6nEg27n3X656MMOxNIHt0HsOD0d+'
raw = ARC4.new(key).decrypt(b64decode(data))
open('data2.txt', 'wb').write(raw)

i_s33_you_m00n@flare-on.com

06 - à la mode

FLARE FACT #824: Disregard flare fact #823 if you are a .NET Reverser too.

We will now reward your fantastic effort with a small binary challenge. You’ve earned it kid!

附件下载

程序是.NET的dll,用dnSpy反编译,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
namespace FlareOn
{
public class Flag
{
public string GetFlag(string password)
{
Decoder decoder = Encoding.UTF8.GetDecoder();
UTF8Encoding utf8Encoding = new UTF8Encoding();
string text = "";
byte[] array = new byte[64];
char[] array2 = new char[64];
byte[] bytes = utf8Encoding.GetBytes(password + "\0");
using (NamedPipeClientStream namedPipeClientStream = new NamedPipeClientStream(".", "FlareOn", PipeDirection.InOut))
{
namedPipeClientStream.Connect();
namedPipeClientStream.ReadMode = PipeTransmissionMode.Message;
namedPipeClientStream.Write(bytes, 0, Math.Min(bytes.Length, 64));
int byteCount = namedPipeClientStream.Read(array, 0, array.Length);
int chars = decoder.GetChars(array, 0, byteCount, array2, 0);
text += new string(array2, 0, chars);
}
return text;
}
}
}

可以看出,程序连接至命名管道FlareOn,发送密码并尝试获取flag。

先用IDA打开此dll,直接点击列表中第一个函数sub_1000,里面调用的sub_14AE引起了我的注意:这个函数在异或解密一些数据。我尝试用python手动解密,结果是Authorization Failed。对此函数查看xref,把所有字符串都解密一下,结果几乎已经出来了:sub_1094会创建名为\\.\pipe\FlareOn的命名管道,这和之前的代码对上了;sub_12F1会动态导入一系列WinAPI。进行名称修复后容易看出,sub_1000就是我们要找的函数,它会解密password和flag。写个脚本即可:

1
2
3
4
5
6
7
from Crypto.Cipher import ARC4

flag = bytes((0xE1, 0x60, 0xA1, 0x18, 0x93, 0x2E, 0x96, 0xAD, 0x73, 0xBB, 0x4A, 0x92, 0xDE, 0x18, 0x0A, 0xAA, 0x41, 0x74, 0xAD, 0xC0, 0x1D, 0x9F, 0x3F, 0x19, 0xFF, 0x2B, 0x02, 0xDB, 0xD1, 0xCD))
cipher = ARC4.new(bytes((0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x20, 0xEB, 0xFE)))
password = cipher.decrypt(bytes((0x3E, 0x39, 0x51, 0xFB, 0xA2, 0x11, 0xF7, 0xB9, 0x2C)))
print(password.decode('ascii'))
print(cipher.decrypt(flag).decode('ascii'))

M1x3d_M0dE_4_l1f3@flare-on.com

07 - anode

You’ve made it so far! I can’t believe it! And so many people are ahead of you!

附件下载

这是一个用nexe打包的Node.js程序。用记事本打开翻到最后,包含主要代码的js就出来了,把它抠出来方便分析,这里展示关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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;
// 此处省略1024个case
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.');
}

阅读代码,为了求出flag,我们面临两个问题,一是那个switch-case太大了,无法手动分析流程,二是程序似乎对整数判断的逻辑进行了修改,随机数也不那么随机了。

第一个问题先放一放,第二个问题我们可以自己写一段代码,然后看看程序输出的是什么。我们可以直接用自己写的代码把原代码替换掉,只要原程序的大小不发生改变即可(写完后才知道使用require把自己的代码引入应该是更好的方法)。运行一些测试用例后,我发现:

  • Math.random()并不随机,程序每次运行取出的随机数序列是固定的。

  • 程序应该对常量的判断逻辑进行了修改,而且只有当判断数字字面量的时候会有异常行为,判断变量则不会。例如:

    1
    2
    3
    4
    5
    >>> 1n ? true : false
    false
    >>> const v = 1n;
    >>> v ? true : false
    true

    这个规则适用于整数和bigint。

  • 对于整数,判断规则与正常规则恰好相反。

  • 对于bigint,如果是0则返回false,否则看String.valueOf(x)中是否含有"0",有则判断为true,否则为false

虽然我们仍然没有搞清楚程序是如何修改JavaScript行为的,但上面的分析对解这道题来说已经足够了。

下面来解决第一个问题。通过对每个case的观察,发现有这么些情况:

  • break分支

  • 正常分支:

    1
    2
    3
    4
    5
    6
    7
    8
    case xxx:
    if (Math.random() < 0.5 或 xxx 或 xxxn) {
    OP;
    } else {
    OP;
    }
    state = xxx;
    continue;

    OP表达式有3种情况:

    1
    2
    b[x] += b[c] + b[d] + b[e] + b[f] + b[g] + b[h] + t;
    b[x] &= 0xFF;
    1
    2
    b[x] -= b[c] + b[d] + b[e] + b[f] + b[g] + b[h] + t;
    b[x] &= 0xFF;
    1
    b[x] ^= (b[c] + b[d] + b[e] + b[f] + b[g] + b[h] + t) & 0xFF;

我们可以把这些case抽出来保存到flow.txt(这里使用了pyparsing库解析结构),用python模拟一遍,然后逆着走一遍流程即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import os, math
from pyparsing import *

ParserElement.inlineLiteralsUsing(Suppress)
Number = Word(nums)
Bigint = Regex('\d+n')
Element = ('b[' + Number + ']').setParseAction(lambda x: int(x[0]))
Appendix = Number().setParseAction(lambda x: {'type': 'int', 'value': int(x[0])}) |\
Suppress('Math.floor(Math.random() * 256)').setParseAction(lambda _: {'type': 'random'})
ExpRight = Group(OneOrMore(Element + '+'))('opr').setParseAction(lambda x: x.asList())
ExpAdd = Element('op') + Literal('+=')('token') + ExpRight + Appendix('addval') + ';' + Element + '&= 0xFF;'
ExpSub = Element('op') + Literal('-=')('token') + ExpRight + Appendix('addval') + ';' + Element + '&= 0xFF;'
ExpXor = Element('op') + Literal('^=')('token') + '(' + ExpRight + Appendix('addval') + ') & 0xFF;'
Pattern = (ExpAdd | ExpSub | ExpXor).setParseAction(lambda x: x.asDict())
Condition = (
Suppress('Math.random() < 0.5').setParseAction(lambda _: {'type': 'random'}) |\
Bigint().setParseAction(lambda x: {'type': 'bigint', 'value': int(x[0][:-1])}) |\
Number().setParseAction(lambda x: {'type': 'int', 'value': int(x[0])}) \
).setParseAction(lambda x: x[0])
trunk = (
'case' + Number('case').setParseAction(lambda x: int(x[0])) + ':' +\
'if (' + Condition('condition') + ') {' +\
Pattern('if') +\
'} else {' +\
Pattern('else') +\
'}' +\
'state = ' + Number('next').setParseAction(lambda x: int(x[0])) + ';' +\
'continue;'
).setParseAction(lambda x: x.asDict())
end = ('case' + Number('case').setParseAction(lambda x: int(x[0])) + ':' + 'break;').setParseAction(lambda x: x.asDict())
block = OneOrMore(trunk | end)

flow = block.parseFile('flow.txt', parseAll=True).asList()
for e in flow:
x = e.get('if')
if x: x.pop('if')
x = e.get('else')
if x: x.pop('else')
flow = dict(map(lambda e: (e.pop('case'), e), flow))
# flow中保存了所有case

# 从nodejs中获取随机数
offset, size, n = 0x35E3874, 0x4E8C8, 4096
code = '''\
for (let i = 0; i < %d; ++i){
console.log(Math.random());
}
''' % n
file = 'anode_.exe'
code = (code + 'process.exit(0);\n').ljust(size, ' ').encode('ascii')
exe = open(file, 'rb').read()
open(file, 'wb').write(exe[:offset] + code + exe[offset+size:])
with os.popen(file) as p:
rands = list(map(float, p.read().splitlines()))
assert len(rands) == n
rands.reverse()

def mybool(x: int, bigint: bool):
if bigint: return x != 0 and '0' in str(x)
else: return not x

# 走一遍流程,保存关键变量
records: list[tuple[int, str, list[int], int]] = []
current = 1337 ^ math.floor(rands.pop() * 2 ** 30)
while True:
trunk = flow.get(current)
if trunk is None: raise IndexError('cannot find branch %d' % current)
cond = trunk.get('condition')
if cond is None: break
if cond['type'] == 'random': # if (Math.random() < 0.5)
if rands.pop() < 0.5:
exp = trunk['if']
else: exp = trunk['else']
else: # if (xxx)
if mybool(cond['value'], cond['type'] == 'bigint'):
exp = trunk['if']
else: exp = trunk['else']
addval = exp['addval']
if addval['type'] == 'random': addval = math.floor(rands.pop() * 256)
else: addval = addval['value']
records.append((exp['op'], exp['token'], exp['opr'], addval))
next = trunk['next'] ^ math.floor(rands.pop() * 2 ** 30)
# print('%d -> %d' % (current, next))
current = next

# 把flag逆回来
flag = [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]
for op, token, opr, addval in reversed(records):
assert op not in opr
if token == '+=': flag[op] = (flag[op] - (sum(map(lambda x: flag[x], opr)) + addval)) & 255
elif token == '-=': flag[op] = (flag[op] + (sum(map(lambda x: flag[x], opr)) + addval)) & 255
elif token == '^=': flag[op] = (flag[op] ^ (sum(map(lambda x: flag[x], opr)) + addval)) & 255
else: assert 0
print(bytes(flag).decode('ascii'))

n0t_ju5t_A_j4vaSCriP7_ch4l1eng3@flare-on.com

08 - backdoor

I’m such a backdoor, decompile me why don’t you…

附件下载

这也是个.NET程序,但是C#代码被加密了:

加密的代码

通过7-zip打开exe,可以看到很多奇怪的段:

奇怪的段

其中5aeb2b97段格外地大,查看大小足足有10.8MB!但先别急,通过dnSpy动态调试可以看出程序流程大概是,先flare_74初始化,然后直接运行加密的代码,引发InvalidProgramException,接着带着此异常调用flare_70解密代码。

然而出题人玩了一个套娃,点开此函数发现又是一个try-catch组合,即此函数的代码也是加密的:

套娃加密

但所幸出题者没有再嵌套了,flare_71中即可看到解密flared_70的代码:

解密过程

传入的参数中,e用来指示要解密的方法名,args是传入的参数,m是要动态解析的外部符号,b是函数的字节码。可以看出,这些代码在做运行时解密。由于代码被加密,dnSpy无法正常保存,这里我比较懒,不想一个一个解密,于是使用了DotNetDetour库,这个库是专门用于hook的,把它用vs2022打开,新建一个项目并写如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
using DotNetDetour;
using System.Reflection;
using System.IO;
using System.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection.Emit;
using System.Text;
using System.Security.Cryptography;

namespace MyInjector
{
internal class Program
{
public static Assembly assemblyFlare;
static void Main(string[] args)
{
MethodHook.Install();
string file = "FlareOn.Backdoor.exe";
if (args.Length > 0)
{
file = args[0];
}
assemblyFlare = Assembly.LoadFrom(Path.GetFullPath(file));
assemblyFlare.EntryPoint.Invoke(null, new object[] { args });
}
}

public class Utils
{
public static string hexlify(byte[] bytes)
{
return string.Join("", bytes.Select(b => b.ToString("X2")));
}
public static string hexlify(Dictionary<uint, int> dict)
{
return string.Join("", dict.Select(d => d.Key.ToString("X") + "=" + d.Value.ToString("X8")));
}
}

public class ProxyFlare : IMethodHook
{
private static HashSet<string> symbols = new HashSet<string>();

[HookMethod("FlareOn.Backdoor.FLARE15")]
public static object flare_71(InvalidProgramException e, object[] args, Dictionary<uint, int> m, byte[] b)
{
DynamicMethod dynamicMethod = flare_71_Original(e, m, b);
MethodBase methodBase = new StackTrace(e).GetFrame(0).GetMethod();
if (!symbols.Contains(methodBase.Name))
{
foreach (var pair in m)
{
b[pair.Key] = (byte)pair.Value;
b[pair.Key + 1] = (byte)(pair.Value >> 8);
b[pair.Key + 2] = (byte)(pair.Value >> 16);
b[pair.Key + 3] = (byte)(pair.Value >> 24);
}
Console.WriteLine("{0} {1} {2}",
methodBase.Name,
Utils.hexlify(methodBase.GetMethodBody().GetILAsByteArray()),
Utils.hexlify(b)
);
symbols.Add(methodBase.Name);
}
return dynamicMethod.Invoke(null, args);
}
public static DynamicMethod flare_71_Original(InvalidProgramException e, Dictionary<uint, int> m, byte[] b)
{
StackTrace stackTrace = new StackTrace(e);
MethodBase method = stackTrace.GetFrame(0).GetMethod();
int metadataToken = method.MetadataToken;
Module module = method.Module;
MethodInfo methodInfo = (MethodInfo)module.ResolveMethod(metadataToken);
MethodBase methodBase = module.ResolveMethod(metadataToken);
ParameterInfo[] parameters = methodInfo.GetParameters();
Type[] array = new Type[parameters.Length];
SignatureHelper localVarSigHelper = SignatureHelper.GetLocalVarSigHelper();
for (int i = 0; i < array.Length; i++)
{
array[i] = parameters[i].ParameterType;
}
Type declaringType = methodBase.DeclaringType;
DynamicMethod dynamicMethod = new DynamicMethod("", methodInfo.ReturnType, array, declaringType, true);
DynamicILInfo dynamicILInfo = dynamicMethod.GetDynamicILInfo();
MethodBody methodBody = methodInfo.GetMethodBody();
foreach (LocalVariableInfo localVariableInfo in methodBody.LocalVariables)
{
localVarSigHelper.AddArgument(localVariableInfo.LocalType);
}
byte[] signature = localVarSigHelper.GetSignature();
dynamicILInfo.SetLocalSignature(signature);
foreach (KeyValuePair<uint, int> keyValuePair in m)
{
int value = keyValuePair.Value;
uint key = keyValuePair.Key;
int token;
if (value >= 0x70000000 && value < 0x7000FFFF)
{
token = dynamicILInfo.GetTokenFor(module.ResolveString(value));
}
else
{
MemberInfo memberInfo = declaringType.Module.ResolveMember(value, null, null);
if (memberInfo.GetType().Name == "RtFieldInfo")
{
token = dynamicILInfo.GetTokenFor(((FieldInfo)memberInfo).FieldHandle, ((TypeInfo)((FieldInfo)memberInfo).DeclaringType).TypeHandle);
}
else
{
if (memberInfo.GetType().Name == "RuntimeType")
{
token = dynamicILInfo.GetTokenFor(((TypeInfo)memberInfo).TypeHandle);
}
else
{
if (memberInfo.Name == ".ctor" || memberInfo.Name == ".cctor")
{
token = dynamicILInfo.GetTokenFor(((ConstructorInfo)memberInfo).MethodHandle, ((TypeInfo)((ConstructorInfo)memberInfo).DeclaringType).TypeHandle);
}
else
{
token = dynamicILInfo.GetTokenFor(((MethodInfo)memberInfo).MethodHandle, ((TypeInfo)((MethodInfo)memberInfo).DeclaringType).TypeHandle);
}
}
}
}
b[(int)key] = (byte)token;
b[(int)(key + 1U)] = (byte)(token >> 8);
b[(int)(key + 2U)] = (byte)(token >> 16);
b[(int)(key + 3U)] = (byte)(token >> 24);
}
dynamicILInfo.SetCode(b, methodBody.MaxStackSize);
return dynamicMethod;
}
}
}

从命令行运行,一段时间后,发现只解密了几个函数:flared_70flared_66flared_69flared_35flared_47flared_67flared_68。不过先别担心,因为这些是第二层的解密函数,它们是用来解密其他更多函数用的。我们先把输出保存到dump.txt中,然后把它们恢复一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import re

pattern = re.compile(r'([A-Za-z_][A-Za-z_\d]*) ([A-F\d]*) ([A-F\d]*)\n')
binary = open('FlareOn.Backdoor.exe', 'rb').read()

def replace(binary: bytes, org: bytes, rep: bytes):
assert len(org) == len(rep)
i = binary.find(org)
assert i != -1 and binary.find(org, i + 1) == -1
return binary[:i] + rep + binary[i+len(org):]

for line in open('dump.txt', 'r').readlines():
match = pattern.fullmatch(line)
assert match
name, org, code = match.group(1), bytes.fromhex(match.group(2)), bytes.fromhex(match.group(3))
if len(org) != len(code):
print('broken method body: %s' % name)
continue
binary = replace(binary, org, code)

open('flare1.exe', 'wb').write(binary)

这样我们就能看到这些函数的逻辑了。查看flared_70函数:

flared_70

这个函数先获取了要解密方法的token,然后调用flare_66获取与函数相关信息的SHA256(hex):

获取函数信息

然后调用flare_69从可执行文件中获取对应的段:

从exe中获取方法

这就解释了exe中那些奇怪的段名。随后,程序调用flare_46对数据解析RC4解密,最后调用flare_67修复并执行函数。这个函数的过程很长就不贴了,它的大意是手动解析字节码然后修复外部符号(token),最后动态执行函数。

那我们也如法炮制,写如下C#代码打印出要解密代码的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
using System.Reflection;
using System.IO;
using System.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection.Emit;
using System.Text;
using System.Security.Cryptography;

namespace MyInjector
{
internal class Program
{
public static Assembly assemblyFlare;
static void Main(string[] args)
{
string file = "flare1.exe";
if (args.Length > 0)
{
file = args[0];
}
assemblyFlare = Assembly.LoadFrom(Path.GetFullPath(file));
DecryptMethods();
}

static void DecryptMethods()
{
foreach (var type in assemblyFlare.GetTypes())
{
foreach (var methodInfo in type.GetMethods())
{
if (!methodInfo.Name.StartsWith("flared_")) continue;
MethodBody methodBody = methodInfo.GetMethodBody();
StringBuilder sign = new StringBuilder();
foreach (LocalVariableInfo localVariableInfo in methodBody.LocalVariables)
{
Type localType = localVariableInfo.LocalType;
if (localType != null)
{
sign.Append(localType.ToString());
}
}
foreach (ParameterInfo parameterInfo in methodInfo.GetParameters())
{
Type parameterType = parameterInfo.ParameterType;
if (parameterType != null)
{
sign.Append(parameterType.ToString());
}
}
IncrementalHash incrementalHash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
incrementalHash.AppendData(BitConverter.GetBytes(methodBody.GetILAsByteArray().Length));
incrementalHash.AppendData(Encoding.ASCII.GetBytes(methodInfo.Attributes.ToString()));
incrementalHash.AppendData(Encoding.ASCII.GetBytes(methodInfo.ReturnType.ToString()));
incrementalHash.AppendData(Encoding.ASCII.GetBytes(methodBody.MaxStackSize.ToString()));
incrementalHash.AppendData(Encoding.ASCII.GetBytes(sign.ToString()));
incrementalHash.AppendData(Encoding.ASCII.GetBytes(methodInfo.CallingConvention.ToString()));
byte[] hashAndReset = incrementalHash.GetHashAndReset();
StringBuilder stringBuilder = new StringBuilder(hashAndReset.Length * 2);
for (int j = 0; j < hashAndReset.Length; j++)
{
stringBuilder.Append(hashAndReset[j].ToString("x2"));
}
string hash = stringBuilder.ToString();
byte[] method = (byte[])assemblyFlare.GetType("FlareOn.Backdoor.FLARE12").GetMethod("flare_46").Invoke(null, new object[] {
new byte[] { 18, 120, 171, 223 },
assemblyFlare.GetType("FlareOn.Backdoor.FLARE15").GetMethod("flare_69").Invoke(null, new object[] { hash })
});
Console.WriteLine("{0} {1} {2}", methodInfo.Name, Utils.hexlify(methodBody.GetILAsByteArray()), Utils.hexlify(method));
}
}
}
}

public class Utils
{
public static string hexlify(byte[] bytes)
{
return string.Join("", bytes.Select(b => b.ToString("X2")));
}
public static string hexlify(Dictionary<uint, int> dict)
{
return string.Join("", dict.Select(d => d.Key.ToString("X") + "=" + d.Value.ToString("X8")));
}
}
}

把它们保存到dump2.txt中,然后写如下脚本恢复函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import re

d = {0:'A', 1:'A', 2:'A', 3:'A', 4:'A', 5:'A', 6:'A', 7:'A', 8:'A', 9:'A', 10:'A', 11:'A', 12:'A', 13:'A', 14:'E', 15:'E', 16:'E', 17:'E', 18:'E', 19:'E', 20:'A', 21:'A', 22:'A', 23:'A', 24:'A', 25:'A', 26:'A', 27:'A', 28:'A', 29:'A', 30:'A', 31:'E', 32:'G', 33:'H', 34:'G', 35:'H', 37:'A', 38:'A', 39:'B', 40:'B', 41:'B', 42:'A', 43:'C', 44:'C', 45:'C', 46:'C', 47:'C', 48:'C', 49:'C', 50:'C', 51:'C', 52:'C', 53:'C', 54:'C', 55:'C', 56:'D', 57:'D', 58:'D', 59:'D', 60:'D', 61:'D', 62:'D', 63:'D', 64:'D', 65:'D', 66:'D', 67:'D', 68:'D', 69:'I', 70:'A', 71:'A', 72:'A', 73:'A', 74:'A', 75:'A', 76:'A', 77:'A', 78:'A', 79:'A', 80:'A', 81:'A', 82:'A', 83:'A', 84:'A', 85:'A', 86:'A', 87:'A', 88:'A', 89:'A', 90:'A', 91:'A', 92:'A', 93:'A', 94:'A', 95:'A', 96:'A', 97:'A', 98:'A', 99:'A', 100:'A', 101:'A', 102:'A', 103:'A', 104:'A', 105:'A', 106:'A', 107:'A', 108:'A', 109:'A', 110:'A', 111:'B', 112:'B', 113:'B', 114:'B', 115:'B', 116:'B', 117:'B', 118:'A', 121:'B', 122:'A', 123:'B', 124:'B', 125:'B', 126:'B', 127:'B', 128:'B', 129:'B', 130:'A', 131:'A', 132:'A', 133:'A', 134:'A', 135:'A', 136:'A', 137:'A', 138:'A', 139:'A', 140:'B', 141:'B', 142:'A', 143:'B', 144:'A', 145:'A', 146:'A', 147:'A', 148:'A', 149:'A', 150:'A', 151:'A', 152:'A', 153:'A', 154:'A', 155:'A', 156:'A', 157:'A', 158:'A', 159:'A', 160:'A', 161:'A', 162:'A', 163:'B', 164:'B', 165:'B', 179:'A', 180:'A', 181:'A', 182:'A', 183:'A', 184:'A', 185:'A', 186:'A', 194:'B', 195:'A', 198:'B', 208:'B', 209:'A', 210:'A', 211:'A', 212:'A', 213:'A', 214:'A', 215:'A', 216:'A', 217:'A', 218:'A', 219:'A', 220:'A', 221:'D', 222:'C', 223:'A', 224:'A', 248:'A', 249:'A', 250:'A', 251:'A', 252:'A', 253:'A', 254:'A', 255:'A', 65024:'A', 65025:'A', 65026:'A', 65027:'A', 65028:'A', 65029:'A', 65030:'B', 65031:'B', 65033:'F', 65034:'F', 65035:'F', 65036:'F', 65037:'F', 65038:'F', 65039:'A', 65041:'A', 65042:'E', 65043:'A', 65044:'A', 65045:'B', 65046:'B', 65047:'A', 65048:'A', 65049:'E', 65050:'A', 65052:'B', 65053:'A', 65054:'A'}

pattern = re.compile(r'([A-Za-z_][A-Za-z_\d]*) ([A-F\d]*) ([A-F\d]*)\n')
binary = open('flare1.exe', 'rb').read()

def replace(binary: bytes, org: bytes, rep: bytes):
assert len(org) == len(rep)
i = binary.find(org)
assert i != -1 and binary.find(org, i + 1) == -1
return binary[:i] + rep + binary[i+len(org):]

def fixCode(code: bytes):
i = 0
while i < len(code):
if code[i] == 254:
key = 65024 + code[i + 1]
i += 1
else: key = code[i]
i += 1
ot = d[key]
if ot == 'A': pass
elif ot == 'B':
code = code[:i] + (int.from_bytes(code[i:i+4], 'little') ^ 2727913149).to_bytes(4, 'little') + code[i+4:]
i += 4
elif ot in 'CE': i += 1
elif ot in 'DG': i += 4
elif ot == 'F': i += 2
elif ot == 'H': i += 8
elif ot == 'I': i += 4 + 4 * int.from_bytes(code[i:i+4], 'little')
return code

for line in open('dump2.txt', 'r').readlines():
match = pattern.fullmatch(line)
assert match
name, org, code = match.group(1), bytes.fromhex(match.group(2)), bytes.fromhex(match.group(3))
if len(org) != len(code):
print('broken method body: %s' % name)
continue
code = fixCode(code)
binary = replace(binary, org, code)
open('flare2.exe', 'wb').write(binary)

恢复后所有代码都可以反编译了,在dnSpy中修改代码也能正常保存了,下面就直接看程序流程:

程序流程

可以看到,程序获取一个互斥体(程序不能多开的原因),然后进行一系列初始化操作,接着进入一个大的switch-case。这里不讲过程了,直接说结论:这个代码块经过了控制流平坦化,不过与一般见到的不同,这个的流程控制有两个变量,你可以把它理解为状态转移(梦回数电课堂)FLARE13.cs中保存了当前状态,然后根据它的值选择相应分支,由分支函数的返回值和当前的cs共同决定下一个cs是什么,具体映射表在flared_48函数中,这里给出它的状态转移图:

状态转移

但这并不关键,如果多点进几个函数,最终也一定可以来到flared_56。因为这个函数也是一个巨大的switch-case,而且里面还有一大群base64字符串,对它们解码发现都是些powershell命令之类的。另一方面,之前的代码解密操作并没有读取那个10.8MB的段,我怀疑那里面可能有一些线索。想到代码可能会复用那个读取PE段的函数,我对此函数查看xref,发现一处可疑的地方:

解密可疑数据

flared_54会把反转后的FLARE14.sh作为参数调用flare_69,接着RC4解密此数据,密码是向FLARE14.h中添加的数据的SHA256值。而那个巨大的switch-case中就有一系列添加数据的操作。那我们就回到flared_56,尝试分析case的顺序。

分析得知,决定跳转的最原始的变量应该是从某网页下请求的数据array2(由后面的Deflate解压猜测),直接变量是text的hash值(hash函数是flare_51)。然而我们无法复现这些数据了,于是只能找找别的方法。我又注意到每个case下都有调用flare_56,而这里面检查了i的值(即text整数化后的值),检查通过就把参数二中的字符串附加到另一个成员变量里,这个变量正是寻找段名用到的字符串FLARE14.sh

顺序线索

通过FLARE15.c这个集合我们可以推断一个合适的顺序。查看这个集合,xor 248后的元素正好和text的取值一一对应:

每个case中的text

那么我们最终得到了它的顺序。每次操作会向FLARE14.h中添加FLARE14.flare_57() + text。查看flare_57,它调用flared_57

flared_57

这是程序的一个检验,它返回这个函数的调用者和调用者的调用者。既然我们能看到这个函数,那么说明这个栈已经不是之前动态调用方法时的栈了,所以我们只能拟运行解密前的程序,这里写了个C#程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
Assembly assembly = Assembly.LoadFrom(Path.GetFullPath("flare1.exe"));
assembly.GetType("FlareOn.Backdoor.FLARE15").GetMethod("flare_74").Invoke(null, null);
Console.WriteLine(assembly.GetType("FlareOn.Backdoor.FLARE14").GetMethod("flare_57").Invoke(null, null));
Console.ReadLine();
}

public static string hexlify(byte[] bytes)
{
return string.Join("", bytes.Select(b => b.ToString("X2")));
}
}
}

但注意我们不能运行原始的FlareOn.Backdoor.exe,而应该运行已解密第一层的flare1.exe,否则会报TargetInvocationException,具体原因我还没搞清楚。然后,我们得到了下面的字符串:

1
System.Object InvokeMethod(System.Object, System.Object[], System.Signature, Boolean)System.Object Invoke(System.Object, System.Reflection.BindingFlags, System.Reflection.Binder, System.Object[], System.Globalization.CultureInfo)

这下行了,我们把那个段单独抽出来放到data.bin里,然后写以下脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from Crypto.Hash import SHA256
from Crypto.Cipher import ARC4

flow = (2,10,8,19,11,1,15,13,22,16,5,12,21,3,18,17,20,14,9,7,4)

data = {
1: "powershell -exec bypass -enc \"RwBlAHQALQBOAGUAdABJAFAAQQBkAGQAcgBlAHMAcwAgAC0AQQBkAGQAcgBlAHMAcwBGAGEAbQBpAGwAeQAgAEkAUAB2ADQAIAB8ACAAUwBlAGwAZQBjAHQALQBPAGIAagBlAGMAdAAgAEkAUABBAGQAZAByAGUAcwBzAA==\"",
2: "powershell -exec bypass -enc \"RwBlAHQALQBOAGUAdABOAGUAaQBnAGgAYgBvAHIAIAAtAEEAZABkAHIAZQBzAHMARgBhAG0AaQBsAHkAIABJAFAAdgA0ACAAfAAgAFMAZQBsAGUAYwB0AC0ATwBiAGoAZQBjAHQAIAAiAEkAUABBAEQARAByAGUAcwBzACIA\"",
3: "whoami",
4: "powershell -exec bypass -enc \"WwBTAHkAcwB0AGUAbQAuAEUAbgB2AGkAcgBvAG4AbQBlAG4AdABdADoAOgBPAFMAVgBlAHIAcwBpAG8AbgAuAFYAZQByAHMAaQBvAG4AUwB0AHIAaQBuAGcA\"",
5: "net user",
7: "powershell -exec bypass -enc \"RwBlAHQALQBDAGgAaQBsAGQASQB0AGUAbQAgAC0AUABhAHQAaAAgACIAQwA6AFwAUAByAG8AZwByAGEAbQAgAEYAaQBsAGUAcwAiACAAfAAgAFMAZQBsAGUAYwB0AC0ATwBiAGoAZQBjAHQAIABOAGEAbQBlAA==\"",
8: "powershell -exec bypass -enc \"RwBlAHQALQBDAGgAaQBsAGQASQB0AGUAbQAgAC0AUABhAHQAaAAgACcAQwA6AFwAUAByAG8AZwByAGEAbQAgAEYAaQBsAGUAcwAgACgAeAA4ADYAKQAnACAAfAAgAFMAZQBsAGUAYwB0AC0ATwBiAGoAZQBjAHQAIABOAGEAbQBlAA==\"",
9: "powershell -exec bypass -enc \"RwBlAHQALQBDAGgAaQBsAGQASQB0AGUAbQAgAC0AUABhAHQAaAAgACcAQwA6ACcAIAB8ACAAUwBlAGwAZQBjAHQALQBPAGIAagBlAGMAdAAgAE4AYQBtAGUA\"",
10: "hostname",
11: "powershell -exec bypass -enc \"RwBlAHQALQBOAGUAdABUAEMAUABDAG8AbgBuAGUAYwB0AGkAbwBuACAAfAAgAFcAaABlAHIAZQAtAE8AYgBqAGUAYwB0ACAAewAkAF8ALgBTAHQAYQB0AGUAIAAtAGUAcQAgACIARQBzAHQAYQBiAGwAaQBzAGgAZQBkACIAfQAgAHwAIABTAGUAbABlAGMAdAAtAE8AYgBqAGUAYwB0ACAAIgBMAG8AYwBhAGwAQQBkAGQAcgBlAHMAcwAiACwAIAAiAEwAbwBjAGEAbABQAG8AcgB0ACIALAAgACIAUgBlAG0AbwB0AGUAQQBkAGQAcgBlAHMAcwAiACwAIAAiAFIAZQBtAG8AdABlAFAAbwByAHQAIgA=\"",
12: "powershell -exec bypass -enc \"JAAoAHAAaQBuAGcAIAAtAG4AIAAxACAAMQAwAC4ANgA1AC4ANAAuADUAMAAgAHwAIABmAGkAbgBkAHMAdAByACAALwBpACAAdAB0AGwAKQAgAC0AZQBxACAAJABuAHUAbABsADsAJAAoAHAAaQBuAGcAIAAtAG4AIAAxACAAMQAwAC4ANgA1AC4ANAAuADUAMQAgAHwAIABmAGkAbgBkAHMAdAByACAALwBpACAAdAB0AGwAKQAgAC0AZQBxACAAJABuAHUAbABsADsAJAAoAHAAaQBuAGcAIAAtAG4AIAAxACAAMQAwAC4ANgA1AC4ANgA1AC4ANgA1ACAAfAAgAGYAaQBuAGQAcwB0AHIAIAAvAGkAIAB0AHQAbAApACAALQBlAHEAIAAkAG4AdQBsAGwAOwAkACgAcABpAG4AZwAgAC0AbgAgADEAIAAxADAALgA2ADUALgA1ADMALgA1ADMAIAB8ACAAZgBpAG4AZABzAHQAcgAgAC8AaQAgAHQAdABsACkAIAAtAGUAcQAgACQAbgB1AGwAbAA7ACQAKABwAGkAbgBnACAALQBuACAAMQAgADEAMAAuADYANQAuADIAMQAuADIAMAAwACAAfAAgAGYAaQBuAGQAcwB0AHIAIAAvAGkAIAB0AHQAbAApACAALQBlAHEAIAAkAG4AdQBsAGwA\"",
13: "powershell -exec bypass -enc \"bnNsb29rdXAgZmxhcmUtb24uY29tIHwgZmluZHN0ciAvaSBBZGRyZXNzO25zbG9va3VwIHdlYm1haWwuZmxhcmUtb24uY29tIHwgZmluZHN0ciAvaSBBZGRyZXNz\"",
14: "powershell -exec bypass -enc \"JAAoAHAAaQBuAGcAIAAtAG4AIAAxACAAMQAwAC4AMQAwAC4AMgAxAC4AMgAwADEAIAB8ACAAZgBpAG4AZABzAHQAcgAgAC8AaQAgAHQAdABsACkAIAAtAGUAcQAgACQAbgB1AGwAbAA7ACQAKABwAGkAbgBnACAALQBuACAAMQAgADEAMAAuADEAMAAuADEAOQAuADIAMAAxACAAfAAgAGYAaQBuAGQAcwB0AHIAIAAvAGkAIAB0AHQAbAApACAALQBlAHEAIAAkAG4AdQBsAGwAOwAkACgAcABpAG4AZwAgAC0AbgAgADEAIAAxADAALgAxADAALgAxADkALgAyADAAMgAgAHwAIABmAGkAbgBkAHMAdAByACAALwBpACAAdAB0AGwAKQAgAC0AZQBxACAAJABuAHUAbABsADsAJAAoAHAAaQBuAGcAIAAtAG4AIAAxACAAMQAwAC4AMQAwAC4AMgA0AC4AMgAwADAAIAB8ACAAZgBpAG4AZABzAHQAcgAgAC8AaQAgAHQAdABsACkAIAAtAGUAcQAgACQAbgB1AGwAbAA=\"",
15: "powershell -exec bypass -enc \"JAAoAHAAaQBuAGcAIAAtAG4AIAAxACAAMQAwAC4AMQAwAC4AMQAwAC4ANAAgAHwAIABmAGkAbgBkAHMAdAByACAALwBpACAAdAB0AGwAKQAgAC0AZQBxACAAJABuAHUAbABsADsAJAAoAHAAaQBuAGcAIAAtAG4AIAAxACAAMQAwAC4AMQAwAC4ANQAwAC4AMQAwACAAfAAgAGYAaQBuAGQAcwB0AHIAIAAvAGkAIAB0AHQAbAApACAALQBlAHEAIAAkAG4AdQBsAGwAOwAkACgAcABpAG4AZwAgAC0AbgAgADEAIAAxADAALgAxADAALgAyADIALgA1ADAAIAB8ACAAZgBpAG4AZABzAHQAcgAgAC8AaQAgAHQAdABsACkAIAAtAGUAcQAgACQAbgB1AGwAbAA7ACQAKABwAGkAbgBnACAALQBuACAAMQAgADEAMAAuADEAMAAuADQANQAuADEAOQAgAHwAIABmAGkAbgBkAHMAdAByACAALwBpACAAdAB0AGwAKQAgAC0AZQBxACAAJABuAHUAbABsAA==\"",
16: "powershell -exec bypass -enc \"JAAoAHAAaQBuAGcAIAAtAG4AIAAxACAAMQAwAC4ANgA1AC4ANQAxAC4AMQAxACAAfAAgAGYAaQBuAGQAcwB0AHIAIAAvAGkAIAB0AHQAbAApACAALQBlAHEAIAAkAG4AdQBsAGwAOwAkACgAcABpAG4AZwAgAC0AbgAgADEAIAAxADAALgA2ADUALgA2AC4AMQAgAHwAIABmAGkAbgBkAHMAdAByACAALwBpACAAdAB0AGwAKQAgAC0AZQBxACAAJABuAHUAbABsADsAJAAoAHAAaQBuAGcAIAAtAG4AIAAxACAAMQAwAC4ANgA1AC4ANQAyAC4AMgAwADAAIAB8ACAAZgBpAG4AZABzAHQAcgAgAC8AaQAgAHQAdABsACkAIAAtAGUAcQAgACQAbgB1AGwAbAA7ACQAKABwAGkAbgBnACAALQBuACAAMQAgADEAMAAuADYANQAuADYALgAzACAAfAAgAGYAaQBuAGQAcwB0AHIAIAAvAGkAIAB0AHQAbAApACAALQBlAHEAIAAkAG4AdQBsAGwA\"",
17: "powershell -exec bypass -enc \"JAAoAHAAaQBuAGcAIAAtAG4AIAAxACAAMQAwAC4ANgA1AC4ANAA1AC4AMQA4ACAAfAAgAGYAaQBuAGQAcwB0AHIAIAAvAGkAIAB0AHQAbAApACAALQBlAHEAIAAkAG4AdQBsAGwAOwAkACgAcABpAG4AZwAgAC0AbgAgADEAIAAxADAALgA2ADUALgAyADgALgA0ADEAIAB8ACAAZgBpAG4AZABzAHQAcgAgAC8AaQAgAHQAdABsACkAIAAtAGUAcQAgACQAbgB1AGwAbAA7ACQAKABwAGkAbgBnACAALQBuACAAMQAgADEAMAAuADYANQAuADMANgAuADEAMwAgAHwAIABmAGkAbgBkAHMAdAByACAALwBpACAAdAB0AGwAKQAgAC0AZQBxACAAJABuAHUAbABsADsAJAAoAHAAaQBuAGcAIAAtAG4AIAAxACAAMQAwAC4ANgA1AC4ANQAxAC4AMQAwACAAfAAgAGYAaQBuAGQAcwB0AHIAIAAvAGkAIAB0AHQAbAApACAALQBlAHEAIAAkAG4AdQBsAGwA\"",
18: "powershell -exec bypass -enc \"JAAoAHAAaQBuAGcAIAAtAG4AIAAxACAAMQAwAC4AMQAwAC4AMgAyAC4ANAAyACAAfAAgAGYAaQBuAGQAcwB0AHIAIAAvAGkAIAB0AHQAbAApACAALQBlAHEAIAAkAG4AdQBsAGwAOwAkACgAcABpAG4AZwAgAC0AbgAgADEAIAAxADAALgAxADAALgAyADMALgAyADAAMAAgAHwAIABmAGkAbgBkAHMAdAByACAALwBpACAAdAB0AGwAKQAgAC0AZQBxACAAJABuAHUAbABsADsAJAAoAHAAaQBuAGcAIAAtAG4AIAAxACAAMQAwAC4AMQAwAC4ANAA1AC4AMQA5ACAAfAAgAGYAaQBuAGQAcwB0AHIAIAAvAGkAIAB0AHQAbAApACAALQBlAHEAIAAkAG4AdQBsAGwAOwAkACgAcABpAG4AZwAgAC0AbgAgADEAIAAxADAALgAxADAALgAxADkALgA1ADAAIAB8ACAAZgBpAG4AZABzAHQAcgAgAC8AaQAgAHQAdABsACkAIAAtAGUAcQAgACQAbgB1AGwAbAA=\"",
19: "powershell -exec bypass -enc \"JChwaW5nIC1uIDEgMTAuNjUuNDUuMyB8IGZpbmRzdHIgL2kgdHRsKSAtZXEgJG51bGw7JChwaW5nIC1uIDEgMTAuNjUuNC41MiB8IGZpbmRzdHIgL2kgdHRsKSAtZXEgJG51bGw7JChwaW5nIC1uIDEgMTAuNjUuMzEuMTU1IHwgZmluZHN0ciAvaSB0dGwpIC1lcSAkbnVsbDskKHBpbmcgLW4gMSBmbGFyZS1vbi5jb20gfCBmaW5kc3RyIC9pIHR0bCkgLWVxICRudWxs\"",
20: "powershell -exec bypass -enc \"RwBlAHQALQBOAGUAdABJAFAAQwBvAG4AZgBpAGcAdQByAGEAdABpAG8AbgAgAHwAIABGAG8AcgBlAGEAYwBoACAASQBQAHYANABEAGUAZgBhAHUAbAB0AEcAYQB0AGUAdwBhAHkAIAB8ACAAUwBlAGwAZQBjAHQALQBPAGIAagBlAGMAdAAgAE4AZQB4AHQASABvAHAA\"",
21: "powershell -exec bypass -enc \"RwBlAHQALQBEAG4AcwBDAGwAaQBlAG4AdABTAGUAcgB2AGUAcgBBAGQAZAByAGUAcwBzACAALQBBAGQAZAByAGUAcwBzAEYAYQBtAGkAbAB5ACAASQBQAHYANAAgAHwAIABTAGUAbABlAGMAdAAtAE8AYgBqAGUAYwB0ACAAUwBFAFIAVgBFAFIAQQBkAGQAcgBlAHMAcwBlAHMA\"",
22: "systeminfo | findstr /i \"Domain\"",
}

prefix = 'System.Object InvokeMethod(System.Object, System.Object[], System.Signature, Boolean)System.Object Invoke(System.Object, System.Reflection.BindingFlags, System.Reflection.Binder, System.Object[], System.Globalization.CultureInfo)'
hash = SHA256.new()
for i in flow:
if i != 4:
hash.update((prefix + data[i]).encode('ascii'))
open('out.txt', 'wb').write(ARC4.new(
hash.digest()
).decrypt(
open('data.bin', 'rb').read()
))

值得注意的是,i == 4时的那个case,得到的数据不会添加到SHA256中,这个我卡了很久才发现,老坑了。。。

然后用记事本打开它,发现是个GIF,改后缀为.gif打开,即可看到flag:

最后的flag

W3_4re_Kn0wn_f0r_b31ng_Dyn4m1c@flare-on.com

09 - encryptor

You’re really crushing it to get this far. This is probably the end for you. Better luck next year!

附件下载

程序的食用方法:flareon file ...,然后程序将加密指定的文件。这也改编自某个勒索病毒,但赛题为了避免误加密重要的数据,只加密.EncryptMe后缀的文件。分析程序流程:

程序流程

程序动态获取微软未公开的函数RtlGenRandom,它用于产生安全的随机数据,然后调用sub_4021D0初始化,产生一对RSA密钥,并用一个已知的公钥加密它(但程序加密文件并没有用到它,它只是最后被程序输出到了加密文件中)。接着程序调用encrypt逐个加密文件,最后调用SHGetFolderPath获取桌面路径(CSIDL_DESKTOP),在那里放一个文件提示你文件被加密,分析的IDB文件如下:

IDB文件下载

通过分析和动态调试,我发现这应该是chacha20加密,nonce和key都是随机的,它们用RSA加密后也附加到加密文件中。然而程序竟然使用私钥加密,变成RSA签名了?那太好了,我们直接用公钥还原就好了:

1
2
3
4
5
6
7
from Crypto.Cipher import ChaCha20

n = int('dc425c720400e05a92eeb68d0313c84a978cbcf47474cbd9635eb353af864ea46221546a0f4d09aaa0885113e31db53b565c169c3606a241b569912a9bf95c91afbc04528431fdcee6044781fbc8629b06f99a11b99c05836e47638bbd07a232c658129aeb094ddaf4c3ad34563ee926a87123bc669f71eb6097e77c188b9bc9', 16)
enc = int('5a04e95cd0e9bf0c8cdda2cbb0f50e7db8c89af791b4e88fd657237c1be4e6599bc4c80fd81bdb007e43743020a245d5f87df1c23c4d129b659f90ece2a5c22df1b60273741bf3694dd809d2c485030afdc6268431b2287c597239a8e922eb31174efcae47ea47104bc901cea0abb2cc9ef974d974f135ab1f4899946428184c', 16)
raw = pow(enc, 65537, n).to_bytes(128, 'little')
data = ChaCha20.new(key=raw[:32], nonce=raw[36:48]).decrypt(open('SuspiciousFile.txt.Encrypted', 'rb').read(73))
print(data.decode('ascii'))

这里有一个小坑:exe中用于运算的大整数是136*8位的大整数,但mulmod在接受大于128*8的整数时会出问题,导致sub_4021D0最后一行的powmod运算结果并不正确。我当时动态调试被这个折磨了很久,一直怀疑自己哪个地方推断错了。不过好在这不影响最终结果,就只提一提吧。

R$A_$16n1n6_15_0pp0$17e_0f_3ncryp710n@flare-on.com

10 - Nur geträumt

See README.txt in the archive.

附件下载

根据README.txt中所说,我下载了Mini vMac,然而官方由于版权问题无法提供Macintosh的ROM,那我们只能自己去网上找了。我最后找到了这个。但我试了很多ROM,几乎没有一个是能正常运行的。于是我中途换了很多其他同类型软件和ROM,没有进展。最后我找到了这里,评论区UP主发的链接中出现了能用的ROM。因为是百度网盘,这里贴一下我用到的Mini vMac,ROM,和传文件可能会用到的HFVExplorer:

文件下载

然后按照这篇文章配好环境,然后我们就可以把题目里给的文件直接拖进来了。从中可以提取出两个可执行文件:

提取的文件

左边那个即是让我们输入flag的程序了,而右边是它的反汇编器。从反汇编器中获得了另一个提示(用Mac Roman解码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Oh. Hello there.

I’m from the DISTANT FUTURE, where normal computers run at 2-4 GHz and 16 GB is considered a “medium” amount of RAM. That’s right; we have more RAM than you have hard drive space.

The good news:

• Unicode really seems to have worked, for the most part. We even have characters for clown faces and smiling piles of poo.
• The Mac is still a pretty big deal, and can still read this program (but hasn’t been able to run it for a while).
• Nearly all computers in the world are connected together through a network called the Internet. Depending on when you read this, you may have heard of it. Don’t get rid of MacTCP just yet.

The bad news:

• Nearly all computers in the world are connected together through a network called the Internet. This has made a lot of people very angry and been widely regarded as a bad move.
• Despite having 16 GB of RAM, Microsoft Word still takes up roughly half of it.
• We’re still using Microsoft Word.

Anyway, because in the future we’re stuck at home due to a worldwide pandemic (no, not the Internet, there is ANOTHER one), we had a competition for finding fun things in computer programs. I’ve hidden a flag in this program, but it’s not going to be all that easy to find just with ResEdit.

You’ll probably need to interact with the program a bit. It will let you know when you've found the right flag. I've done you the favor of including Super ResEdit here, which has a disassembler, and I'm even nice enough to give you the debug version of the program with all the symbol names, to give you a head start (because I'm not wicked enough to make you step through it with MacsBug, but you could if you wanted).

Here’s your first hint: 1983 was a pretty good year in music.

Have fun, and enjoy the challenge! If you're still having trouble, maybe try asking the program if it has a bit of time for you; perhaps it will sing you a song.

- Dave Riley, July 2022

嗯,很有沧桑感,但先放一边。打开程序提示输入flag,我们输入123时和输入123123时得到的flag完全相同,如图所示:

程序界面

所以猜测是RC4加密。但进一步观察,输入1234得到的flag前3字节并没有变化,那么可能就是传统的异或加密或者加减加密了。尝试后发现这就是异或加密,而且密钥长度不够时会循环拼接。那我们把异或前的原数据抠出来先:

1
bytes.fromhex('0C001D1A7F171C4E0211280810480500001A7F2AF61744320FFC1A602C08101C6002194117115A0E1D0E390A04')

然后根据之前的解题,推测flag的后缀是@flare-on.com。用这个线索去异或字符串,得到du etwas Zei。去网上搜索这条字符串,最后得到这么两句:

1
2
Hast du etwas zeit für mich?
Dann singe ich ein Lied für dich.

这个与题目给的提示对上了。从异或原数据中可以看出,key和flag中一共有两个变音字母(编码值大于0x7F)。通过尝试,可以试出flag:

1
2
key = 'Hast du etwas Zeit für mich?'
flag = 'Dann_singe_ich_ein_Lied_für_dich@flare-on.com'

然而交上去结果不对,于是我又试了些其他的组合,还是不行。然后我发现hint中有这么一句话:You’ll probably need to interact with the program a bit. It will let you know when you've found the right flag.。那么我就想直接输入key看看对不对。但是我不知道如何输入变音符号,然后找了个笨办法:把包含变音符号的文件弄到虚拟机的文件系统里面去。我按照这篇文章中提到的HFVExplorer复制文件(这软件还挺难操作的),最后成功输入key:

测试成功

程序提示我们去掉变音符号,那我们就去掉吧。早知道直接搜索程序的字符串了,使用HFVExplorer的过程是真的折磨。

Dann_singe_ich_ein_Lied_fur_dich@flare-on.com

11 - The challenge that shall not be named.

Protection, Obfuscation, Restrictions… Oh my!!

The good part about this one is that if you fail to solve it I don’t need to ship you a prize.

附件下载

通过程序中的字符串得知,这是一个python程序,但用了pyinstaller打包。解包过程就不细说了,网上教程很多,这里使用了pyinstxtractor,解包后的11修复后改成11.pyc。最后这些文件是我们要重点关注的:

相关文件

用uncompyle6反编译此pyc,得到下面的:

1
2
from pytransform import pyarmor
pyarmor(__name__, __file__, b'一些数据...', 2)

百度搜索pyarmor,查到的是python代码加密库。在这里有一篇关于脱壳文章,里面介绍了3种方法。但我这边的情况是,解包就无法运行了,提示内容如下:

报错

我没办法用上面文章的方法,因为无法一直保持运行状态。IDA打开pyd文件,搜索pyarmor,找到一处。在它的下面有一个函数sub_6D605C30,那应该是它了,F5查看,果然如此:

代码

这个sub_6D605C30带着检验了文件头的代码数据指针调用了sub_6D6052E0,参数1就是被加密的代码,推测这个就是解密过程。这里IDA识别函数参数出了点问题,修复一下就好。往下翻可以看到PyEval_EvalCode函数,那么解密代码应该就在这附近了。最后我找到了关键的地方:

解密过程

因为点开sub_6D60E410函数后,有这么一条字符串:"src/encauth/gcm/gcm_init.c",GitHub上搜索得知,此代码来自libtomcrypt。稍微修一下代码:

修复的代码

能推断出代码用了AES-GCM模式解密。而我们目前得不到mix_key的值,因为它在bss段,而查看xref发现没有引用。那我们就选择动态调试,把call gcm_init改成jmp $,保存,开始执行后程序会卡住,这时我们附加到进程,最后得到它的值为bytes.fromhex('6CFCBAB1AFB2AA67027561AB8C9AB20B')。写个脚本逆回来即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from Crypto.Cipher import AES

code = b'一些数据...'

p32 = lambda b, p: int.from_bytes(b[p:p+4], 'little')

mix = bytes.fromhex('6CFCBAB1AFB2AA67027561AB8C9AB20B')
pos, len = p32(code, 28), p32(code, 32)
body = code[pos:pos+len]
key = memoryview(bytearray(code[40:56])).cast('L')
key[1] -= 15138; key[2] += 32815; key[3] += 9498
key = bytes(map(lambda x, y: x ^ y, mix, key.cast('B')))
cipher = AES.new(key, AES.MODE_GCM, code[40:52])
open('code.txt', 'wb').write(cipher.decrypt(body))

逆出来的结果理论上是可以被marshal模块执行的,但实际却不行。好在用记事本打开后的flag字符串能直接看到,那就直接提交吧!

在折腾那个网上的脱壳文章时,我发现我改过的代码无法执行,保错信息:

1
2
3
Traceback (most recent call last):
File "<11.py>", line 3, in <module>
RuntimeError: Check restrict mode of module failed

在网上找到的结果是,pyarmor有限制模式,这个模式可以避免代码被修改。但这个字符串特征可以在pyd文件中直接找到,那我们作对应修改就行了。我当时的patch是:

1
2
3
4
5
6
7
8
Address           Length  Original        Patch
00000000000052F1 0x5 E8 4A 4A 07 00 90 90 90 90 90
000000000000591B 0x3 0F 84 F3 E9 F4 00
0000000000005920 0x1 00 90
0000000000005A1C 0x3 0F 85 D8 E9 D9 00
0000000000005A21 0x1 00 90
0000000000005B01 0x4 0F 84 79 F8 E9 7A F8 FF
0000000000005B06 0x1 FF 90

这个最后没有用到,那么就不详细说了。

Pyth0n_Prot3ction_tuRn3d_Up_t0_11@flare-on.com

尾声

这次比赛的题还是很好的。其中第5、8、9题改编自病毒,都是我平常做题时较少见到的,如果它们是真的病毒,那还不敢直接动态调试,在虚拟机里配环境也够喝一壶了;第7、8题都使用了混淆技术,但好在它们并不算太复杂,反混淆的过程还算流畅。最后就是,我感觉题目的顺序不太合理,第8题应该放到最后的,当时要是我没解出第8题,就再也没动力继续了。