Flare-On 2020 Writeup

这writeup是断断续续写下来的,最近事情比较多,所以也拖得比较久 (然后就没想到又拖了快4个月——2021.3.14)

在今年的flareon上,再次ak,这次排名是107,希望能拿到那根钥匙吧T_T (收到了 yeah!),去年的牌居然给我寄丢了,气死我了

再次是简单回顾总结整个比赛 (其实就是流水账writeup)

因为官方的wp已经很详细了,所以这篇会很流水账,主要记录一些跟官方不一样的东西,或者有学到的新的奇淫技巧

感觉今年的难度比去年的要高一些

官方WP:https://www.fireeye.com/blog/threat-research/2020/10/flare-on-7-challenge-solutions.html

1_-_fidler

第一题是个用python写的喂猫猫游戏

因为都是用python写的,源码一目了然,并且这题就算不会逆向,也可以直接玩通关,不用很久

想让知道详情的可以直接查看官方wp

https://www.fireeye.com/content/dam/fireeye-www/blog/pdfs/flareon7-challenge1-solution.pdf

我当时直接拼了个脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def decode_flag(frob):
last_value = frob
encoded_flag = [1135, 1038, 1126, 1028, 1117, 1071, 1094, 1077, 1121, 1087, 1110, 1092, 1072, 1095, 1090, 1027,
1127, 1040, 1137, 1030, 1127, 1099, 1062, 1101, 1123, 1027, 1136, 1054]
decoded_flag = []

for i in range(len(encoded_flag)):
c = encoded_flag[i]
val = (c - ((i%2)*1 + (i%3)*2)) ^ last_value
decoded_flag.append(val)
last_value = c

return ''.join([chr(x) for x in decoded_flag])


current_coins = 1000*1000000000
target_amount = (2**36) + (2**35)
if current_coins > (target_amount - 2**20):
while current_coins >= (target_amount + 2**20):
current_coins -= 2**20
print(decode_flag(int(current_coins / 10**8)))
# idle_with_kitty@flare-on.com

有趣的是,因为我太菜了,这个脚本拼了一段时间,当我刚刚拼凑好脚本,游戏那边也刚好通关跑出flag来了= =

2–garbage.exe

这题卡了很多人,也卡了我有点时间

首先,PE文件是损坏的,但是我们不知道是哪里损坏

直接扔进ida,会发现经过upx加壳的,直接通过upx会提示invalid overlay size

这个报错的话,因为之前大概看过一下upx的源码,好像差不多意思就是其中一个加壳的chunk大小跟header中的size不一样了

找各种工具看了很久才发现,文件最后的xml没了一块

1
2
3
4
<?xml version='1.0' encoding='UTF-8' standalone='yes'?> 
<assembly xmlns='urn:schemas-microsoft-com:asm.v1' manifestVersion='1.0'>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<securit

好吧,猜这个损坏是文件最后被截掉了一块,因为通过各种工具查看各种头都是好的,除了下面这些invalid

2-invalid

于是根据PE文件头中的文件大小在后面padding了一堆0,然后就可以用upx解压了

虽然解压出来有些地址有问题,但是并不影响看懂程序的逻辑,很快就弄出来了

C0rruptGarbag3@flare-on.com

这里就不贴代码了


但是,注意到一个东西,在message中包含了一句:

You should be able to get it working again, reverse engineer it, and acquire the flag.

似乎是可以把它恢复成可运行的binary,官方文档也介绍了其中的方法

在upx脱壳后,通过CFF explorer其实可以看到,在Import table中import的函数都是能看到的,但是缺失了import的dll名称

因此根据import的函数就可以推断出dll分别是kernel32.dll和shell32.dll

修复后,再把configuration File删掉(因为是少了一段的,这个configrue会导致程序错误)

就能够正常跑起来了 (CFF explorer真好用)

2-run

3-Wednesday

是一个类似flappy bird的憨憨小游戏,上下通过障碍物
txt里面介绍了怎么玩

好吧,那直接猜一下当达到XX分数的时候给flag

直接祭出cheat engine!

找到了分数,然后改大……诶,没什么反应

试了一轮,还是老老实实去逆吧。

发现这个是通过nim写的
https://github.com/Vladar4/nimgame2/blob/master/nimgame2/nimgame.nim

看了下感觉语法有点像脚本语言,但其实际还是编译出来的exe,因而没办法直接恢复出源码,只能照着上面示例代码慢慢看

时间有点太久了,不太记得具体逆的过程中遇到了什么问题,这里大致描述一下我的思路。

首先逆到逻辑发现总共分数达到296的时候就能够getflag,但是试过直接cheat engine修改成296没反应

或者最后出来winner的界面,但是没有flag

再细逆,发现flag是从障碍物的排序解码过来的,但是我没理清游戏操作原理,所以最后还是选择了patch

patch位置如图,对应了两个操作的patch,1、操作的cmp;2、reseteverything

3-patch

patch完后,我就等他自动跑了,这个时候撞到柱子不会死,而且也不会因为撞柱而重置

最后getflag

3-win

官方wp提到了3种方法,

  1. 找到flag buffer,然后静态解;

  2. patch program

  3. 写bot

当然也有头铁的老哥直接玩通关了……

找了下,bot的代码

https://github.com/TWVyY3VyaW8K/shitty-wednesday-solution

这是通过图像识别做的,这样就完全不用逆了,其中有个关键的库叫pyautogui,看着好像还挺好用

4-report.xls

这题给了个excel文件

一开始以为是普通的excel宏,结果发现还是too naive

按照正常的VBA分析,把宏代码去混淆,看完逻辑解密出来一个stomp.mp3

