Flare-On 2019 Writeup

写在前面

好久没更新,之前立的 toWrite plan几乎全倒了

不过比较好的是,去年flareon2018做了6/12,立了个flag今年要拿牌

现在做到了!!!

所以必须得来更新一下writeup

其实跟去年的题目比较,感觉今年前面的题目相对比较简单,因此前面做的比较快

最后卡在了最后一题,断断续续做了快20天,心态都快崩了

不过还好,最后还是拿到了牌(虽然现在还没寄到手上

不得不说,flareon总是能学到许多

下面开始描述我的解法,在记录完我的解法后,我才会提及到一些题目相关(题目设计、官方解法)

1 - Memecat Battlestation

.net直接逆,很简单,xor 一下就出来了

2 - Overlong

一段data,程序只循环了0x1c次,把循环patch成0xb0次,则那块数据的长度,得到flag

3 - Flarebear

这是一道android题,用kotlin写的apk,直接逆

对小熊有3种操作play, feed, clean

能看到满足一定条件后会调用dancewithflag

总结一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
play
mass -= 2
happy += 4
clean -= 1
feed
mass += 10
happy += 2
clean -= 1
poo += 0.34
clean
poo -= 1 if poo > 1
mass += 0
happy -= 1
clean += 6
setMood

setMood
isHappy
feed/play = [2, 2.5]
isEcstatic
mass = 72
happy = 30
clean = 0
call danceWithFlag

得到方程

-2p + 10f = 72

4p + 2f - c = 30

-p - f + 6c = 0

解出,得到flag

p = 4

f = 8

c = 2

4 - Dnschess

一个下国际象棋的ai,通过dns协议与server交互

包含有一个pcap数据包

chessAI会把每一步编码成类似pawn-d2-d4.game-of-thrones.flare-on.com的域名,然后通过dns查询得到ip

注意到得到的ip会把数据取出来操作

并得到值放进`@flare-on.com`前面的buffer上,我通过scapy从pcap中提出来ip,模拟一下操作就得到flag了

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
#-*-coding:utf-8-*-

result = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x66, 0x6C, 0x61, 0x72, 0x65, 0x2D, 0x6F, 0x6E, 0x2E, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]

table = [0x79, 0x5A, 0xB8, 0xBC, 0xEC, 0xD3, 0xDF, 0xDD, 0x99, 0xA5, 0xB6, 0xAC, 0x15, 0x36, 0x85, 0x8D, 0x09, 0x08, 0x77, 0x52, 0x4D, 0x71, 0x54, 0x7D, 0xA7, 0xA7, 0x08, 0x16, 0xFD, 0xD7]

def genResult(ip):
if ip[0] != 127 or ip[3] & 1:
return
print ip
a1 = ip[2] & 0xf
result[2*a1] = ip[1] ^ table[2*a1]
result[2*a1+1] = ip[1] ^ table[2*a1+1]


from scapy.all import *

def getIp_list():
ip_list = []
pkts = rdpcap('capture.pcap')
for p in pkts:
if p.haslayer(DNS):
if isinstance(p.an, DNSRR):
ip = p.an.rdata
ip_list.append(list(map(int, ip.split('.'))))
return ip_list

ip_list = getIp_list()


for ip in ip_list:
genResult(ip)

print ''.join(map(chr, result))

# LooksLikeYouLockedUpTheLookupZ@flare-on.com

5 - demo

demoscense

