hint
It seems to be hard to reverse-engineer the anti-virus signature???
阶段一:cbc后缀是啥?
print_flag.cbc
不知道是什么格式,看起来是plaintext,都是可见ascii字符
run.sh
中出现了clamav、clamscan等关键词
首先找到clamav的官方文档,得知clamav是一个反病毒引擎
然后搜工具clanscan的资料,看到一个Debian上clanscan的手册
--database=FILE/DIR
- load virus database from FILE or load all supported db files from DIR
[--bytecode-unsigned[=yes/no(*)]](https://manpages.debian.org/testing/clamav/clamscan.1.en.html#bytecode)
Allow loading bytecode from outside digitally signed .c[lv]d files. Caution: You should NEVER run bytecode signatures from untrusted sources. Doing so may result in arbitrary code execution.
手册指出run.sh
中命令是加载print_flag.cbc
这个病毒数据库,并允许加载不受信任的字节码
即print_flag.cbc
这个文件是一个病毒数据库
搜索clamav病毒数据库的格式是cvd,结合官方文档,cbc后缀的似乎是bytecode signatures
搜索bytecode signatures到的一份官方比较旧的相关资料ClamAV® blog: Brief Re-introduction to ClamAV Bytecode Signatures,其中关于字节码数据库内容摘抄如下:
Bytecode Databases
Bytecode signatures are stored in a separate database from the standard ClamAV signatures. In fact, it is impossible to generate database files (with sigtool) that contain both bytecode signatures and standard signatures. Bytecode databases are generated by building a database that is named “bytecode.*” which triggers specialized handling by sigtool. Sigtool will add all bytecode signatures in the specified directory regardless if the name of the signature matches the name of the database. Bytecode signatures thus can be named anything provided the extension is “.cbc”.
总结一下就是cbc后缀的是字节码签名,一般从clamav 的 cvd后缀数据库中通过clamav提供的工具可以解压出若干字节码签名。
字节码签名允许拓展反病毒引擎匹配病毒的机制,如其名字,clamav中包含一个vm可以执行字节码签名中包含的字节码(意味着字节码签名有任意代码执行的风险,不受信任的来源获取字节码签名是不安全的,虽然这与本题无关)
阶段二:字节码签名格式是啥?
继续搜索Bytecode Signatures相关信息,官方文档 Bytecode Signatures章节提到编译字节码签名的项目是ClamAV Bytecode Compiler.
项目中有个过期pdf documentatio,没啥太大作用,提到一个测试字节码签名的工具叫clambc,该工具在安装clamav时会被一起安装
试用一下clamscan
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | clamscan --bytecode-unsigned -d /home/yssfpwn/ 下载 /hitcon-av/print_flag .cbc run.sh
/home/yssfpwn/ 下载 /hitcon-av/run .sh: OK
/home/yssfpwn/ 下载 /hitcon-av/print_flag .cbc: OK
----------- SCAN SUMMARY -----------
Known viruses: 1
Engine version: 0.103.11
Scanned directories: 1
Scanned files: 2
Infected files: 0
Data scanned: 0.02 MB
Data read : 0.01 MB (ratio 2.00:1)
Time: 0.005 sec (0 m 0 s)
Start Date: 2024:07:13 00:11:41
End Date: 2024:07:13 00:11:41
|
前面过期文档提到的用GDB调试字节码的方法,没用上,需要libtool没有细究是啥
1 | . /libtool --mode=execute gdb clamscan /clamscan
|
据说clamav配套的sigtool工具可以解析字节码签名,但不行,和这个工具应该是无关的
1 | sigtool --decode-sigs < print_flag.cbc
|
ubuntu上安装官网下载的deb安装包安装失败,apt install的版本是0.x的
于是在windows上安装了官网下载的1.0.6版本msi安装包,试了一下加载字节码签名扫描
sigtool照样不行,这个工具应该是和字节码签名无关的?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | > clamscan --bytecode-unsigned -d 'print_flag.cbc'
Loading: 0s, ETA: 0s [========================>] 1 /1 sigs
Compiling: 0s, ETA: 0s [========================>] 40 /40 tasks
C:\Users\wind\Downloads\hitcon-av\print_flag.cbc: OK
----------- SCAN SUMMARY -----------
Known viruses: 1
Engine version: 1.0.6
Scanned directories: 1
Scanned files: 1
Infected files: 0
Data scanned: 0.02 MB
Data read : 0.01 MB (ratio 2.00:1)
Time: 0.362 sec (0 m 0 s)
Start Date: 2024:07:13 13:15:26
End Date: 2024:07:13 13:15:26
> sigtool --decode-sigs < print_flag.cbc
ERROR: decodesig: Invalid or not supported signature format
TOKENS COUNT: 2
|
看clambc的help,终于找到解析字节码签名(cbc后缀)方法,该工具-c
方法可以输出 字节码汇编/字节码中间代码(bcir是bytecode ir吧)
--info
方法可以看到字节码签名的一些基本信息,没啥用
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 | > clambc --help
...
--printbcir -c Print IR of bytecode signature
...
> clambc print_flag.cbc -c > bytecode.txt
> clambc print_flag.cbc --info
Bytecode format functionality level: 6
Bytecode metadata:
compiler version: 0.105.0
compiled on: (1719557581) Fri Jun 28 14:53:01 2024
compiled by:
target exclude: 0
bytecode type : logical only
bytecode functionality level: 0 - 0
bytecode logical signature: PRINT_FLAG.{Fake,Real};Engine:56-255,Target:0;0;0:4d5a
virusname prefix: (null)
virusnames: 0
bytecode triggered on: files matching logical signature
number of functions: 2
number of types: 25
number of global constants: 13
number of debug nodes: 0
bytecode APIs used:
read , seek, setvirusname
> clambc print_flag.cbc --input file .bin --debug
|
输出的字节码汇编 bytecode.txt
阶段三:处理字节码汇编(bcir)
搜索clambc --help
中输出IR描述提到的clamav IR of bytecode signature "setvirusname"
找到一篇wp:SECCON CTF 2022 Quals writeup - st98 の日記帳 - コピー (hateblo.jp)
结合wp和字节码汇编的描述,推断字节码汇编的格式
类型信息表
开头是类型信息表,在本题中只在OP_BC_GEP1中发现类型信息似乎有用:
1 2 3 4 5 6 7 8 | found 25 extra types of 89 total, starting at tid 69
TID KIND INTERNAL
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
65 : DPointerType i8 *
66 : DPointerType i16 *
67 : DPointerType i32 *
68 : DPointerType i64 *
......
|
函数
接着是函数定义,包括:三行井号的开头、全局变量表(globals)、变量表(values)、常量表(constants)、函数字节码
本题有两个函数,分别是F.0和F.1
函数F.0的三行井号的开头:
函数:全局变量表、变量表、常量表
三个变量表中都包含ID,其中变量表和常量表的ID是顺序编号不重复的,用于在本函数内索引
全局变量表GID和ID似乎是一样的,索引方式和变量/常量表不一样(详见 索引全局变量 章节)
全局变量表(globals),两个函数全局变量表是一样的:
1 2 3 4 5 6 7 8 9 | found a total of 13 globals
GID ID VALUE
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
0 [ 0 ]: i0 unknown
1 [ 1 ]: [ 32 x i8] unknown
2 [ 2 ]: [ 396 x i8] unknown
......
5 [ 5 ]: i8 * unknown
......
|
其中id为0的全局变量是不可用的
VALUE列是变量的类型,但没有区分有无符号,只是区分数组、指针、长度
[32 x 8]
表示数组,等价于c语言的char g1[32];
i8*
表示8位整型的指针,等价于c语言的char*
i64
表示64位整型,其他以此类推
变量表(values),其中包括函数参数(arguments)和局部变量(locals),这里列F.1的变量表(F.0没有函数参数):
1 2 3 4 5 6 7 8 | found 303 values with 2 arguments and 301 locals
VID ID VALUE
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
0 [ 0 ]: i8 * argument
1 [ 1 ]: i32 argument
2 [ 2 ]: alloc i64
3 [ 3 ]: alloc i64
......
|
其中ID为0、1的变量VALUE列中标注了argument,表示是函数参数(argument)
其余为函数的局部变量(local)
常量表(constants):
1 2 3 4 5 6 7 8 | found a total of 154 constants
CID ID VALUE
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
0 [ 303 ]: 7 ( 0x7 )
1 [ 304 ]: 0 ( 0x0 )
2 [ 305 ]: 0 ( 0x0 )
3 [ 306 ]: 32 ( 0x20 )
......
|
常量表的VALUE是常量的十进制和十六进制值,没有给出常量的长度
函数:字节码列表
接着是函数字节码:
1 2 3 4 5 6 7 8 9 10 11 12 | FUNCTION ID : F. 1 - > NUMINSTS 453
BB IDX OPCODE [ ID / IID / MOD] INST
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
0 0 OP_BC_TRUNC [ 14 / 71 / 1 ] 19 = 1 trunc ffffffff
0 1 OP_BC_AND [ 11 / 56 / 1 ] 20 = 19 & 303
0 2 OP_BC_ICMP_EQ [ 21 / 108 / 3 ] 21 = ( 1 = = 304 )
0 3 OP_BC_BRANCH [ 17 / 85 / 0 ] br 21 ? bb. 92 : bb. 1
1 4 OP_BC_ZEXT [ 16 / 84 / 4 ] 22 = 1 zext ffffffff
1 5 OP_BC_COPY [ 34 / 174 / 4 ] cp 305 - > 11
1 6 OP_BC_JMP [ 18 / 90 / 0 ] jmp bb. 2
......
|
比较有用的信息:BB列表示的是基本块号,OPCODE是字节码类型,INST列是字节码汇编文本
在指令中,变量(value,包括函数参数argument和局部变量local)和常量(constant)由上述 ID 引用。全局变量见 索引全局变量 章节。
比如BB=0, IDX=1
的指令20 = 19 & 303
,中的20、19、303都是变量/常量的ID,查变量表和常量表可知是id=19的局部变量 与 id=303的常量,赋值给id=20的局部变量。
如果 ID 以 p 为前缀,则表示局部变量被视为指针,相当于c语言的(char*)value
,所以直接把p无视即可。
字节码的功能根据INST列来猜就行,大部分比较好猜,猜不到就看项目源码
字节码的功能实现在bytecode_vm.c中
OP_BC_CALL_API
OPCODE=OP_BC_CALL_API调用VM提供的API,API的功能需要参考源码
比如
1 | 1 5 OP_BC_CALL_API [ 33 / 168 / 3 ] 7 = setvirusname[ 4 ] (p. - 2147483636 , 36 )
|
表示v7 = setvirusname(g2147483660, v36)
(-2147483636=2147483660=0x8000000c,详见 索引全局变量 章节)
bytecode_api.h是API的定义,bytecode_api.c是API的实现
比如api setvirusname的定义和实现
本题出现了setvirusname、seek、read三个api
setvirusname需要配合函数返回值使用,返回0表示未匹配,返回1表示匹配,通过该api设置匹配的病毒的名称
seek类似c语言中的fseek,不同是不需要传入fp这个参数,相当于fp是要扫描的文件,返回值是相对文件开头的偏移(相当于c语言的fseek+return ftell)
read是读取要扫描的文件的内容,类似fread
字节码汇编转C语言
转换成c语言(没有仔细处理signed和unsigned,只是在注释里标注)
结果:bytecode.c
分析见 阶段四:分析字节码功能 章节
索引全局变量
关于负数id,其实是32位整型按有符号输出了,最高位为1(有符号负数)表示是索引全局变量表
比如id=-2147483636,这实际上是32位有符号数,转无符号是0x8000000c,即对应全局变量表(globals)中id=0xc的全局变量
参考bytecode_vm.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 | #define READNfrom(maxBytes, from, x, n, p) \
CHECK_GT((maxBytes), (p) + (n / 8) - 1); \
CHECK_EQ((p) & (n / 8 - 1), 0); \
x = *(uint_type(n) *)&(from)[(p)]; \
TRACE_R(x)
#define READN(x, n, p) \
do { \
if (p & 0x80000000) { \
uint32_t pg = p & 0x7fffffff; \
if (!pg) { \
x = 0; \
} else { \
READNfrom(bc->numGlobalBytes, bc->globalBytes, x, n, pg); \
} \
} else { \
READNfrom(func->numBytes, values, x, n, p); \
} \
} while (0)
#define READ1(x, p) \
READN(x, 8, p); \
x = x & 1
#define READ8(x, p) READN(x, 8, p)
#define READ16(x, p) READN(x, 16, p)
case OP_BC_COPY * 5 + 3: {
uint32_t op;
READ32(op, BINOP(0));
WRITE32(BINOP(1), op);
break ;
}
case OP_BC_COPY * 5 + 4: {
uint64_t op;
READ64(op, BINOP(0));
WRITE64(BINOP(1), op);
break ;
}
|
阶段四:分析字节码签名
大致功能
字节码汇编,转C+O2优化+ida f5分析
输入是扫描的文件,f1对扫描文件进行加密,然后把加密结果和全局变量的内容比较。IR中没有找到全局变量初始化相关的IR
找全局变量初始化内容位置
cli_vm_execute往上找找到cli_bytecode_load
clamav/libclamav/bytecode.c/cli_bytecode_load这个函数是从cbc文件中加载bytecode
同文件中的parseGlobals(struct cli_bc *bc, unsigned char *buffer)函数处理全局变量的加载,加载cbc文件中以G开头的一行内容
特别处理OP_BC_ICMP_SGT有符号及常量长度问题
本题中唯一一个SGT
1 | 2 14 OP_BC_ICMP_SGT [ 27 / 136 / 1 ] 30 = ( 29 > 308 )
|
id=308是常量 255(0xFF)
这个有符号大于应该解释为 v30 = (((char)v29) > (-1));
阶段五:解密
题目大意是f1异或加密输入f1(input) = input ^ xor
,然后比较f1(input) == secret
一个思路是输入396个A,dump f1加密的结果:f1('A'*396) = fake_secret
然后解出异或的数据xor = fake_secret ^ 'A'*396
最后secret ^ xor = input
得到正确输入
但是发现有问题,xor数组不是完全固定不变的,其中几个元素会受input影响,即f1(input) = input ^ fxor(input, xor)
这个fxor是啥懒得去研究了,input对fxor结果影响很小,fxor结果基本等于xor
解决方法是先按上面思路得出一个近似异或数组,以及近似正确输入,然后迭代直到得到正确结果
- 先算一组异或后的结果
f1(ori_input) = ori_input^ fxor(ori_input, xor)
- 比较
f1(ori_input)
和secret
是否相等,相等就结束,否则继续第3步
- 由异或后的结果算近似的异或数组
fake_xor = f1(ori_input) ^ ori_input
- 然后反推近似的正确输入
new_input = fake_xor ^ secret
- 将
new_input
带入ori_input
,回到第1步开始新一轮循环
具体实现,试了一下迭代3、4轮就相等了,就没有做退出的判定:
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 | const unsigned char v2147483649bak[32] = {0x2C, 0x5, ...};
const unsigned char v2147483650bak[396] = {0x45, 0x5F, ...};
const unsigned char v2147483651bak[16] = {0x50, 0x52, ...};
const unsigned char v2147483652bak[16] = {0x50, 0x52, ...};
unsigned char v2147483649[32] = {0x2C, 0x5, ...};
unsigned char v2147483650[396] = {0x45, 0x5F, ...};
unsigned char v2147483651[16] = {0x50, 0x52, ...};
unsigned char v2147483652[16] = {0x50, 0x52, ...};
unsigned char *v2147483653;
unsigned char *v2147483654 = v2147483649;
unsigned char *v2147483655;
unsigned char *v2147483656 = v2147483650;
unsigned char *v2147483657;
unsigned char *v2147483658 = v2147483651;
unsigned char *v2147483659;
unsigned char *v2147483660 = v2147483652;
int main( int argc, char *argv[]) {
unsigned char obuf[396], xor[396];
unsigned char buf[396];
for ( int i = 0; i < 396; i++){
buf[i] = 'A' ;
}
for ( int round = 0; round < 5; round++){
memcpy (v2147483649, v2147483649bak, sizeof (v2147483649));
memcpy (v2147483650, v2147483650bak, sizeof (v2147483650));
memcpy (v2147483651, v2147483651bak, sizeof (v2147483651));
memcpy (v2147483652, v2147483652bak, sizeof (v2147483652));
v2147483654 = v2147483649;
v2147483656 = v2147483650;
v2147483658 = v2147483651;
v2147483660 = v2147483652;
memcpy (obuf, buf, 396);
f1(buf, 396);
for ( int i = 0; i < 396; i++){
if (buf[i] != v2147483650[i]){
printf ( "%d, " , i);
}
}
for ( int i = 0; i < 396; i++){
xor[i] = buf[i] ^ obuf[i];
}
for ( int i = 0; i < 396; i++){
buf[i] = v2147483650bak[i] ^ xor[i];
}
printf ( "round=%d\n" , round);
}
for ( int i = 0; i < 396; i++){
printf ( "0x%X, " , obuf[i]);
}
printf ( "\n" );
return 0;
}
|
阶段z:一些未验证的东西
OPCODE=OP_BC_GEP1的第4个id猜测是tid
比如
1 | 2 12 OP_BC_GEP1 [ 35 / 179 / 4 ] 28 = gep1 p. 0 + ( 27 * 65 )
|
65是tid,查到是 65: DPointerType i8*
,i8是1字节
这条字节码猜测为 v28 = v0 + (v27 * 1);
赛后总结
这题做得不是很理想,很多细节没到位,头脑有些混方向不明确,导致做题时间很长,花了一天半
第二天(13号)早上11点半跟随官方文档找到编译字节码的源码,这里算是被误导了;头脑有些混乱,当时目标是找cbc文件(即字节码签名)的格式,应该找解释、运行cbc文件的虚拟机的代码,而不是找编译cbc文件的代码
第二天(13号)下午1点半找到输出字节码汇编(bcir),花了很多时间在百度,但是似乎clamav在互联网的资料很少,收效甚少;应该早点看工具的--help的
接着卡在负数id上一段时间,直到晚上9点找到虚拟机源码,本来应该是步入正轨了,但后面又犯病了
解决完负数id又卡在全局变量初始化上,傻傻的又去百度找资料,不幸的是没有找到;明明都找上源码了,应该去细读源码的
第三天(14号)百度无果,终于又回到正轨去看源码了,然后在下午3点找上虚拟机加载cbc的代码
后面4、5点左右又犯大病了,此时已经完成字节码到c的转换,gcc -O2+ida f5分析,妈个鸡都有源码了,直接源码里输出就好了,还非要在ida里dump,一番调试浪费了很多时间,直到6点多才幡然醒悟,直接改转换出来的c代码去解密
总结,没有一直提醒自己当前阶段明确的目标是什么,以至于收集信息、逆向分析阶段找错方向,解密阶段也绕弯路浪费了不少时间