然后就什么都没发现了

而事实上,这个stomp.mp3可以算是一个hint

有个叫做 VBA stomping 的东西,参考

https://zanderchang.github.io/2019/04/30/VBA-Stomping%E7%AE%80%E4%BB%8B/

https://vbastomp.com/

简单来说,在excel宏上,打开office实际执行的不是宏代码,而是文档文件中的p-code

p-code是宏代码的编译版本

如果文档中的p-code与当前系统上的VBA版本兼容,那实际执行的是储存的p-code,宏编辑器中显示的是反编译的p-code

而如果在不同版本的excel上打开,p-code不可重用,VBA源码将重新编译成p-code

使用 VBA Stomping 技术的恶意文档只能使用用于创建文档时相同的 VBA 版本执行。

因此,这个excel中实际的flag代码要从p-code中恢复,而光通过宏编辑器是看不到的

我使用了https://github.com/bontchev/pcodedmp 反编译p-code

然后就很容易得到flag了

4-flag

5-TKApp

题目文件是个 TKApp.tpk

tpk…我首先反应怎么像apk这样,而且file了一下发现实际也是zip

查了下这个似乎是三星智能手表的应用

搭模拟器太麻烦了。。直接静态搞吧

解压出来能看到很多文件

其中最关键的是当中的一个TKApp.dll

这个dll是.net的,直接用dnspy分析

实际上整个过程没什么太大的难点,纯粹就是分析代码,然后从app的配置文件、xml等地方去找一些资源值,然后拼凑出来key

按AES处理一下就出来flag了,详细可以看官方wp

5-flag

6-codeit

这题开始有点麻烦了

整个应用是输入文字,然后会显示出来文字对应的二维码

反编译后发现,他是通过AutoIt写的应用

AutoIt is a freeware BASIC-like scripting language designed for general scripting and automating the Windows GUI.

这还是个脚本语言,搜了一轮后,用工具把脚本提出来了

具体用的是什么工具,,时间有点久远忘了,我文件夹里面还留着这两个工具

https://github.com/SanseoLab/ejExtractor

https://github.com/nazywam/AutoIt-Ripper

官方wp中也提到了两种

https://github.com/fireeye/flare-vm/issues/172

https://gitlab.com/x0r19x91/autoit-extractor

这个不太重要,能顺利吧Autoit脚本提取出来就行了

因为这个Autoit脚本经过了很严重的混淆,我写了个脚本patch一下,输出的语法肯定不对的,但是能静态逆着舒服很多

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
#... some global constant
# s='......'

arr = s.split("4FD5$")


def decode(s):
return bytes.fromhex(s).decode('utf-8')

# for i in arr:
# print(decode(i))

data = open('out.txt', 'r').read()


import re
import sys

pattern = re.compile(r'AREHDIDXRGK \( \$OS \[ .*? \] \)') # AREHDIDXRGK \( \$OS \[ .*? \] \)

func_call = pattern.findall(data)

for s in func_call:
var = s.split(' ')[4][1:]
try:
repl = decode(arr[globals()[var]-1])
data = data.replace(s, f"\"{repl}\"")
except:
print(var)
print(globals()[var])
# sys.exit(1)

for g in var_dict:
if type(var_dict[g]) == int:
data = data.replace(f"${g}", str(var_dict[g]))

open('out.patch2.txt', 'w').write(data)

逆着发现,每次生成二维码的时候,都会在当前目录有文件创建,然后马上又被删除

因为对windows不熟悉,我用了种很笨的办法。。用frida hook WIN API

让他不删除掉那个文件

1
2
3
4
var DeleteFileA = Module.findExportByName("kernel32.dll", 'DeleteFileA');

Interceptor.replace(DeleteFileA, new NativeCallback(function (pathPtr) {
}, 'int', ['pointer']));

而实际上,通过官方的wp得知,其实直接设置当前文件夹权限就行了

6-delete.png

然后发现被删掉的图片其实就是开头welcome的那张图,bmp格式,但是后面代码也会引用到了这张图

代码会经过一系列操作,通过GetComputerNameA的结果计算密钥,进行解密生成带flag的二维码

这一步卡了很久,怎么能确定ComputerName?

万万没想到这一步是靠猜的

当中有一步是引用了上面提到的图片,然后对图片前面一部分字节进行处理

看了下,这似乎是LSB

因为那堆数据是 0xFF和0xFE混搭

于是,图片隐写提取出来aut01tfan1999

试了下把计算机名改成这个,然后再生成二维码,发现出来的就是flag了

L00ks_L1k3_Y0u_D1dnt_Run_Aut0_Tim3_0n_Th1s_0ne!@flare-on.com

7-re_crowd

题目给的是一个流量包

首先,通过http的请求可以还原出他访问的网站,这个网站是题目自己编写的网站,描述了一些背景

因为网站的漏洞,服务器上的 C:\accounts.txt 被偷了

那么就接着分析流量吧

从流量包种能看到大量的像这样的http请求

7-payload

看着很奇怪,特别是PROPFIND这种请求方式,以及"(Not <locktoken:write1>)"这种字段

那么上网上搜,就能发现有个IIS的漏洞利用方式跟这段数据包非常像

漏洞详情参考https://paper.seebug.org/259/

CVE-2017-7269

这个漏洞能达成RCE

由于存在很多个类似的流量包,我按顺序把这些流量包下了下来,放在一起比较,就能发现

这些流量包最后的ascii字符都是一样的,而前面一段,则是按顺序,每一次都比上一次少2字节

分析完那个漏洞,感觉这个过程应该是在爆破路径,前面的乱码就对应的padding,后面开始是shellcode