一个非常cool的程序,通过代码压缩技术把一段动画压缩到超小的文件(这个甚至还有比赛

这个关系到windows 的 directX,先安装上cinst install directx

简单吐槽一下,这题主要时间都费在翻directX的文档上了

由于该代码加壳了(猜测是个压缩壳

所以得先脱壳才能分析,window嘛,直接调,

start定位到004000d3 retn指令

会直接跳转到0x420000继续进行程序的逻辑

跳转过去后,逻辑什么的都能看到了

5-dx_init

在查了半天的DirectX文档后,基本确定,下面两个函数是绘图相关的

1
2
(*(*off_430050 + 12))(off_430050, 0); 
(*(*off_430054 + 12))(off_430054, 0);

两个渲染,430050是flare-on的mash

430054是flag的mash

但是现在屏幕上只显示出来flare-on logo的图案,怎么让他把flag现实出来呢?

我一开始觉得,这个原因是flare-on的logo覆盖了flag(类似不透明图层),导致flag不能显示出来

于是我patch掉第一个渲染(*(*off_430050 + 12))(off_430050, 0);

发现图案全没了(开始智障之旅= =)

事实证明,我对directX的使用非常不熟悉

在尝试了很久把背景色改透明等等的操作后,我突然想到,为什么我不直接把第一个渲染改成flag的渲染就好了。。。

直接patch program把430050改成430054

得到flag

5-flag


PS: 从这题开始,算是终于进入flareon有意思的部分了,前四题有点水水的

这题官方还给出别的解法

  1. 通过api trace得到directX调用的顺序,然后可以直接dump出图案的model obj,就可以直接用一些软件直接查看了
  2. 代码中渲染了flag的图案,至于为什么不能显示出来,是因为设置了transform,移动了图形的位置,此时flag图案的位置摄像机背后,自然无法显示到界面上

6 - bmphide

这题上来就又是一个.net

bmphide.exe可以把一段数据(可以是文本或任意东西),隐藏到一张bmp图片底下

逆了一段时间,大概看到几个关键点

  1. 前面调用一个未知函数,里面非常复杂,不知道在干什么,直接看后面
  2. 读数据,进行一轮操作变换后,以LSB的方式编码到bmp图片底下

LSB的方式如下

1
2
3
4
5
6
Color pixel = bm.GetPixel(i, j);
int red = ((int)pixel.R & 248) | ((int)data[num] & 7);
int green = ((int)pixel.G & 248) | (data[num] >> 3 & 7);
int blue = ((int)pixel.B & 252) | (data[num] >> 6 & 3);
Color color = Color.FromArgb(0, red, green, blue);
bm.SetPixel(i, j, color);

那么,就是怎么逆回来的问题了

去调了一下代码

???

抛异常了(借用一下官方的图)

6-exception

调了几遍。似乎还是不知道什么原因,只好静态逆了= =

再仔细看了一下,当中的函数,有很大一部分是不用逆的,他们用于生成中间的index值,直接抄就好了

program a, b, c, d, e, f, g -> getByte

program h -> getByteArray

program i -> change input Bitmap

program j -> getInt

于是写了个逆,跑了一下

。。。

。。。

???

跑出来是乱码,继续改改,好像没什么问题,但是跑出来为什么是乱码

继续细看

这个时候我注意到一个点,有几个类似数据处理的函数定义了,但是没有被调用

同时这4个函数a, b, c, d,ac被调用了,bd没被调用,但是他们的函数类型(参数类型、返回类型)是一样的

因为在写逆的时候发现函数c其实是不可逆的,那么就可以猜测其实在init那个很复杂的函数里面把ac替换成了bd

1
2
d (b, r): right rotate b, r bits
b (b, r): left rotate b, r bits

回去细看init中根据函数hash搜索部分的代码,验证了我的想法,他就是替换了两个函数的内容!

那么把逆的ac修改成bd后,再跑

。。。

。。。

???

还是乱码???什么情况

init还有些很奇怪的代码,只能继续看看了

这个时候发现他会调用一些WriteIntPtrgetJit之类的函数,难道他还修改了一些常数?

这个时候还注意到一个点,有个G函数没被调用!他的函数类型跟F是一样的,而F是生成index的关键函数!

看来F也被hook成G了,再结合到getJit上下文,大概就能猜出来修改的是什么常数了

在这里猜的时候,我发现了只要把dnspy一些反反调试机制保护什么的关掉就能调试了,虽然单步好像不太好使,但能让我检验F函数的返回值,的确就是被hook成修改过的G函数了

1
2
3
4
5
6
def programG(idx):
# b = ((idx + 1) * 3988292384) & 0xff
# k = ((idx + 2) * 1669101435) & 0xff
b = ((idx + 1) * 309030853) & 0xff
k = ((idx + 2) * 209897853) & 0xff
return b ^ k

solve.py (代码其实不算长,就不放github了)

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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#-*-coding:utf-8-*-


def getCheckSumTable():
ret = []
for i in range(256):
num = i
for j in range(8):
num = (1611621881 ^ num >> 1) if (num & 1) != 0 else num >> 1
ret.append(num)
return ret

def programA(b, r):
return (b + r ^ r) & 255

def programB(b, r):
for i in range(r):
b2 = (b & 128) / 128
b = (b * 2 & 0xff) + b2
return b

def programC(b, r):
b2 = 1
for i in range(8):
if (b & 1) == 1:
b2 = (b2 * 2 + 1 & 0xff)
else:
b2 = (b2 - 1 & 0xff)
return b2

def programD(b, r):
for i in range(r):
b2 = (b & 1) * 128
b = (b / 2 & 0xff) + b2
return b

def programE(b, k):
return b ^ k


def programF(idx):
array = [121, 255, 214, 60, 106, 216, 149, 89, 96, 29, 81, 123, 182, 24, 167, 252, 88, 212, 43, 85, 181, 86, 108, 213, 50, 78, 247, 83, 193, 35, 135, 217, 0, 64, 45, 236, 134, 102, 76, 74, 153, 34, 39, 10, 192, 202, 71, 183, 185, 175, 84, 118, 9, 158, 66, 128, 116, 117, 4, 13, 46, 227, 132, 240, 122, 11, 18, 186, 30, 157, 1, 154, 144, 124, 152, 187, 32, 87, 141, 103, 189, 12, 53, 222, 206, 91, 20, 174, 49, 223, 155, 250, 95, 31, 98, 151, 179, 101, 47, 17, 207, 142, 199, 3, 205, 163, 146, 48, 165, 225, 62, 33, 119, 52, 241, 228, 162, 90, 140, 232, 129, 114, 75, 82, 190, 65, 2, 21, 14, 111, 115, 36, 107, 67, 126, 80, 110, 23, 44, 226, 56, 7, 172, 221, 239, 161, 61, 93, 94, 99, 171, 97, 38, 40, 28, 166, 209, 229, 136, 130, 164, 194, 243, 220, 25, 169, 105, 238, 245, 215, 195, 203, 170, 16, 109, 176, 27, 184, 148, 131, 210, 231, 125, 177, 26, 246, 127, 198, 254, 6, 69, 237, 197, 54, 59, 137, 79, 178, 139, 235, 249, 230, 233, 204, 196, 113, 120, 173, 224, 55, 92, 211, 112, 219, 208, 77, 191, 242, 133, 244, 168, 188, 138, 251, 70, 150, 145, 248, 180, 218, 42, 15, 159, 104, 22, 37, 72, 63, 234, 147, 200, 253, 100, 19, 73, 5, 57, 201, 51, 156, 41, 143, 68, 8, 160, 58]

num = 0
num2 = 0
result = 0
for i in range(idx+1):
num += 1
num %= 256
num2 += array[num]
num2 %= 256
array[num], array[num2] = array[num2], array[num]
result = array[(array[num] + array[num2]) % 256]
return result

def programG(idx):
# b = ((idx + 1) * 3988292384) & 0xff
# k = ((idx + 2) * 1669101435) & 0xff
b = ((idx + 1) * 309030853) & 0xff
k = ((idx + 2) * 209897853) & 0xff
return b ^ k

def programH(in_data):
num = 0
ret = [0] * len(in_data)
for i in range(len(in_data)):
num2 = programG(num)
num += 1
num3 = in_data[i]
num3 = programE(num3, num2)
num3 = programB(num3, 7)

num4 = programG(num)
num += 1
num3 = programE(num3, num4)
num3 = programD(num3, 3)
ret[i] = num3
return ret

def programJ(z):
b = 5
num = 0
value = ''
bytes_v = ''
while True:
if b == 1:
num += 4
b += 2
elif b == 2:
num = num * 2738
b += 8
elif b == 3:
num += programF(6)
b += 1
elif b == 4:
z = programB(z, 1)
b += 2
elif b == 5:
num = int('1F7D1482', 16)
b -= 3
elif b == 6:
break
elif b == 7:
num += int(value)
b -= 6
elif b == 10:
bytes_v = 'MzQxOTk='.decode('base64')
b += 4
elif b == 14:
value = bytes_v
b -= 7

return (z ^ num) & 0xff


from PIL import Image
def extractData():
img = Image.open('flag.bmp').convert('RGB')
data = []
for i in range(img.width):
for j in range(img.height):
r, g, b = img.getpixel((i, j))
num = (r & 7) | ((g & 7) << 3) | ((b & 3) << 6)
data.append(num)

r, g, b = img.getpixel((0, 0))
num = (r & 7) | ((g & 7) << 3) | ((b & 3) << 6)
print num
return data

def dataDeTranslate(data):
result = []
num = 0
for d in data:
num2 = programG(num)
num += 1
num4 = programG(num)
num += 1

num3 = d
num3 = programB(num3, 3)
num3 ^= num4
num3 = programD(num3, 7)
num3 ^= num2
result.append(num3 & 0xff)
with open('flag2.bmp', 'wb') as f:
f.write(''.join(map(chr, result)))




data = extractData()
dataDeTranslate(data)

处理运行得到flag.bmp

还是一张普通的图,再把flag.bmp输入得到flag2.bmp,得到真正的flag

6-flag

6-flag2


PS: 这题算是.net的骚操作了,对.net完全不熟,真的脑壳疼,全程靠猜

至于为什么不能调试,这其实又是.net的一个骚操作

根据官方writeup,这是一个anti-debug,没太看懂是什么原理,似乎他是会把getJit hook加上IncrementMaxStack,但是还是没懂为什么就抛异常了

然后把dnspy的反反调试去掉就又好了

官方的一种解法很值得我们学习:

Black Box Analysis

通过对黑盒输入空白图片,统计输出规律,进行暴力破解还原隐藏的byte

详细方法可参考官方题解

https://www.fireeye.com/content/dam/fireeye-www/blog/pdfs/FlareOn6_Challenge6_Solution_BMPHIDE.pdf

官方的另一种解法就是通过写逆了

7 - wopr

这题乍一看不知道什么鬼,但其实看一下搜到的strings就能发现许多python相关的字符串

可以推断出用的pyinstaller打的包

通过https://github.com/countercept/python-exe-unpacker提取出pyc

提取出来要注意这是python3的pyc,有些pyc缺少了文件头,需要自己加上,有些不用,不然用uncompyle6不能反编译

但是extract出来一大堆文件,里面还包含不少库文件,怎么定位到到底是哪个文件呢?这个提取出来不像别的包含跟exe同名的script

我用的方法非常简单,跑起来,ctrl+c

7-identify

然后给cleanup加上pyc header,这里注意python的版本,在extract的时候可以得到

header = [0x42, 0x0d, 0x0d, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]

他会通过

code = lzma.decompress(fire(eye(__doc__.encode()), *bytes*([i]) + BOUNCE))

提取出隐藏的代码。里面许多函数不需要逆,直接cv了用就好

但这里直接跑是拿不到code的

注意__doc__.encode(),从doc里拿到的全是空格,怎么可能包含数据,这种tricks以前就遇到过了,用tab+space的组合隐藏数据,直接查看pyc的确能看到一大堆tab+space组合的数据,看来是uncompyle6把数据抹掉了

直接提出来就拿到真实跑的代码了

然后就能发现一堆约束,直接用z3解就完事了

其中一个关键的key是binary的代码段md5,要先计算出reloc固定的偏移,原代码中是直接搜索本进程的内存,想要直接沿用代码,必须注入进去,这步似乎挺麻烦的,因为我直接理解后手动计算了

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
#-*-coding:utf-8-*-

import hashlib
from z3 import *
import struct

def patchText(data, reloc, base):
e = 0
textvaddr = 0x1000
# cal = 0
while e <= len(reloc) - 8:
addr, size = struct.unpack_from('=II', reloc, e)
if addr == 0 and size == 0:
break

slot = reloc[e+8: e+size]

for i in range(len(slot) >> 1):
# print 'solt: ' + str(i)
value, = struct.unpack_from('=H', slot, 2*i)
f = value >> 12
if f != 3: continue
value = value & 0xfff
ready = addr + value - textvaddr
if 0 <= ready < len(data):
# numstr = (struct.unpack_from('=I', data, ready)[0] - base)
# print hex(ready) + ' ' + hex(numstr)
# print hex(ready)
# data = data[:ready] + struct.pack('=I', numstr) + data[ready+4:]
struct.pack_into('=I', data, ready, (struct.unpack_from('=I', data, ready)[0] - base))

e += size

return data

def getH():
data = open('7 - wopr/wopr.exe', 'rb').read()
textdata = data[0x400:0x400+0x1f224]
reloc = data[0x49a00:0x49a00+0x17b8]
data = patchText(bytearray(textdata), bytearray(reloc), 0x400000)
return hashlib.md5(data).digest()


def solve(h):
x = [BitVec('x'+str(i), 8) for i in range(16)]
s = Solver()
s.add(h[0] == x[2] ^ x[3] ^ x[4] ^ x[8] ^ x[11] ^ x[14])
s.add(h[1] == x[0] ^ x[1] ^ x[8] ^ x[11] ^ x[13] ^ x[14])
s.add(h[2] == x[0] ^ x[1] ^ x[2] ^ x[4] ^ x[5] ^ x[8] ^ x[9] ^ x[10] ^ x[13] ^ x[14] ^ x[15])
s.add(h[3] == x[5] ^ x[6] ^ x[8] ^ x[9] ^ x[10] ^ x[12] ^ x[15])
s.add(h[4] == x[1] ^ x[6] ^ x[7] ^ x[8] ^ x[12] ^ x[13] ^ x[14] ^ x[15])
s.add(h[5] == x[0] ^ x[4] ^ x[7] ^ x[8] ^ x[9] ^ x[10] ^ x[12] ^ x[13] ^ x[14] ^ x[15])
s.add(h[6] == x[1] ^ x[3] ^ x[7] ^ x[9] ^ x[10] ^ x[11] ^ x[12] ^ x[13] ^ x[15])
s.add(h[7] == x[0] ^ x[1] ^ x[2] ^ x[3] ^ x[4] ^ x[8] ^ x[10] ^ x[11] ^ x[14])
s.add(h[8] == x[1] ^ x[2] ^ x[3] ^ x[5] ^ x[9] ^ x[10] ^ x[11] ^ x[12])
s.add(h[9] == x[6] ^ x[7] ^ x[8] ^ x[10] ^ x[11] ^ x[12] ^ x[15])
s.add(h[10] == x[0] ^ x[3] ^ x[4] ^ x[7] ^ x[8] ^ x[10] ^ x[11] ^ x[12] ^ x[13] ^ x[14] ^ x[15])
s.add(h[11] == x[0] ^ x[2] ^ x[4] ^ x[6] ^ x[13])
s.add(h[12] == x[0] ^ x[3] ^ x[6] ^ x[7] ^ x[10] ^ x[12] ^ x[15])
s.add(h[13] == x[2] ^ x[3] ^ x[4] ^ x[5] ^ x[6] ^ x[7] ^ x[11] ^ x[12] ^ x[13] ^ x[14])
s.add(h[14] == x[1] ^ x[2] ^ x[3] ^ x[5] ^ x[7] ^ x[11] ^ x[13] ^ x[14] ^ x[15])
s.add(h[15] == x[1] ^ x[3] ^ x[5] ^ x[9] ^ x[10] ^ x[11] ^ x[13] ^ x[15])

s.check()
result = s.model()
return [result.get_interp(x[i]).as_long() for i in range(16)]



def fire(wood, bounce):
meaning = bytearray(wood)
bounce = bytearray(bounce)
regard = len(bounce)
manage = list(range(256))

def prospect(*financial):
return sum(financial) % 256

def blade(feel, cassette):
cassette = prospect(cassette, manage[feel])
manage[feel], manage[cassette] = manage[cassette], manage[feel]
return cassette

cassette = 0
for feel in range(256):
cassette = prospect(cassette, bounce[(feel % regard)])
cassette = blade(feel, cassette)

cassette = 0
for pigeon, _ in enumerate(meaning):
feel = prospect(pigeon, 1)
cassette = blade(feel, cassette)
meaning[pigeon] ^= manage[prospect(manage[feel], manage[cassette])]

return bytes(meaning)

def getFlag(x):
eye = [219, 232, 81, 150, 126, 54, 116, 129, 3, 61, 204, 119, 252, 122, 3, 209, 196, 15, 148, 173, 206, 246, 242, 200, 201, 167, 2, 102, 59, 122, 81, 6, 24, 23]
flag = fire(eye, x).decode()
return flag

if __name__ == "__main__":
xor = [212, 162, 242, 218, 101, 109, 50, 31, 125, 112, 249, 83, 55, 187, 131, 206]
h = getH()
h = [ord(h[i]) ^ xor[i] for i in range(16)]
x = solve(h)
print ''.join(map(chr, x))
print getFlag(x)

PS:这题官方的解法中规中矩,没有太特别的地方

看了一下别人的解法,是直接dump memory修改了一下代码直接用的

flag是 L1n34R_4L93bR4_i5_FuN@flare-on.com

emmm?linear algebra? 🙃

8 - snake

居然是个nes的镜像

找了一些资料

http://wiki.nesdev.com/w/index.php/Bad_Apple

http://wiki.nesdev.com/w/index.php/Emulators

然后选择了一款看着挺好用的模拟器

FCEUX emulator

有点像cheat Engine的做法

可以在内存中搜索值,然后可以定位到吃苹果的个数,也就是分数值

在那个内存设定break in read

就可以在吃到苹果的一刻断下,查看附近代码,能看到两个关键点

cmp 0x33 和 cmp 0x4

然后修改内存后,flag就出来了

1
2
0x0025: 吃的苹果个数        0x33时进行下一关
0x0027: 当前速度(关卡?) 0x04时...flag出来了

8-getflag

PS:这题是真的很简单呀

在twitter上看到有人真的靠玩出来的😂 tqltql

官方还提到一种做法,是跟NES的PPU data相关的

Nes把图案显示到屏幕,是通过把像下面的title map写到PPU Name Table上的

map的时候,按下面Normal title map,0-9A-Z应该是对应着从数字0数到35,但是通过模拟器的PPU viewer,可以看到他被打乱的,同时通过调试可以看到他还做了一些小混淆,计算显示时把data-0x10,所以通过这种方法恢复后,就可以直接从binary中搜到flag了

8-ppu

9 - reloadered

main逻辑很简单,但是还原不出来flag

拿其中一个有效flag输入是显示错误的(wrong key),但是在调试的时候则显示正确

message提到

I hear that it caused problems when trying to analyze it with ghidra.

似乎用ghidra分析会报异常?

不过用ida没事

并且用OD调试的时候,发现在原main逻辑代码底下有另外一套相似的代码,也包含wrong key

看来,程序应该是做了一些有趣的骚操作

尝试了几次,发现了一个东西,就是我反复执行的时候,有时候显示的UI界面不一样!

有时包含logo,有时只有单独一行字符

尝试了一下,捉摸不到显示的规律,结合ida分析,一开始分析的代码应该是包含logo的那个

不知道是什么原理,直接在ida中搜索找不到相关的代码

检查了一下md5,binary是一直没有任何修改的

换种调试方法

直接启动后,通过attach上去调试

发现其rebase到0x0010000的位置

在0x112D0发现奇怪的代码,里面还包含了GetModuleHandleA(0)的函数

大致看了一下是直接在进程了修改了相关代码,运行完以后直接就一大片nop掉了

前面还看到了反调试的代码,还有很多不知道什么check,但是不重要,因为我们已经拿到代码了

代码非常简单,跟前面的逻辑差不多

直接就能解出来flag了


PS: 89这两题,感觉就有点过于简单了

但是对于这题,背后实现原理远比解题要有意思

为什么会显示不同的逻辑?

首先,正常逻辑包含有几部分的代码

anti-vm, anti-debug, decoding, testing the password

而关键位置anti-vm部分是通过时间检测实现的,这是个非常玄的检测方式,这也就说明了直接跑为什么有时会显示正常逻辑,有时候会显示假逻辑

但是直接从start分析也没那么好分析

隐藏函数在0x112d0的偏移,但是跳转过去会查看到一大片nop

官方wp没有详细说明这个函数是怎么生成的

似乎是跟base relocation table有关?

事实是,这跟PE的加载机制有关,这片代码不是从start的地方生成的,只要binary加载到内存上就会生成这片代码

具体查看https://corkamiwiki.github.io/PE#relocations

文档也给出了实现代码https://github.com/angea/corkami/blob/master/src/PE/reloccrypt.asm

看了一下,没太看懂= =,这应该算是一种基于PE loader操作的加密方式

10 - Mugatu

这题一开始分析的时候看得我一头雾水,各种api乱调用的感觉

行吧,调试看看,结果一调试发现,实际的import table跟显示出来的不一样!

看来是做了些混淆

对PE不熟悉,手动根据调试信息恢复了一下导入表,总算能正常静态分析了

首先,这是一个勒索病毒,会把gif图片加密

但是直接分析没什么特别的头绪,程序从资源中加载了两张图片,分别为G和F

那么这两张什么图片呢?

直接debug跟踪了一下,把图片数据dump了出来,发现。。。F在用BitBlt的xor处理完后是个dll

总结一下是这么个操作

1
2
3
4
5
6
7
8
9
1. 获取本地信息Src
2. http get获得xml,把本地信息Src与title进行循环xor得到SrcXor
3. 组成packet: 0x1FACEEEE | Size | SrcXor , 进行base64编码,得到Base64Packet
4. 获取到到pubdata作为http包additional header,把Base64Packet作为数据post到mugatu.flare-on.com
5. 返回数据进行base64解码,并检查header xor 0x4D是否为"orange mocha frappuccino",若是,则写到文件`\\.\mailslot\Let_me_show_you_Derelicte`
6. 从binary load image g和f,用gdi32_BitBlt两张图片xor,并用gdi32_GetObjectW提取信息,
7. data段数据,VirtualProtect 修改成 rwx,没细看,image_f 包含一个dll数据
8. createThread,调用dll的导出函数,
...

然后就可以去分析dll了

这个dll也做了跟exe一样的import table混淆

结合调试,得到参数结构体的数据

就是从硬盘中搜索gif并加密

加密函数看了看,非常眼熟,😯是个xtea

但是密钥是刚开始的时候访问网络得到的,那个是个现在已经访问不了的url

但题目还有个提示,文件the_key_to_success_0000.gif.Mugatu

直接解出来

10-out

密钥是4个byte的,这么就3个byte直接爆破了

得到flag

10-flag


PS: 这gif图怎么这么骚气!(Dear God…It’s beautiful)

这题有意思的地方应该是在import table混淆上

emmmm 看了一下官方wp,原来这个混淆是这么简单,在WinMain调用之前,start的最开始调用了一个函数,把import address table(IAT)翻转了。。。因为反编译器通常都不会处理这种情况

至于怎么去混淆,也不用像我这么蠢手动恢复,直接在跑起来后dump memory再分析就好了

11 - vv_max

这题就有意思了,上来先给你来个AVX2指令集

先检查一下CPU支不支持AVX2,然后就开始了程序的逻辑

大概看出来像是个VM,对那个贼大的结构体恢复了一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct YMM{
char value[32];
};

struct VM{
char text[2048];
//reg1: 2048 0x800
//reg2: 2304 0x900
//reg3: 2560 0xa00
//reg4: 2861 0xb00
YMM reg[32];
//rip: 3072 0xc00
QWORD rip;
void *funclist[24];
//data: 3272 0xcc8
char data[1848];
};

然后看了半天的文档,写了个反汇编器

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
#-*-coding:utf-8-*-

def disasm(text):
f = open('dis.txt', 'w')
ip = 0
while ip < len(text):
f.write('{}: '.format(ip))
if ip > 2 and ip < 32+3:
ip += 1
f.write('reserve INPUT1\n')
elif ip >= 37 and ip < 37 + 32:
ip += 1
f.write('reserve INPUT2\n')
elif text[ip] == 0:
ip += 1
f.write('EmptyReg\n')
elif text[ip] == 1:
param1, param2, param3 = text[ip+1], text[ip+2], text[ip+3]
ip += 4
f.write('muladd16 r{}, r{}, r{}\n'.format(param1, param2, param3))
elif text[ip] == 2:
param1, param2, param3 = text[ip+1], text[ip+2], text[ip+3]
ip += 4
f.write('muladd32 r{}, r{}, r{}\n'.format(param1, param2, param3))
elif text[ip] == 3:
param1, param2, param3 = text[ip+1], text[ip+2], text[ip+3]
ip += 4
f.write('xor r{}, r{}, r{}\n'.format(param1, param2, param3))
elif text[ip] == 4:
param1, param2, param3 = text[ip+1], text[ip+2], text[ip+3]
ip += 4
f.write('or r{}, r{}, r{}\n'.format(param1, param2, param3))
elif text[ip] == 5:
param1, param2, param3 = text[ip+1], text[ip+2], text[ip+3]
ip += 4
f.write('and r{}, r{}, r{}\n'.format(param1, param2, param3))
elif text[ip] == 6:
param1, param2 = text[ip+1], text[ip+2]
ip += 3
f.write('not r{}, r{}\n'.format(param1, param2))
elif text[ip] == 7:
param1, param2, param3 = text[ip+1], text[ip+2], text[ip+3]
ip += 4
f.write('add8 r{}, r{}, r{}\n'.format(param1, param2, param3))
elif text[ip] == 8:
param1, param2, param3 = text[ip+1], text[ip+2], text[ip+3]
ip += 4
f.write('sub8 r{}, r{}, r{} # p1 = p2 - p3\n'.format(param1, param2, param3))
elif text[ip] == 9: # not check below
param1, param2, param3 = text[ip+1], text[ip+2], text[ip+3]
ip += 4
f.write('add16 r{}, r{}, r{}\n'.format(param1, param2, param3))
elif text[ip] == 10:
param1, param2, param3 = text[ip+1], text[ip+2], text[ip+3]
ip += 4
f.write('sub16 r{}, r{}, r{}\n'.format(param1, param2, param3))
elif text[ip] == 11:
param1, param2, param3 = text[ip+1], text[ip+2], text[ip+3]
ip += 4
f.write('add32 r{}, r{}, r{}\n'.format(param1, param2, param3))
elif text[ip] == 12:
param1, param2, param3 = text[ip+1], text[ip+2], text[ip+3]
ip += 4
f.write('sub32 r{}, r{}, r{}\n'.format(param1, param2, param3))
elif text[ip] == 13:
param1, param2, param3 = text[ip+1], text[ip+2], text[ip+3]
ip += 4
f.write('add64 r{}, r{}, r{}\n'.format(param1, param2, param3))
elif text[ip] == 14:
param1, param2, param3 = text[ip+1], text[ip+2], text[ip+3]
ip += 4
f.write('sub64 r{}, r{}, r{}\n'.format(param1, param2, param3))
elif text[ip] == 15:
param1, param2, param3 = text[ip+1], text[ip+2], text[ip+3]
ip += 4
f.write('muldq r{}, r{}, r{}\n'.format(param1, param2, param3))
elif text[ip] == 16:
param1, param2 = text[ip+1], text[ip+2]
ip += 3
f.write('movdq r{}, r{}\n'.format(param1, param2))
elif text[ip] == 17:
param1 = text[ip+1]
ip += 2
f.write('movdq r{}, mem[{}]\n'.format(param1, ip))
array = [text[ip + j] for j in range(32)]
f.write('\t({}-{}): '.format(ip, ip+32) + str(array) + '\n')
ip += 32
elif text[ip] == 18:
param1, param2, param3 = text[ip+1], text[ip+2], text[ip+3]
ip += 4
f.write('srld r{}, r{}, {}\n'.format(param1, param2, param3))
elif text[ip] == 19:
param1, param2, param3 = text[ip+1], text[ip+2], text[ip+3]
ip += 4
f.write('slld r{}, r{}, {}\n'.format(param1, param2, param3))
elif text[ip] == 20:
param1, param2, param3 = text[ip+1], text[ip+2], text[ip+3]
ip += 4
f.write('shufb r{}, r{}, r{}\n'.format(param1, param2, param3))
elif text[ip] == 21:
param1, param2, param3 = text[ip+1], text[ip+2], text[ip+3]
ip += 4
f.write('permd r{}, r{}, r{}\n'.format(param1, param3, param2))
elif text[ip] == 22:
param1, param2, param3 = text[ip+1], text[ip+2], text[ip+3]
ip += 4
f.write('cmpeq r{}, r{}, r{}\n'.format(param1, param2, param3))
elif text[ip] == 23:
ip += 1
f.write('nop\n')
elif text[ip] == 0xff:
ip += 1
f.write('exit\n')
else:
f.write('unknown: {}'.format(text[ip]))
ip += 1

f.close()



if __name__ == "__main__":
text = open('text.bin', 'r').read()
text = map(ord, text)
disasm(text)

然后就是要去逆这个东西了

总结了一下最后检查部分的要求

1
2
3
4
final check:
1. reg2 == reg20
2. input1[:9] == 'FLARE2019'
flag = reg1 xor reg31

其中,R20是个固定的数组,可以直接从调试中获得

R2跟我们的输入有关,接下来就是看R2到底进行了哪些操作了

1
2
3
4
5
6
7
8
9
10
11
12
13
781: srld r7, r1, 4
813: and r7, r7, r6
849: cmpeq r8, r1, r6 // 0000000 ?
889: add8 r7, r8, r7 // equal - 1 / not equal = 0
901: shufb r7, r5, r7

949: add8 r2, r1, r7
1029: muladd16 r7, r2, r10
1109: muladd32 r2, r7, r11
1297: shufb r2, r2, r12
1421: permd r2, r13, r2
1029: muladd16 r7, r2, r10
1109: muladd32 r2, r7, r11

主要翻了半天文档,去理解这些指令

刚开始我打算直接写Z3跑的,但在理解了这些指令的运算后,感觉好像没那么好写?

而且关键是,这指令的个数不多,应该可以直接写逆

但是这时有个关键点,muladd这种指令不好写逆

这时,我注意到一个东西,因为当中包含有常数数组,所以结合起来有如下的特点

1
2
3
muladd32:
r2[31:0] = 0x1000 * r7[15:0] + r7[31:16]
= r2[7:0] << 18 + r2[15:8] << 12 + r2[23:16] << 6 + r2[31:24]

6,12,18,24

每一段只占6个bit

这让我想起了base64

但是结合上下文我根本看不出来这是一个base64算法,因为还有一些奇怪的操作

但这时我从网上搜到这篇,用AVX2实现base64算法https://arxiv.org/pdf/1704.00605.pdf

哦对!我还可以调试,直接当成黑盒看一下输入输出的关系

然后就发现。。。

这是一个base64 decode

然后就出来了

1
2
3
.\vv_max.exe FLARE2019 cHCyrAHSXmEKpyqoCByGGuhFyCmy86Ee
That is correct!
Flag: AVX2_VM_M4K3S_BASE64_C0MPL1C4T3D@flare-on.com

PS: 骚操作真多

这题官方解法没有很特别的地方

以后遇到这些算法就学会了,先当成黑盒去猜,然后再去逆

12 - help

这是我做过最最最最难的一道题了!前面11道题,基本花费时间都不会超过两天,这题,断断续续抽空做弄了足足20天!我真是太太太太太菜了,但真的学到了非常多

首先说说这题吧,给了一个window的crash dump文件,大小2G,然后还有一个pacp

翻了一遍,发现一堆一堆的192.168.1.244和192.168.1.243的TCP交互数据,但是看不出来是啥数据

有一堆以8 bytes重复的字符

还看到了一些正常http访问的数据包

那么就去分析crash dump文件吧

因为之前没有弄过,所以先去网上搜了一波,用windbg拿到了一些信息

1
2
3
4
5
6
7
8
9
Windows 7 Kernel Version 7601 (Service Pack 1) UP Free x64
Product: WinNt, suite: TerminalServer SingleUserTS Personal
Built by: 7601.18741.amd64fre.win7sp1_gdr.150202-1526
...
BugCheck 7E, {ffffffffc0000005, fffffa8003f9c621, fffff88007c6b958, fffff88007c6b1b0}

*** WARNING: Unable to verify timestamp for man.sys
*** ERROR: Module load completed but symbols could not be loaded for man.sys
Probably caused by : man.sys ( man+1ce7 )

看起来,问题出在了man.sys这个文件上,因为题目描述说道是病毒文件存在bug把电脑搞崩了

那么怎么分析这个文件呢

有一个很出名的工具叫volatility

1
volatility.exe -f '.\12 - help\help.dmp' --profile=Win7SP1x64 modscan > .\ana\modscan

Then I found man.sys by modscan.

But when I try to dump this file,

volatility.exe -f '.\12 - help\help.dmp' --profile=Win7SP1x64 moddump -D dump2 --regex=man.sys

I got
0xfffff880033bc000 man.sys Error: e_magic 0000 is not a valid DOS signature.

emmmmm 这个文件的文件头似乎被抹掉了

然后就发现可以用volshell dump出来

https://lists.volatilityfoundation.org/pipermail/vol-users/2013-March/000829.html

1
2
3
>>> data = addrspace().zread(assumed_base_address, assumed_module_size)
>>> with open('file.dmp', 'wb') as f:
...... f.write(data)

dump出来后,因为没有文件头,分析不了

后来我发现ghidra可以直接识别到函数,所以就用ghidra做了(后来别人告诉我其实ida也可以的,不过只是要在初始化时选一个参数)

这样我就不用一个个找0x55然后C了


因为刚开始只有一个去掉文件头的man.sys

我当时在想里面会不会藏有什么东西,就试着binwalk了一下,果然发现了一个dll文件

根据pdb字符串,这个就叫m.dll

e:\dropbox\dropbox\flareon_2019\code\cd\objchk_win7_amd64\amd64\m.pdb

分析m.dll,去掉RC4的混淆,逆完发现他是在本地监听4444端口,然后根据发送过来的数据包调用相应的ioctl

看来还是得继续逆man.sys

man.sys好多奇怪的操作

emmmmm 还有好多疑似import的函数

12-libFunc

emmmm 这个地址,是什么函数呢?

啊!我能直接通过windbg查看!

这么就能一个个恢复出来了

12-getlibFuncInWindbg

花了比较长的时间去逆man.sys,主要注意到他的ioctl的操作,都是从一个链表中搜索代码出来调用,调用的似乎都是export函数

在man.sys初始化的时候,会通过同样的函数调用m.dll的export函数,从而在本地开启4444端口监听

那么就计划先来分析一下4444端口接收到的数据进行了什么ioctl的调用吧

诶??这数据完全对不上??

我是哪里弄错了吗?

卡了一轮,无果,我以为我是开头哪里搞错了

上twitter问了一下大佬@hasherezade

How many DLLs did you extract? There is one dll which functionality is to encrypt the traffic…

emmmmmm

我太菜了

好的,然后我就知道往哪个方向去继续做了

我用volatility把基本能跑的都跑了一遍,似乎好像没发现什么特别的东西

在driver、module相关的信息里面倒是看到挺多flare on字样的驱动等等,但是似乎都提不出来,定位不到文件

这时看回man.sys,我主要到一个东西,加载的m.dll是要先attach到pid=876的进程上!

然后去查看pid 876,是svchost.exe,针对这个进程,emmmmm各种方法都找不到那些隐藏的dll

然后试着用yarascan命令搜了一下driver的ID FLID

突然发现,在附近带有pdb的字符串!,我可以直接搜索pdb!

volatility.exe -f '.\12 - help\help.dmp' --profile=Win7SP1x64 yarascan -Y dropbox\\dropbox\\flareon_2019\\code --pid=876 > .\ana\yarascan_code

Jesus…

1
2
3
4
5
6
7
8
9
24d0a4 E:\Dropbox\Dropbox\flareon_2019\code\stmedit\sys\x64\Release\stmedit.pdb
2934fc e:\dropbox\dropbox\flareon_2019\code\cryptodll\objchk_win7_amd64\amd64\c.pdb
29650c e:\dropbox\dropbox\flareon_2019\code\networkdll\objchk_win7_amd64\amd64\n.pdb
2a0508 e:\dropbox\dropbox\flareon_2019\code\keylogdll\objchk_win7_amd64\amd64\k.pdb
2a74fc e:\dropbox\dropbox\flareon_2019\code\screenshotdll\objchk_win7_amd64\amd64\s.pdb
2ac4fc e:\dropbox\dropbox\flareon_2019\code\filedll\objchk_win7_amd64\amd64\f.pdb
6cbe124 e:\dropbox\dropbox\flareon_2019\code\id\objchk_win7_amd64\amd64\man.pdb
6cbf600 e:\dropbox\dropbox\flareon_2019\code\cd\objchk_win7_amd64\amd64\m.pdb
1af27b80 e:\dropbox\dropbox\flareon_2019\code\shellcodedriver\objchk_wxp_x86\i386\driver1.pdb

然后我就结合binwalk把这些都dump出来了

注意到他们目录名称非常有意思

stmedit cryptodll networkdll keylogdll screenshotdll filedll shellcodedriver

刚开始我完全没看懂stmedit是什么东西,逆了一下完全没看懂,非常复杂

然后我去搜了一下……搜到了一个同名的MS的driver example

https://github.com/microsoft/Windows-driver-samples/tree/master/network/trans/stmedit

原来这就是那个加密TCP流的driver!

他通过WPF的api替换TCP上的数据

结合这MS的sample逆了大半天,终于找到那个关键的函数了,是一个8bytes密钥的xor

并且,能从这个sys里面找到初始化4444端口的密钥,但是别的端口都是通过ioctl配置的,根本找不到密钥

先解4444端口的好了

刚开始我想沿用第4题的方法,同scapy提出来,但是存在一个问题,数据太多TCP包被拆分了,需要TCP重组,然后又是查了半天的资料,后来发现直接用wireshark就可以提出来了= =

然后就把4444端口的数据解出来了


首先,数据大量调用了0xd180dab5的ioCode

解析出来的数据类似下面

1
{'buf': 'GG\xda\xbe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'ioCode': '0xd180dab5', 'size': 26}

结合man.sys可以猜测这是远程发送控制调用相应的函数,其中buf的前4个byte是指明调用的哪个dll,但是我找不到这些值是在哪里初始化的,也就是说我并不知道他这调用了哪个函数

先总结一下前面提取出来的dll的作用,很快就逆了一遍,这都不难

1
2
3
4
5
6
7
8
9
stmedit.sys: 	TCP包加密
c.dll: 数据加密,先用lznt1压缩,再用rc4加密,密钥是username 'FLARE ON 2019\x00'
n.dll: 数据打包,发送到ip 192.268.1.243
k.dll: 键盘记录,保存并打包
s.dll: 屏幕截图,bmp格式
f.dll: 文件操作,读文件,文件搜索,文件写等
man.sys: 控制driver
m.dll: 4444监听端口
driver1.sys: 执行shellcode

于是总结了一下数据包

4444接受发送来的数据

6666 7777 8888 都是发送到ip 192.268.1.243的数据

其中7777dump出来的数据包非常有规律,正是8byte的循环,虽然上面dll还有个数据加密的dll,但7777没有用到

后面再细逆man.sys就会发现,在dll的函数调用完毕后,会有一个设置的标志位,只有设置了那个标志位的数据,才会调用c.dll进行数据加密

6666 8888传输的数据均经过加密,而7777则没有

因为4444数据中包含了接收文件的操作,和有显示出来的字符串C:\\keypass\\keys.kdb,可以确定几个man.sys调用的函数

4444接收的文件我dump出来看了一下,其实正是driver1.sys,这个我们已经分析过了

而那个字符串,结合调用的f.dll逆,可以发现他的操作是

  1. 在C盘搜索keys.kdb文件
  2. 发送keys.kdb文件出去

那么6666 7777 8888 端口的数据我们就能猜测一下,分别是发送的key.kdb数据,屏幕截图数据和键盘记录数据

其中7777的数据量最大,数据包最多,可以先猜测这是屏幕截图数据

因为只是xor加密,而7777每次发送的数据都是packet_len + bmpdata的结构

bmp头有十几个byte的数据是已知的,那么就可以破解出密钥!

key = 'J\x1fK\x1c\xb0\xd8%\xc7'

当我把屏幕截图解完,就发现了非常有意思的图片

12-pkt150

12-pkt164

结合图片可以知道flag就藏在keys.kdb里面!(不是看到这个我都差点忘了我是要找flag的了。。。)

打开keys.kdb的master key长度是18

接下来自然可以想到目标是解密出6666和8888端口的数据

拿到keys.kdb文件,并且从keylogger数据中读到密码


解密6666 8888的数据

7777的密钥是我们直接破解出来的,但是6666和8888的数据因为还再加了一层加密,就很难这么干了

因为加密顺序是LZNT1 compress -> RC4 with key ‘FLARE ON 2019\x00’ -> xor with xor key

可以先猜出前面的一点点数据,然后同理加密后破解

但尝试了一段时间没有成功,也不知道哪里出问题了,理论上是可以的

于是我又卡住了

之后我在dump数据包出来的时候,发现了一样东西12-pkgtrick

这里是同一个TCP数据包,怎么有两块长度一样的?

然后翻到下面的数据包,发现开头是\x10\x09\x00\x00,这不正是发出去的数据包的格式,前4个byte是包长度吗

但是正常情况应该是这4个字节也经过了xor加密

然后我就发现,这个包里面前半部分是经过xor加密的数据,后半部分是未经过xor加密的数据,两个数据是一样的。。。(或许是正如截图里的encrypt加密了两次?)

总的来说,这是stmedit没有逆清楚的锅,有可能是因为API用的Inject,原数据没有删掉(这里原因还不清楚

总之,接着这个思路6666和8888的数据都拿到了

keys.kdb拿到了

keylog也拿到了

从keylog拿到的密钥是th1sisth33nd111,这只有15位,还缺了3位

再细逆keylogger,发现这个keylogger问题很大

  1. 特殊字符不能记录到
  2. 大小写统一都转换成小写

从twitter上看到别人说从截图推断出来密码,嗯???

然后我就有点点被误导了

根据语义试了一堆

1
2
3
4
5
Th1s_Is_TH3_3nd111
Th1s_Is_Th3_3nd111
Th1s-is_th3-3nd111
Th1s-is-th3_3nd111
...

卡了整整一天后,我就去问大佬了

超感谢@zvikam非常多的提示,指导我怎么去找到这个key

试着从dump文件中搜了一下3nd

居然找到了!

Th!s_iS_th3_3Nd!!!

终于拿到了flag


其中前面我刚开始爆破的时候思路有点错了,我着重爆破的是那3个不可见字符,大小写没有太多的考虑

并且!我没想到叹号会被转化成1,现在再细看,的确就是这样的,这个keylogger对于特殊字符没有检查shift,所以0-9键盘上的特殊字符都会被转化成数字

所以其实爆破也是一个正解,只不过我爆破的方向不对

这整一个病毒的思路非常明确

Man.sys是主病毒,通过4444端口接收控制数据

再发送捕获的数据到 6666 7777 8888 端口


PS:官方题解是直接用的windbg做的,tql

这里说好一下一些我自己做的时候没注意到的关键点

crash dump的原因是因为man.sys下载了一段32位的驱动,但系统是64位的,当32位驱动试图运行在64位系统的时候,系统crash了

至于man.sys头部被抹掉,官方的一种做法是把另外的sys文件头复制过来,再手动修正,只要确保.text是对的就OK了,其他部分可以一律看作.data

我是手动恢复RC4混淆的字符串的。。的确是太蠢太麻烦了,可以通过ida的插件辅助完成

https://github.com/fireeye/flare-emu

emmmm关于我提取dll的方式,那的确是另一种方式

这些dll不是全都注入到svchost里的,因为有些dll的安装方式是通过ioctl,最终这些信息都储存在man.sys的那个链表中

关键点在这,因为这些都是直接从crash dump中获得的,那个链表我们其实可以直接通过windbg跟踪查看具体数据,通过这个信息,我们可以看到具体是注入到哪个进程,以及dll捆绑的端口是多少

引用一下官方wp的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _INJECTED_PAYLOADS
{
ULONG moduleId;
LIST_ENTRY link;
KMUTEX payloadMutex;
ULONG_PTR baseAddress;
ULONG_PTR runOffset;
ULONG_PTR loadedDllSize;
BOOLEAN isExfilDll; // Sends data to 192.168.1.243
BOOLEAN isCryptoDll; // Used to encrypt some payloads’ return data
ULONG exfilPort; // Set per payload at injection time
PEPROCESS injectedProcess;
} INJECTED_PAYLOADS, *PINJECTED_PAYLOADS;

像这些都是可以获得的

同理,stmedit在安装端口加密也是通过ioctl实现的,并且同样包含一个链表,dump出这个链表同样是可以找到加密的key,并不是一定要靠猜的


关于其他大佬的做法,也能学到很多

https://bruce30262.github.io/flare-on-challenge-2019-write-up/#level-12

中提到,在逆man.sys的时候,要恢复库函数,可以通过Volatility的帮助

Later did I know that you can just use Volatility’s impscan command to identify calls to APIs. The command even has an option to generate an IDA .idc file that help us mark the function name in IDA !

还有一个写得超详细的大佬在获取键盘记录上提供了一个非常刺激的思路

https://github.com/eleemosynator/writeups/tree/master/flare-on-6/12%20-%20help

大致意思就是根据windows键盘驱动云云~~可以直接从windows驱动拿到键盘Keystroke event信息

  • Keyboard issues a processor interrupt when a keystroke event (key-up or key-down) is ready to be processed.
  • The [Interrupt Service Routine] of the low-level keyboard driver i8042prt.sys handles the interrupt.
  • As the operating system may be busy with other things when the keyboard interrupt is received, i8042prt.sys stores the keystroke event in a ring buffer (which resides in Non-Paged memory) which the OS can then process at its leisure.

这个方法简直太炫酷了,这样就可以直接拿到键盘记录数据,而不是靠猜靠爆破了

高能预警!!!

然后还有大佬是………………纯粹靠数据分析????

https://sysenter-eip.github.io/FlareOn2019_NoReversing

screenshot数据是直接看出来8byte xor然后用GBS(一个工具)处理查看发现是图片,然后猜key解出来的

在看完截图知道需要获得kdb后,直接从crash dump中得到了keys.kdb文件

然后猜了一下可能存在keylogger数据,和8888端口发送的数据包用的是同一个密钥,对比不同的数据包,发现有相同的MSB存在,就用滑动窗口去破解了(Orz)并且猜出来许多windows的病毒都是使用LZNT1

剩下就跟我一样在crash dump中搜到了master key

还有一种情况,既然拿到keys.kdb了,那是不是可以直接破解这个文件?

答案是可以的,需要逆一下cng.sys

https://gist.github.com/Sin42/feb693a5b29679f8137b2d751aeb1e31

直接就把keys.kdb破解了 Orz

佩服得五体投地

总结

其实整篇文章都写得像流水账,主要记录的是我的解题过程,我当时是怎么想的

详细的writeup网上有很多,而且flareon官方的writeup也足够详细

这总是能让我学到许多

大佬们都tqltql Orz

希望明年能再接再厉

要说真的挑一道自己喜欢的题目,那可能就是12题吧,因为真的学到了许多许多,非常偏向于实际的病毒分析

但是也折磨得我够呛的Orz

感谢flareon的组织(我们明年再见(滑稽))

PPS:牌子什么时候才能寄到呢?

×

赞助gif换电脑、吃双皮奶(逃

扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

文章目录
  1. 1. 写在前面
  2. 2. 1 - Memecat Battlestation
  3. 3. 2 - Overlong
  4. 4. 3 - Flarebear
  5. 5. 4 - Dnschess
  6. 6. 5 - demo
  7. 7. 6 - bmphide
  8. 8. 7 - wopr
  9. 9. 8 - snake
  10. 10. 9 - reloadered
  11. 11. 10 - Mugatu
  12. 12. 11 - vv_max
  13. 13. 12 - help
  14. 14. 总结
,