HITCON 2022 Reverse Writeup

Preface

这次比赛是我加入 AAA 之后第一次与 Katzebin 联队打国际赛,最终获得了 rank 7.

今年 HITCON 从北京时间 11 月 25 日晚 10 点开始到 11 月 27 日晚 10 点结束,比赛过程中我主要负责逆向部分的题目。逆向部分在 10 点开赛后首先放出的是 Checker 这道题目,在我分析完 sys 文件中 SMC 的 trick 并把思路放在队伍共享文档之后,Shino 师傅很快便写出了解题脚本,先我一步解出了题目;第二天凌晨 4 点放出了剩下的两道逆向,我是早上看到的题目,由于 Meow Way 比较简单,很快我便解出了;gocrygo 这道题目是针对勒索病毒的分析,关键在于加密算法的识别与 core dump 中 key 的提取,在与 Shino 师傅的合作下,我们很早便拿到了 Tripple DES 加密用到的 24 bytes 密钥,但很长时间都没有明确加密使用的模式,之后又做了许多错误的尝试 … 不过幸运的是在当天晚上,tkmk 师傅查看了这道题目的共享文档,一针见血的指出加密模式为 CTR,于是题目便迎刃而解了。

Checker [198pts]

problem description

just a deep and normal checker

analysis

附件给出了 checker.exechecker_drv.sys 两个文件,其中前者通过 [DeviceIoControl](https://learn.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-deviceiocontrol) 函数向后者对应的驱动程序发送 control code 调用其 check 逻辑,对应驱动程序的设备名为 hitcon_checker

于是主要的逆向工作转向了 sys 即驱动文件,通过 DriverEntry 很容易定位到 sub_140001B50 函数

sub_140001B50.png

变量 obj 为对应的驱动对象 (Driver Object),简单了解后可知 sub_1400011B0 即为其驱动消息处理函数,图中下方容易识别出驱动程序使用了 SMC 这一 trick 来干扰静态分析

进一步分析 sub_1400011B0 函数,该 dispatcher 使用 swich-case 实现了对不同驱动消息的处理,8 个 case 中 7 个调用了相同的函数 sub_1400014D0 ,case 0x222080 对应 exe 中通过 DeviceIoControl 发送的 control code,也就是最终的 check

分析 sub_1400014D0 可知,该函数通过传入的 offset 将对应 data 段字节序列 offset 处的数据用于地址 0x140001B30 的 SMC,在相应修改完成后,调用 0x140001B30 处函数对 flag 对应数据逐字节运算。因此,猜测只需知道上文提到的 7 个 case 的前后调用顺序即可计算得到 flag 满足 check 的条件

起初的想法是爆破这一调用顺序,但随后考虑到每次经过 sub_1400014D0 修改后的相应代码理应是 reasonable 的,即所谓“正常”的字节运算函数,于是结合自动化 patch + 人工核查容易恢复 7 个 case 正确的调用顺序,并根据恢复出的运算逻辑得到 flag

recover script

from ida_bytes import get_byte, patch_byte

code_addr = 0x140001b30
init_addr = 0x140001430
data_addr = 0x140003030

def do_init():
    global code_addr
    global init_addr
    for i in range(32):
        code_byte = get_byte(code_addr + i % 16)
        init_byte = get_byte(init_addr + i)
        patch_byte(code_addr + i % 16, code_byte ^ init_byte)

def do_step(offset, sure:bool):
    global code_addr
    global init_addr
    for i in range(16):
        code_byte = get_byte(code_addr + i)
        data_byte = get_byte(data_addr + i + offset)
        patch_byte(code_addr + i, code_byte ^ data_byte)
    if sure:
        for i in range(16):
            code_byte = get_byte(code_addr + i)
            data_byte = get_byte(data_addr + i + offset + 16)
            patch_byte(code_addr + i, code_byte ^ data_byte)

do_init()
do_step(224, True)
'''
__int64 __fastcall sub_140001B30(unsigned __int8 a1)
{
  __int64 result; // rax

  result = a1 >> 5;
  LOBYTE(result) = (8 * a1) | (a1 >> 5);
  return result;
}
'''
do_step(64, True)
'''
__int64 __fastcall sub_140001B30(unsigned __int8 a1)
{
  return a1 ^ 0x26u;
}
'''
do_step(192, True)
'''
__int64 __fastcall sub_140001B30(unsigned __int8 a1)
{
  __int64 result; // rax

  result = a1 >> 4;
  LOBYTE(result) = (16 * a1) | (a1 >> 4);
  return result;
}
'''
do_step(0, True)
'''
lea     eax, [rcx+37h]
retn
'''
do_step(32, True)
'''
lea     eax, [rcx+7Bh]
retn
'''
do_step(128, True)
'''
__int64 __fastcall sub_140001B30(unsigned __int8 a1)
{
  __int64 result; // rax

  result = a1 >> 1;
  LOBYTE(result) = (a1 << 7) | (a1 >> 1);
  return result;
}
'''
do_step(96, True)
'''
__int64 __fastcall sub_140001B30(unsigned __int8 a1)
{
  return 0xad * (unsigned int)a1;
}
'''
do_step(160, False)
'''
__int64 __fastcall sub_140001B30(unsigned __int8 a1)
{
  __int64 result; // rax

  result = a1 >> 6;
  LOBYTE(result) = (4 * a1) | (a1 >> 6);
  return result;
}
'''

solver script

from ida_bytes import get_byte

enc = []
flag_addr = 0x140003000
flag_len = 43

def encrypt(case):
    global enc
    if case == 0:
        for i in range(len(enc)):
            enc[i] = (8 * enc[i]) | (enc[i] >> 5)
            enc[i] &= 0xFF
    elif case == 1:
        for i in range(len(enc)):
            enc[i] ^= 0x26
    elif case == 2:
        for i in range(len(enc)):
            enc[i] = (16 * enc[i]) | (enc[i] >> 4)
            enc[i] &= 0xFF
    elif case == 3:
        for i in range(len(enc)):
            enc[i] += 0x37
            enc[i] &= 0xFF
    elif case == 4:
        for i in range(len(enc)):
            enc[i] += 0x7B
            enc[i] &= 0xFF
    elif case == 5:
        for i in range(len(enc)):
            enc[i] = (enc[i] << 7) | (enc[i] >> 1)
            enc[i] &= 0xFF
    elif case == 6:
        for i in range(len(enc)):
            enc[i] *= 0xAD
            enc[i] &= 0xFF
    elif case == 7:
        for i in range(len(enc)):
            enc[i] = (enc[i] << 2) | (enc[i] >> 6)
            enc[i] &= 0xFF

for i in range(flag_len):
    enc.append(get_byte(flag_addr + i))

for case in range(8):
    encrypt(case)

print(''.join(map(chr, enc)))
# hitcon{r3ally_re4lly_rea11y_normal_checker}

Meow Way [193pts]

problem description

Reverse-engineering like the meow way!

analysis

这个题目主要考察了 windows 程序 x86 与 x64 之间的切换,相关汇编指令如下:

// convert x86 to x64
push 0x33
call $+5
add dword [esp], 5
retf
// convert x64 to x86
call $+5
mov dword [rsp + 4], 0x23
add dword [rsp], 0xD
retf

核心是利用 retf 对 CS 寄存器进行修改

此外,题目中 x86 程序对 x64 函数的调用中参数传递还使用到了 cdq 指令,可以理解为该指令实现了 32 位字长的扩展,从而满足调用 64 位函数时对参数字长的要求

算法的逆向部分比较简单,这里不再赘述

solver script

from ida_bytes import get_bytes

add_data = [196, 22, 142, 119, 5, 185, 13, 107, 36, 85, 18, 53, 118, 231, 251, 160, 218, 52, 132, 180, 200, 155, 239, 180, 185, 10, 87, 92, 254, 197, 106, 115, 73, 189, 17, 214, 143, 107, 10, 151, 171, 78, 237, 254, 151, 249, 152, 101]

xor_data = [0xba, 0x2f, 0xcd, 0xf6, 0x9f, 0xd0, 0x22, 0xf7, 0xd0, 0x1f, 0xa8, 0x3d, 0xc7, 0xa5, 0x47, 0x68, 0xd7, 0x4a, 0x96, 0x91, 0x2e, 0x19, 0xc5, 0xe3, 0x88, 0xbd, 0x4e, 0x93, 0x13, 0xf1, 0xcc, 0x47, 0xab, 0xc9, 0x48, 0x2b, 0x09, 0x50, 0x4f, 0xe9, 0xc0, 0x5e, 0xef, 0x8b, 0x85, 0xcb, 0x55, 0x70]

enc = get_bytes(0x405018, 48)

flag = ''
for i in range(48):
    temp = (enc[i] ^ xor_data[i]) - add_data[i]
    if temp & 0xFF > 128:
        temp = add_data[i] - (enc[i] ^ xor_data[i]) 
    flag += chr(temp & 0xFF)

print(flag)
# hitcon{___7U5T_4_S1mpIE_xB6_M@G1C_4_mE0w_W@y___}

gocrygo [248pts]

problem description

There are three files in this challenge:

  1. gocrygo: A crypto-ransomware.
  2. gocrygo_victim_directory: The directory fucked up by this ransomware.
  3. core: The ransomware’s core dump when infecting gocrygo_victim_directory.

Unfortunately, no decryption service is available :(

Your goal is to:

  1. Reverse gocrygo
  2. Figure out the encryption algorithm it uses
  3. Find the encryption key in the core dump
  4. Decrypt the entire infected directory.
  5. Find the flag in gocrygo_victim_directory.

Note: The encryption algorithm is a common cryptographic algorithm (I’m too dumb to implement one myself). In other words, please don’t waste your time figuring out the details of the algorithm. Once you know which algorithm it uses, you can move on to the next phase and try to excavate the encryption key from the core.

analysis

题目给出了勒索病毒对应的 exe、运行到某时刻的 core dump 与被勒索病毒加密的一个文件夹,我们需要找出文件夹中文件被加密的方式并恢复原始文件

关键在于通过分析 exe 得知对应的加密算法,随后利用 core dump 提取内存中的 key

根据题目名猜测是用 go 编写的,但尝试用工具恢复符号表未果。使用 IDA 分析 exe 并查看字符串,可以看到 crypto/des 相关字符串,推测是使用了 go 中相应模块进行加密操作

在分析过程中,我找到的突破口是 0x2074D3 处对应的字符串,借助 CyberChef 可以识别出来该字符串是 base64 + base85 加密后的结果,交叉引用可以最终定位到 sub_221A61 函数,该函数为遍历文件夹下文件并进行加密的核心函数

分析该函数内部调用到的其他函数,可知:

  • sub_2214EB(char* ptr, uint length): base85 + base64 解密 ptr 处长度 length 的字符串
  • sub_21406C(char* src, uint src_length, char* dst, uint dst_length): src 与 dst 的 strncmp
  • sub_212FDD(…): go 中 runtime 的 new 操作
  • sub_21332F(char* err, uint length): panic 并输出 err 处 length 长度的字符串作为错误提示
  • sub_220965(…): 遍历文件夹并匹配对应文件或文件夹
  • sub_223E5A(…): 调用加密函数,对文件进行加密
__int64 __fastcall sub_223E5A(__int64 a1)
{
  return sub_222A30(*a1, *(a1 + 8), *(a1 + 16), *(a1 + 24));
}

gdb 调试并在该函数设下断点,该函数参数对应的类型是一个结构体,其中 key 在 *(*(a1 + 24) + 8) 处,对应长度 24 bytes,基本可以确定使用的加密算法是 Tripple DES

结合对 sub_222A30 函数的静态分析,并对照 https://github.com/golang/go/blob/master/src/crypto/des/cipher.go 中 3DES 初始化的实现,可在下图位置得知加密使用的模式为 3DES-CTR

sub_222A30

在提取密钥的过程中,最初的思路是通过 gdb 的 core 命令加载 core dump,然后查看调用栈定位 dump point,之后另起 gdb 在相同位置下断点正常调试程序,命中断点后查看此时 key 在后者栈上的位置,通过偏移计算前者 key 在栈上的位置。实际操作过程中,由于栈上存有环境变量等其他信息,导致这个偏移并非准确,也就是正常调试的环境与 core dump 中的环境并不一致

幸运的是,调试过程中留意到,key 所在地址的后方留有 /dev/urandom 字符串,将该字符串作为标志在 gdb 中使用 search 命令检索,最终顺利找到 key 所在地址

pwndbg> x/8gx 0x7fecc3580040
0x7fecc3580040:	0xbd349a8f52ae89b3	0x1b8566979b593598
0x7fecc3580050:	0x18a320b78025b482	0x00706f746b736544
0x7fecc3580060:	0x6172752f7665642f	0x000000006d6f646e
0x7fecc3580070:	0x0000000000000000	0x0000000000000000

至此,我们找到了 3DES-CTR 加密所用到的 key,iv 位于被加密文件的前 8 字节,得解

solver script

package main

import (
	"os"
	"fmt"
	"log"
	"bytes"
	"io/ioutil"
	"encoding/hex"
	"crypto/des"
	"crypto/cipher"
)

func main() {
	txtFile, err := os.Open("gocrygo_victim_directory/Desktop/flаg.txt.qq")
	if err != nil {
		log.Fatal(err)
	}
	defer txtFile.Close()

	picFile, err := os.Open("gocrygo_victim_directory/Pictures/rickroll.jpg.qq")
	if err != nil {
		log.Fatal(err)
	}
	defer picFile.Close()

	txtData, err := ioutil.ReadAll(txtFile)
	if err != nil {
		log.Fatal(err)
	}
	picData, err := ioutil.ReadAll(picFile)
	if err != nil {
		log.Fatal(err)
	}

	tripleKey, _ := hex.DecodeString("b389ae528f9a34bd9835599b9766851b82b42580b720a318")
	txtPlaintext, _ := TripleDesDecrypt(txtData, []byte(tripleKey))
	picPlaintext, _ := TripleDesDecrypt(picData, []byte(tripleKey))
	
	ioutil.WriteFile("flag.txt", txtPlaintext, 0664)
	ioutil.WriteFile("rickroll.jpg", picPlaintext, 0664)

	fmt.Printf("%s", txtPlaintext)
}

func TripleDesDecrypt(ciphertext, key []byte) ([]byte, error) {
	ciphertext = PKCS5Padding(ciphertext, des.BlockSize)
	block, err := des.NewTripleDESCipher(key)
	if err != nil {
		return nil, err
	}
	iv := ciphertext[:des.BlockSize]

	decrypter := cipher.NewCTR(block, iv)
	plaintext := make([]byte, len(ciphertext) - 8)
	decrypter.XORKeyStream(plaintext, ciphertext[des.BlockSize:])
	plaintext = PKCS5UnPadding(plaintext)
	return plaintext, nil
}

func PKCS5Padding(data []byte, blockSize int) []byte {
	padding := blockSize - len(data) % blockSize
	padtext := bytes.Repeat([]byte{byte(padding)}, padding)
	return append(data, padtext...)
}

func PKCS5UnPadding(data []byte) []byte {
	length := len(data)
	unpadding := int(data[length - 1])
	return data[:(length - unpadding)]
}
// hitcon{always_gonna_make_you_cry_always_gonna_say_goodbye}