但是这段shellcode,直接丢进IDA看,感觉非常乱,看不出来

尝试把前面VVYAIAIAIAIAIAIAIAIAIAIAIAIAIAIA搜一下,发现这是alpha shellcode

简单来说,就是用可见字符组成的shellcode

而前面则是自解码的shellcode片段

参考https://gitlab.com/kalilinux/packages/metasploit-framework/blob/07ca796c8f087fa0c77730b95edac2461ea6a7ab/spec/lib/rex/encoder/alpha2/unicode_mixed_spec.rb

在网上能找到这些shellcode的编码器,但是找不到解码器,所以照这编码器,我写了个逆

1
2
3
4
5
6
7
8
9
10
11
def shellcode_decoder(s):
ret = b''
for i in range(0, len(s), 2):
X, Y = s[i], s[i+1]
C, E = ((X & 0xf0) >> 4), ((Y & 0xf0) >> 4)
D, F = (X & 0xf), (Y & 0xf)
A = (D + E) & 0xf
B = F
ret += bytes([((A<<4) + B)])

return ret

但其实实际上,官方的做法是通过shellcodeRunner去进行调试,然后再dump出来(我太菜了)

当时没有直接调试成功,应该是设置的问题,以后试试用shellcodeRunner这个工具

shellcode解密出来,可以发现这是个下载器,从流量中找到下一段shellcode,

由于当中还包含了通过对api hash,然后比对调用特定的win api

根据hash找到了这么一个网站,直接找就完事了

https://hiddencodes.wordpress.com/2014/08/22/windows-api-hash-list-1/

然后很快,就找到flag了

h4ve_you_tri3d_turning_1t_0ff_and_0n_ag4in@flare-on.com


关于最后的hash,这其实是恶意代码中常用的伎俩

官方wp中提到有很多工具可以拿来搜

8-Aardvark

这题非常无语

直接打开发现报错了,逆进去发现,这似乎是要跟WSL交互

从pe的资源节释放出来一个elf,这个elf将运行在WSL里面,然后作为井字棋的AI

玩家需要赢了才能getflag

首先,这个AI写的很好,在井字棋这种情况下能保证输不了,玩家最多跟它打平

看了看getflag的逻辑,是从系统中取了一堆信息内容运算后得出的

居然是跟系统相关的?那么还是动态跑起来


这里,我就直接尝试在WSL中安装了gdb,然后attach到AI进程

跑到check的分支,直接修改了寄存器的值。。

然后发现就输出flag了,这里多得看了眼twitter,有人说,如果你得到一个有点乱的flag,不要怀疑,尝试提交一下

没想到就真的就是这个flag

`c1ArF/P2CjiDXQIZ@flare-on.com`

8-solve


这题出的确实一般,下面根据官方wp的内容补充一些相关的技术知识

  1. WSL与Windows的交互

    这里交互方式是通过Unix socket,socket(AF_UNIX, SOCK_STREAM, 0);,绑定的名称为496b9b4b.ed5

    在Win7的时候,似乎是不支持AF_UNIX的,而在Window 10 17063版本后,Windows支持AF_UNIX了

    https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/

    这让window支持通过Unix Socket跨进程通信

  2. WSL链接 CoCreateInstance

    通过指定 COM Class ID and Interface ID,与WSL链接,根据windows版本的不同,其接口函数也不太一样

    题目中适配了Windows 10 1803, 1809, 1903, 1909, 2004, or 20H2

  3. flag生成中使用到的环境信息

    因为flag是通过环境信息xor得来的,那怎么保证的flag唯一性?

    首先,题目中有对WSL版本检查,elf跑的环境只能是WSL1

    而当中用到的4种环境信息:

    1. /proc/modules WSL1中没有这个目录,跳过
    2. /proc/mounts 中根目录类型,WSL中是wslfslxfs,最后都是fs,用fs异或
    3. /proc/version_signature信息,WSL前面固定是Microsoft
    4. VDSO shared library 的program header的virtual address,四个固定的0xffffffffff70
    5. /proc下文件i-number,因为WSL下的i-number高16位肯定都是0

    所以,题目就是通过这种办法跑出flag(Q:这是否可以用来当作识别WSL环境的特征呢?)

9-crackinstaller

对于像我这样不熟悉windows的来说,这题很复杂

题目总共给的就一个文件 crackstaller.exe

这个文件的行为像一个dropper

他里面内嵌(embedded)了3个二进制文件

首先crackstaller其实是整个crackme的安装器

在mian之前,会释放出C:\Windows\System32\cfs.dll

以及加载DriverBootstrap,其具体表现还是一个PE文件,实际是一个.sys

在官方wp中他把这两个binary叫做 capcom.sysdriver.sys

官方wp说到,binary里面用到了chacha20解密,然后再进行lznt1解压缩

在实际逆向过程,调试直接跳过了这步

其在释放出capcom.sys后,会通过DeviceIoControl调用0xAA013044的处理

实际上capcom.sys的作用是disable SMEP(Supervisor Mode Execution Prevention)

并且同时,会在capcom上加载driver.sys

整个过程,实际上是关闭了SMEP保护,然后加载driver.sys到用户层,通过文件头找到DriverBootstrap调用

其实相当于运行了一个shellcode

而运行shellcode之前,crackstaller通过一系列操作吧driver.sys加载到内存上,为了保证其能正常运行,并且把导入函数通过参数的方式传入DriverBootstrap

而中间还有个非常鸡贼的操作

9-patch

把整个driver.sys搜索一遍,把0xDC16F3C3B57323 patch成 “BBACABA”

在逆driver.sys的时候能发现,这个其实是解密出password的密钥,所以也难怪我一开始直接逆怎么都解不出真实值

driver.sys通过CmRegisterCallbackEx((PEX_CALLBACK_FUNCTION)Function, &Altitude, a1, a1, &Cookie, 0i64);注册了个回调函数

这个函数,搜了一大堆文档,最后发现是

which is called every time a thread performs an operation on the registry.

里面用ZwCreateKey(&KeyHandle, KEY_ALL_ACCESS, &ObjectAttributes, 0, &Class, 0, Argument2->Disposition);

Class里面包含了解密出来的password H@n $h0t FiRst!

这一步ZwCreateKey其实我完全没理解是在干什么

官方wp说,他把password储存 in a registry class string (class type string?)

当使用RegCreateKeyEx创建key时,lpClass可以被忽略,被设置成NULL

lpClass

​ A pointer to a buffer that receives the user-defined class of the key. This parameter can be NULL.

官方wp写到因为这个参数从来不会被使用,所以数据可以储存在里面,并且通过regedit、reg.exe等工具都不会返回class strings

正确的提取方式是下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void PrintPassword()
{
HKEY hkey = NULL;
CHAR password[MAX_PATH] = {0};
DWORD buf_size = MAX_PATH;

if (ERROR_SUCCESS != (result = RegOpenKeyA(
HKEY_CLASSES_ROOT,
"CLSID\\{CEEACC6E-CCB2-4C4F-BCF6-D2176037A9A7}\\Config,
&hkey))) return;

if (ERROR_SUCCESS != (result = RegQueryInfoKeyA(
hkey,
password,
&buf_size,
NULL,
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL))) return;

if (0 == buf_size) return;
printf(“password: %s\n”, password);
return;
}

而剩下的部分,就是注册得到的COM server credHelper.dll

通过COM与该server交互,就能读到flag

当然,我当时并不懂COM,完全靠猜,把credHelper.dll翻了个底朝天找出来的= =

并且时通过静态解出来,这题如果能调到DriverBootstrap,能省很多事

而整个crackstaller的本质作用是安装了一个COM server,本身并不具备正常crackme拥有的输入

因此如果要正常交互还是需要用户写一个COM client去跟他交互,再从注册表读出flag


这题如果没搞懂的话强烈建议去看官方wp,包括里面描述到的COM细节

另外,在解密一些字符串的时候,我用到了flare团队中的一些ida插件,用来自动解密字符串

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
from __future__ import print_function
import flare_emu

# def decrypt(argv):
# myEH = flare_emu.EmuHelper()
# myEH.emulateRange(myEH.analysisHelper.getNameAddr("dec3_7FF6FDF81C34"), registers = {"arg1":argv[0], "arg2":argv[1], "arg3":argv[2]})
# return myEH.getEmuString(0x7FF6FDFB94E0).decode('utf-8')

def decrypt(data, l):
key = [0x3C, 0x67, 0x7E, 0x7B, 0x3C, 0x69, 0x74, 0x00]
ret = b''
for i in range(l):
ret += bytes([data[i] ^ key[i % 7]])
return ret.decode('utf-8')



def iterateCallback(eh, address, argv, userData):
s = decrypt(eh.getEmuBytes(argv[0], argv[1]), argv[1])
# s = decrypt(argv)
print("%s: %s" % (eh.hexString(address), s))
eh.analysisHelper.setComment(address, s, False)

if __name__ == '__main__':
eh = flare_emu.EmuHelper()
# eh.iterate(eh.analysisHelper.getNameAddr("dec2struct_140004D60"), iterateCallback)
eh.iterate(eh.analysisHelper.getNameAddr("dec4_7FF6F9C11CA8"), iterateCallback)

10-break

终于迎来了一道ELF的题目,原本以为这会比较常规一些,没想到,作者这操作也太魔鬼了

首先,粗略的看一下main函数,会发现只是一个非常简单的strcmp

借用官方wp的图,输入这个字符串会提示错误

10-trick

那当然不可能这么简单啦

那么看一下.init,会发现两个函数

一个叫parent_sub 另一个叫first_fork

而在里面还会出现另一个second_chlid,通过fork创建了两个子进程,总共算起来是3个进程

在运行起来后通过ps就能看出来

1
2
3
root       154  0.0  0.0   3644   540 pts/2    t+   10:27   0:00 ./break
root 155 0.0 0.0 3780 64 pts/2 S+ 10:27 0:00 ./break
root 156 0.0 0.0 3780 80 pts/2 S+ 10:27 0:00 ./break

首先,我们区分三个进程为parent、first_child、second_child

在这三个进程中

first_child —–(ptrace attach)——-> parent

second_child –(ptrace attach)——> first_child

接下来说一下每个ptrace都在做什么

first_child: 通过ptrace_me(PTRACE_SYSEMU, parent_pid, 0, 0)捕获parent进程的syscall

并根据不同的syscall进行不同的运算

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
v25 = 0x1337CAFE * (v15.orig_eax ^ 0xDEADBEEF);
...
switch ( v25 )
{
case 0x4A51739A: // eax = 0x5c truncate
readdata_fromaddr_804BBF8(parent_pid, v15.ebx, (int *)&outbuf, 40000);
for ( i = 0; i <= 39999 && *((_BYTE *)&outbuf + i); ++i )
{
v14[i] = *((_BYTE *)&outbuf + i);
if ( v32 == -1 && v14[i] != byte_81A5100[i] )
v32 = i; // memcmp
}
v32 = v30(0xA4F57126, userinput_81A56C0, v32);
v15.eax = v32;
ptrace_me(PTRACE_SETREGS, parent_pid, 0, (int)&v15);
break;
case 0x7E85DB2A: // eax = 0x4 write
size = v15.edx;
buf = malloc(v15.edx); // emu puts()
readdata_fromaddr_804BBF8(parent_pid, v15.ecx, (int *)buf, size);
write(STDOUT_FILENO, buf, size);
v15.eax = size;
ptrace_me(PTRACE_SETREGS, parent_pid, 0, (int)&v15);
free(buf);
break;
case 0x3DFC1166: // 0x22 nice
buf = xor_withbuf_idx_8056281(v15.ebx);// why? It call dec_idx in true.
v5 = strlen((const char *)buf);
writedata_toaddr_804BB2D(parent_pid, (int)tmpstring_81A52A0, (int *)buf, v5 + 1);
free(buf); // return some string to tmpstring
v15.eax = 0;
ptrace_me(13, parent_pid, 0, (int)&v15);
break;
}

second_child: 通过PTRACE_PEEKDATAPTRACE_GETREGS等捕获SIGSEGV,并根据栈上的数据进行运算

并根据arg1的值选择不同的计算,返回值放到eax上,这有点类似一个VM fetch instruction然后解析执行的过程

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
if ( data == SIGSEGV )                  // 0xB7F
{
ptrace_me(PTRACE_GETREGS, child1_pid, 0, (int)&reg_15);
v9 = ptrace_me(PTRACE_PEEKDATA, child1_pid, reg_15.esp, 0);
arg1 = ptrace_me(PTRACE_PEEKDATA, child1_pid, reg_15.esp + 4, 0);
arg2 = ptrace_me(PTRACE_PEEKDATA, child1_pid, reg_15.esp + 8, 0);// point at [Memory 0] function call in child1
arg3 = ptrace_me(PTRACE_PEEKDATA, child1_pid, reg_15.esp + 12, 0);
...

...
switch ( arg1 )
{
case 0xA4F57126:
reg_15.eax = arg3;
if ( arg3 != -1 )
{
readdata_fromaddr_804BBF8(child1_pid, arg2, (int *)userinput_81A56C0, 62);
if ( strncmp(&userinput_81A56C0[48], "@no-flare.com", 0xDu) )
reg_15.eax = -1;
}
break;
case 0xB82D3C24:
reg_15.eax = arg2 + 1;
break;
case 0x91BDA628:
reg_15.eax = (16 * (arg2 - 1)) | ((_BYTE)arg3 - 1) & 0xF;
break;
}
...

因为second_child捕获的是first_child中的SIGSEGV,所以正常情况下second_child都是处于Sleep状态

而first_child捕获的是parent中的系统调用,在没有系统调用的情况下也是处于Sleep状态

这个从开始通过ps查看的进程信息就能看出

S interruptible sleep (waiting for an event to complete)

在first_child开始时,会通过ptrace_me(PTRACE_POKEDATA, parent_pid, (int)sub_8048CDB, 0xB0F)

把sub_8048CDB开头字节patch掉,而这个函数正是main函数进去调用的strcmp的函数

1
2
3
4
_BOOL4 __cdecl sub_8048CDB(char *s1)
{
return strcmp(s1, "sunsh1n3_4nd_r41nb0ws@flare-on.com") == 0;
}

当开头被patch掉后,main函数执行到这里的时候,会产生Illegal Instruction错误,而这个错误正好被first_child捕获

1
2
3
4
5
6
7
8
9
10
11
if ( (stat_loc[0] & 0xFF00) >> 8 == SIGILL )// Illegal Instruction
{
v7 = strlen(userinput_81A56C0);
writedata_toaddr_804BB2D(parent_pid, (int)userinput_81A56C0, (int *)userinput_81A56C0, v7);
ptrace_me(PTRACE_GETREGS, parent_pid, 0, (int)&v15);
v21 = v15.esp;
if ( ptrace_me(PTRACE_POKEDATA, parent_pid, v15.esp + 4, (int)userinput_81A56C0) == -1 )
exit(0);
v15.eip = (int)rm_rf;
ptrace_me(PTRACE_SETREGS, parent_pid, 0, (int)&v15);
}

而通过这个处理,把parent的执行流控制到函数rm_rf

这个函数非常鸡贼

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
_BOOL4 __cdecl rm_rf(char *userinput)
{
_BOOL4 result; // eax
char v2[176]; // [esp+4h] [ebp-D4h] BYREF
char *argv[5]; // [esp+B4h] [ebp-24h] BYREF
int v4; // [esp+C8h] [ebp-10h]
size_t v5; // [esp+CCh] [ebp-Ch]

v5 = strlen(userinput);
argv[0] = "rm";
argv[1] = "-rf";
argv[2] = "--no-preserve-root";
argv[3] = "/";
argv[4] = 0;
execve(userinput, argv, 0);
--v5;
v4 = -nice(165);
aes_key_expension_804B495((int)v2, v4);
aes_decrypt_804BABC((int)v2, (int)&unk_81A50EC);
aes_decrypt_804BABC((int)v2, (int)&unk_81A50F0);
aes_decrypt_804BABC((int)v2, (int)&unk_81A50F4);
aes_decrypt_804BABC((int)v2, (int)&unk_81A50F8);
if ( !memcmp(userinput, &unk_81A50EC, 0x10u) )// w3lc0mE_t0_Th3_l
{
memset(&unk_81A50EC, 0, 0x10u);
result = sub_8048F05(userinput + 16);
}
else
{
memset(&unk_81A50EC, 0, 0x10u);
result = 0;
}
return result;
}

乍一看,他还会先执行rm -rf --no-preserve-root /,算是个恶趣味了

但是实际上,execve这个系统调用会被first_child捕获,进行的实际是别的操作

如果哪个人把first_child这个进程kill掉了,那么,恭喜~获得 rm -rf /大礼包

在看到下面的memcmp,其实就能想到我直接调试到这然后直接读内存不就好了

但是,这整个程序是通过ptrace驱动的

而linux上的调试器也是通过ptrace实现的,一个经典的反调试是,当一个进程已经被ptrace,就不能再被另一个进程ptrace

所以有反调试会通过ptrace自己来反调试,但是这种反调试很好过,只要把ptrace自己的代码patch掉就好了

但是这题不行,因为整个执行流都需要靠ptace去驱动,这没办法去掉。

开始我是在想通过虚拟机之类的方法捕获执行流的,但是发现qemu对其中一些系统调用并没有模拟好,包括ptrace的一些功能

导致整个题目没法正常跑通。后来也想过使用frida等工具进行插桩,但是实际上frida也是通过ptrace再插桩的

也想过用pintool等一些工具去尝试,当时忘了什么原因,也报了一些错误。

最后其实是通过libc hook来输出一些中间值的。

简单来说就是通过LD_PRELOAD=$(pwd)/libchook.so ./10_-_break/break

对libc进行hook

stage1

通过这个方式直接输出了第一部分的flag

w3lc0mE_t0_Th3_l

官方wp提到了另外一种方法,就是通过linux的mem接口

10-firststage

stage2

然后stage2,stage2是对代码段一大块区域进行解密的操作。然后这一大块的前32字节就是stage2的flag

这个函数里面会发现这样的调用

0804C40C: ((void (__cdecl *)(void *, int *))MEMORY[0])(&loc_804C3C4, &v5);

这个调用乍一看毫无道理,函数地址为0,但是这样的代码会引起segmentfault

SIGSEGV会被ptrace捕获,下面是first_child中捕获SIGSEGV的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if ( (stat_loc[0] & 0xFF00) >> 8 == SIGSEGV )
{
ptrace_me(PTRACE_GETREGS, parent_pid, 0, (int)&v15);
v20 = ptrace_me(PTRACE_PEEKDATA, parent_pid, v15.esp, 0);
v19 = ptrace_me(PTRACE_PEEKDATA, parent_pid, v15.esp + 4, 0);
addr = ptrace_me(PTRACE_PEEKDATA, parent_pid, v15.esp + 8, 0);
data = ptrace_me(PTRACE_PEEKDATA, parent_pid, addr, 0) + 1;
v15.esp += 4;
if ( data > 15 )
{ // loop 16
v15.eip = v20;
}
else
{
v15.eip = v19;
ptrace_me(PTRACE_POKEDATA, parent_pid, addr, data);
v15.esp += 16;
}
ptrace_me(PTRACE_SETREGS, parent_pid, 0, (int)&v15);
}

实际上,这就是一个从当前地址到第一个参数0x804C3C4的循环,循环次数为16

而这个函数的运算则是使用了许多奇怪的系统调用,而实际上这些系统调用都被替换成first_child中的操作

而当时我好像也没太识别出这个这是什么算法,就直接抄写并写了个逆,中间还因为一些抄写错误卡了一段时间= =

最后还是通过libc hook进行验证才写对的

Flag2: 4nD_0f_De4th_4nd_d3strUct1oN_4nd

也贴一下代码吧,官方wp提到,这个一个Custom ARX Feistel Cipher

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
#!/usr/bin/python3


enc_flag = [0x64, 0xA0, 0x60, 0x02, 0xEA, 0x8A, 0x87, 0x7D, 0x6C, 0xE9, 0x7C, 0xE4, 0x82, 0x3F, 0x2D, 0x0C, 0x8C, 0xB7, 0xB5, 0xEB, 0xCF, 0x35, 0x4F, 0x42, 0x4F, 0xAD, 0x2B, 0x49, 0x20, 0x28, 0x7C, 0xE0]

constant_s = b'This string has no purpose and is merely here to waste your time.'

# constant_s = b'Wasting your time6'

def PAIR64(a, b):
return (a << 32) + b

def init(a1, s, l):
global data
ret = a1
for i in range(l):
v3 = (s[i] ^ ret) & 0xff
low = ((ret >> 8) ^ data[2*v3]) & 0xffffffff
hi = ((((ret >> 32) & 0xffffffff) >> 8) ^ data[2*v3+1]) & 0xffffffff
ret = PAIR64(hi, low)
return ret


def count1(n):
ret = 0
while n != 0:
if (n & 1) != 0:
ret += 1
n >>= 1
return ret

def buf_round(a, buf):
flags = a

buf[7] = flags & 0xffffffff
buf[19] = (flags >> 32) & 0xffffffff
buf[41] = (count1(flags) >> 1)
v6 = flags & 1
flags >>= 1
if v6 == 1:
v8 = 0x9E3779B9C6EF3720
flags ^= v8
return flags, buf

def ror(a1, a2):
return ((a1 >> (a2 & 0x1F)) | (a1 << (-(a2 & 0x1F) & 0x1F))) & 0xffffffff

def catch_chmod(buf, hi):
v2 = (hi + buf[7]) & 0xffffffff
v3 = ror(v2, buf[41])
return v3 ^ buf[19]



def enc_once(out, num, buf):
v7 = num

for i in range(16):
out, buf = buf_round(out, buf)
hi = (v7 >> 32) & 0xffffffff
# print("hi: ", hex(hi))
v3 = catch_chmod(buf, hi)
low = (v7 ^ v3) & 0xffffffff
v7 = PAIR64(low, hi)
num = PAIR64(v7 & 0xffffffff, (v7 >> 32) & 0xffffffff)
return num

def enc(num_buf):
global constant_s
out = init(0, constant_s, len(constant_s)) # 0x7d08ff3b28b975ec
# memcpy(&num_buf, flagfrom16, 0x20u);
buf = [0] * 496
ret_arr = []
for i in range(len(num_buf)):
num = enc_once(out, num_buf[i], buf)
ret_arr.append(num)
return ret_arr

def dec_once(out, num, buf):
buf_array = []
for i in range(16):
out, tmpbuf = buf_round(out, buf)
buf_array.append(list(tmpbuf))
buf = tmpbuf
buf_array = buf_array[::-1]

v7 = PAIR64(num & 0xffffffff, (num >> 32) & 0xffffffff)
for i in range(16):
buf = buf_array[i]
low = v7 & 0xffffffff
v3 = catch_chmod(buf, low)
hi = (v7 >> 32) & 0xffffffff
hi ^= v3
v7 = PAIR64(low, hi)
return v7


def dec(num_buf):
global constant_s
out = init(0, constant_s, len(constant_s)) # 0x674a1dea4b695809
buf = [0] * 496
ret = []
for i in range(len(num_buf)):
ret_num = dec_once(out, num_buf[i], buf)
ret.append(ret_num)
return ret

import struct
def main():
num_buf = struct.unpack("<QQQQ", bytearray(enc_flag))
out_buf = dec(num_buf)
flag = struct.pack("<QQQQ", *out_buf)
print(flag)

main()

stage3

同样的代码,把后续代码段的代码也解密出来了

其实当时这个后续代码段我没看出来是怎么跳转过去的,然后没搞懂就直接顺着代码继续逆了

现在看官方wp

说的是下面代码的栈溢出,原本v30是0,会导致SIGSEGV被second_child捕获,从而调用到second_child中的cmp

但实际上,这个栈溢出会一直到很后边才会停止,这个取决于outbuf[i]是否为0,而数据中第一个出现0的地方前面存在一个疑似地址的值

而这个值刚好溢出到把v30给覆盖了,而覆盖成0x08053b70,恰好为解密后的代码的入口点Orz

这题目的操作真的太骚了

10-stackoverflow

而当时这个函数,有些函数逆着发现,存在类似进位的操作,猜测这是一个大整数库,照着这个思路很快就把他算术逻辑逆出来了

10-hint

甚至当中胡乱解密解出来了一张图片的提示,让我发现其中一个函数其实是除法23333

最后逆出来的算术如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const1 = 0xd1cc3447d5a9e1e6adae92faaea8770db1fab16b1568ea13c3715f2aeba9d84f
const2 = 0xc10357c7a53fa2f1ef4a5bf03a2d156039e7a57143000c8d8f45985aea41dd31
const3 = 0x480022d87d1823880d9e4ef56090b54001d343720dd77cbc5bc5692be948236c
const4 = 0x480022d87d1823880d9e4ef56090b54001d343720dd77cbc5bc5692be948236c
const5 = 0xd036c5d4e7eda23afceffbad4e087a48762840ebb18e3d51e4146f48c04697eb

# v9 = rand % const1
# v11 = pow(const2, v9, const1)
# v13 = pow(const4, v9, const1)
# const5 == flag * v11 % const1
# const3 == v13

## 然后使用sage去解了
v9 = discrete_log(const3,mod(const4,const1)) # 因为const3和const4是一样的,这个是存在bug的
print(v9)
# 1

v11 = const2 % const1
flag = hex(const5 * v11.inverse_mod(const1) % const1)
bytearray.fromhex(flag[2:])[::-1]
# bytearray(b'_n0_puppi3s@flare-on.com')

获得最终的flag

`w3lc0mE_t0_Th3_l4nD_0f_De4th_4nd_d3strUct1oN_4nd_n0_puppi3s@flare-on.com `

11-rabbithole

这题也是够狠

flareon官方直接把一个现成的病毒进行去毒处理,然后做成一道题给我们

首先把我自己的做题过程记录下来吧,整个过程花了很长时间,重复造了一些轮子。


首先题目拿到手的是一个.dat的文件

file一下

NTUSER.DAT: MS Windows registry file, NT/2000 or above

数据库文件,对windows不是很熟悉,于是网上瞎找这文件的打开方式,后来发现其实是可以直接从regedit中打开的

这其实是user registry hive,这个文件通常储存在%USERPROFILE%中,在win10下就是C:/Users/<username>里面

参考Registry Hives - Win32 apps | Microsoft Docs

这个似乎就是每个用户自己的注册表数据

然后我便开始瞎翻注册表,但是什么都没发现,里面的内容实在太多了

然后就转想,开始网上搜索关于registry hive取证的内容

很巧,最后被我找到了fireeye自己的一篇文章

SAIGON, the Mysterious Ursnif Fork | FireEye Inc

这个时候看twitter上的讨论,一堆大佬在劝:善用搜索引擎,能为你省很多时间。

看着很像呀,我还以为他们说的就是这个文章,于是我开始翻注册表,尝试找到类似开机启动的东西

但是也还是什么都没发现,最后引起我的注意是Software/Timerpro

这名字看着像个定时器软件呀(事实证明,too naive,并且这真是巧合了)

注册表这项里面,第一项就是一个powershell脚本

开逆!

这个脚本在做一些类似dll注入的操作,而dll则是从base64解码出来

开始逆dll,发现。。真的复杂,有许多操作都不知道在干什么,而且,他也不是一个标准的dll格式

整个文件格式很奇怪,但是按照正常的文件头偏移计算,又能索引到关键的地址

用了上面文章中附录的一个脚本,转换成了PE文件 Shellcode Converter Script

代码很长一段,就不贴上来了,查看源码到SAIGON, the Mysterious Ursnif Fork | FireEye Inc

整个dll也带一些混淆,像通过文件头的索引找到某个section,对section进行解密操作

这个解密操作能够解密出来使用到的一些字符串内容。而xor解密的密钥则是跟文件的timestamp相关

前面一系列解析文件头也包含了从文件头中找到这个timestamp

然后,上面提到的那片文章给了我很大的帮助,他在文件末尾有嵌入了一些文件

这个dll中,总共有两个embedded file

并且,当中有用到的解压算法 aplib,文章中也有提到,这让我省了很多算法识别的时间

两个embbedded file中,第一个是一个RSA public key,第二个是一堆字符串

并且,通过XorShiftRng随机数算法,用sid作为种子的一部分,利用embbeded file中的字符串作为字符串表

生成特定的字符串,用这个作为注册表中的key值,所以其实之前看到的Timerpro是随机数生成的字段,然后被我误解成一个软件了。。。

当时还搜了好久这个软件,结果啥都没搜到

而这个sid,则是从注册表中查找S-1-5-21-3823548243-3100178540-2044283163-1006

而RSA public key,则是用作其中一部分数据的解密,作为密钥使用serpent算法进行解密

关于这个加密算法,我当时没有找到有现成的库,是找到了一个serpent的python代码

而一开始没看仔细,直接使用ECB模式解密,发现失败,后来突然想到,会不会是CBC模式,然后手动改成了CBC模式

初始化IV用0,解出

而在Timerpro底下,包含了ColumncurrentLanguagetheme两个项,里面存着都是加密压缩过的PX文件,一个目录存32位,一个目录存64位,通过上面提到的方式解密出来

什么是PX文件?其实是有点像一开始dump出来的dll,都不是一个正规的PE文件,但是通过病毒的加载器,能成功跑起来

这个时候,我需要找到解析这些文件片段的方法

网上搜了一圈

https://research.checkpoint.com/2020/gozi-the-malware-with-a-thousand-faces/

https://github.com/0ver-fl0w/ISFB_Tools

https://github.com/hasherezade/funky_malware_formats

好家伙,才发现我网上搜了一堆的算法,拼凑起来的脚本,人家早就整理好了= =,毕竟这是个现成的病毒样本

浪费了贼多时间= =

做到这我才知道他们说的善用搜索引擎的意思……

在解出来后,主要关注几个

这些文件跟开始的dll很类似,都用着相同的字符串加密方式,以及相同的embedded file嵌入方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
RowmapGuiprotocol:     (run time library)(rtl.dll)
这个就复杂很多了,看了下没看到什么特别的,有文件操作、Event操作
还有ShellExecuteW
传进去的是不知名数据,暂时看不出什么,待继续看
big switchcase here


WebmodeThemearchive:(45a0fcd0.dll) (netwrk.dll)
包含WinHttp的操作


WebsoftwareProcesstemplate:(8576b0d0.dll 最开始的dll)
跟原本的exe很多相似的函数片段,包含了PRnd、还有aplib等等
似乎就是一模一样的= =


WordlibSystemser:(d6306e08.dll) (explorer.dll)
很多字符操作类似的东西,还有注册表设置等

其实这个病毒主要是怎么驱动起来的我也说不清楚,整个过程分析得我很混乱,并且由于我刚开始分析的是64位文件夹下的,没有关注32位文件夹下的,以为他们完全一样

但是32位文件夹下还包含了一个配置文件,这个配置文件写明了最后储存到注册表的flag的加密密钥

一直到最后卡住,找twitter的大佬询问才解决。

然后,通过serpent解出储存在注册表中的flag

`r4d1x_m4l0rum_357_cup1d1745@flare-on.com`


由于这个病毒是从真实病毒修改而来的,而很多大佬本身就是做病毒研究的工作,因此,这题对他们来说做起来应该非常快

而很多人都有提到,善用搜索引擎,因为确实是非常多人已经把里面算法、脚本打包好了

这里再说一下开始时应该怎么分析。

开始我是靠猜找到Timerpro这个关键的注册表项

而实际上,有更加靠谱的办法。。

微软有个工具叫autorun

Autoruns for Windows - Windows Sysinternals | Microsoft Docs

这个工具可以分析windows中所有自动启动项,包括注册表、定时任务等等

借用官方wp的图,通过这个工具,能发现

11-autorun

一个自动运行的脚本,而那串base64解码出来,正是Timerpro注册表项

iex (gp 'HKCU:\SOFTWARE\Timerpro').D

写在最后

最后这题,大佬们对这个病毒做了去毒操作,,真的太强了Orz

并且patch了所有密钥相关的数据,避免病毒与原server交互

整个过程其实我写得很流水账,因为感觉这题没有什么特别的技巧可言,更多是在逆向过程学到一个又一个的骚操作

病毒分析真是有意思

这片流水账wp也从2020年10月拖到2021年4月,emmmm半年了,因为之前忙毕设忙得焦头烂额

不知道21年还有没有空继续打flareon了:)

Respect!

×

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

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

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

文章目录
  1. 1. 1_-_fidler
  2. 2. 2–garbage.exe
  3. 3. 3-Wednesday
  4. 4. 4-report.xls
  5. 5. 5-TKApp
  6. 6. 6-codeit
  7. 7. 7-re_crowd
  8. 8. 8-Aardvark
  9. 9. 9-crackinstaller
  10. 10. 10-break
    1. 10.1. stage1
    2. 10.2. stage2
    3. 10.3. stage3
  11. 11. 11-rabbithole
  12. 12. 写在最后
,