ByteCTF re moderncpp 题解
题目本身并不难,最大的难点在于要逆C++的数据结构,导致程序的逻辑难以理解。 Analysis main函数位于0x402954,主要的逻辑就是输入、进行检查、初始化、数据处理以及最后的对比。 int __cdecl main(int argc, const char **argv, const char **envp) { __int64 v3; // rax __int64 v4; /
题目本身并不难,最大的难点在于要逆C++的数据结构,导致程序的逻辑难以理解。
Analysis
main函数位于0x402954,主要的逻辑就是输入、进行检查、初始化、数据处理以及最后的对比。
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rax
__int64 v4; // rbx
__int64 v5; // rbx
void (__fastcall *v6)(char *, __int64, __int64); // rbx
__int64 v7; // rax
__int64 v8; // rax
__int64 v9; // rax
char v11[16]; // [rsp+0h] [rbp-C0h] BYREF
char v12[16]; // [rsp+10h] [rbp-B0h] BYREF
char answer[32]; // [rsp+20h] [rbp-A0h] BYREF
char input[47]; // [rsp+40h] [rbp-80h] BYREF
char v15; // [rsp+6Fh] [rbp-51h] BYREF
char v16[16]; // [rsp+70h] [rbp-50h] BYREF
char v17[16]; // [rsp+80h] [rbp-40h] BYREF
__int64 v18; // [rsp+A0h] [rbp-20h]
__int64 v19; // [rsp+A8h] [rbp-18h]
sub_4257A0(input);
nullsub_7(&v15);
sub_427EC0(answer, &dword_536800, 40LL, &v15); //最终结果
nullsub_9(&v15);
v3 = sub_41F910(&stdout, "input:");
((void (__fastcall *)(__int64))sub_41F2C0)(v3);
sub_40BDC0(&stdin, input); //输入
v4 = malloc(64LL);
sub_4010A0(v4);
v18 = v4;
v5 = malloc(64LL);
sub_4010A0(v5);
v19 = v5;
check_1(input); //检查以及初始化
(*(void (__fastcall **)(char *, __int64, char *))(*(_QWORD *)v18 + 8LL))(v16, v18, input);
sub_404334(v12, v16);
sub_403E24(v16);
v6 = *(void (__fastcall **)(char *, __int64, __int64))(*(_QWORD *)v19 + 8LL);
v7 = sub_404EF6(v12);
v6(v17, v19, v7);
sub_404334(v11, v17);
sub_403E24(v17);
v8 = sub_404EF6(v11);
if ( (unsigned __int8)sub_404F07(v8, answer) ) //比较
v9 = sub_41F910(&stdout, "congrats!");
else
v9 = sub_41F910(&stdout, "try again.");
((void (__fastcall *)(__int64))sub_41F2C0)(v9);
sub_403E24(v11);
sub_403E24(v12);
sub_4258C0(answer);
sub_4258C0(input);
return 0;
}
0x4027D5处函数(check_1)先对输入进行了检查,要求长度为41且格式为bytectf{*****}。

接下来进行了部分初始化操作,输出了一句似乎没什么用的话,然后进行了编码操作。
第一步是在0x401414函数处生成字母表

该处操作在内存中生成了一个字母表abcdefghijklmnopqrstuvwxyz0123456789!@#%^&*()_+-=[]{};,接着在该函数中调用0x4018D6处函数,生成与字母表长度相同的一个整数数组,生成的规则如下:

第二步在上述函数中调用0x401C86处函数,主要进行的操作就是将上面生成的两个进行一一对应的组合

第三步是经过对数据的一些预处理操作之后,在sub_4015EE(gen_code)内对字母表中的字母进行编码

很像一棵树,先添“0”,遍历左子树,然后添“1”,遍历右子树,很明显利用预先构造好的树对字符进行0-1编码。编码结束之后对输入进行处理。
第一步处理很简单,对输入进行逐字节编码,拼接在一起,然后8位一组转化为int。

可以发现是高位在前,低位在后

该部分经过多次调试可以发现,每一个字母为不等长0-1编码,且每次编码固定,如果不确定是什么编码方式的话也可以使用调试的方式,通过输入不同的字符来查找对应的编码。但实际上很容易想到这是Huffman编码。
后面的处理是加密,在0x4024A4处,根据特征值0x9E3779B9或者使用findcrypt插件可以很容易发现这是tea加密

明文为刚刚进行编码过的数据,对该函数交叉引用很快就可以找到key

继续回到main函数,加密的结果与最开始拷贝的0x536800处的40个字节进行对比,相同则正确。
Solution
先解密tea得到编码后的结果
#include <stdio.h>
#include <stdint.h>
void decrypt(uint32_t *v, uint32_t *k)
{
uint32_t v0 = v[0], v1 = v[1], sum = 0xC6EF3720, i;
uint32_t delta = 0x9e3779b9;
uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3];
for (i = 0; i < 32; i++)
{
v1 -= ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);
v0 -= ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);
sum -= delta;
}
v[0] = v0;
v[1] = v1;
}
int main()
{
uint32_t v[10] = {0x0C5D3669F, 0x0B917171A, 0x0B4B37B19, 0x0AE80C5F, 0x8D80307F, 0x21522880, 0x34D80589, 0x0DE6C83D1, 0x59B73618, 0x0C6E65D35}, k[4] = {0x62797465, 0x2D637466, 0x77656C63, 0x6F6D657E};
for (int i = 0; i < 5; i++)
{
uint32_t tmp_v[]={v[2*i],v[2*i+1]};
decrypt(tmp_v, k);
printf("%x,%x,", tmp_v[0], tmp_v[1]);
}
return 0;
}
// d869f00c,62fb324a,ccca48e,e56322c0,5e07fdb6,8dc6fee6,ad518dfd,14fa68e4,78
按照小段序转换为字节后,继续转换为8位二进制字符串拼接,从头开始向后遍历,在表中找到对应的字母后调整开头位置,继续向后遍历,Huffman编码保证了这样做结果的唯一性。字母表使用Huffman编码的方式生成会更快,但是手动调试出来对于程序分析的难度会较低,是个比较讨巧的办法。
class Node():
def __init__(self, name=None, value=None):
self.name = name
self.value = value
self.right = None
self.left = None
class HuffmanTree():
def __init__(self, arr):
self.charset = {}
self.nodes = [Node(values[0], values[1])
for values in arr]
while len(self.nodes) != 1:
self.nodes.sort(key=lambda node: node.value)
p = Node(value=(self.nodes[0].value +
self.nodes[1].value))
p.left = self.nodes.pop(0)
p.right = self.nodes.pop(0)
self.nodes.append(p)
self.root = self.nodes[0]
self.Code = list(range(60))
def HuffmanCode(self, tree, length):
node = tree
if (not node):
return
elif node.name:
result = ''
for i in range(length):
result += str(self.Code[i])
self.charset[node.name] = result
return
self.Code[length] = 0
self.HuffmanCode(tree.left, length + 1)
self.Code[length] = 1
self.HuffmanCode(tree.right, length+1)
def GenCode(self):
self.HuffmanCode(self.root, 0)
alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789!@#%^&*()_+-=[]{};'
arr = []
for i in range(54):
arr.append((43*(127+i)) % 233)
char_weights = list(zip(alphabet, arr))
tree = HuffmanTree(char_weights)
tree.GenCode()
# check
charset = {
'b': '00001',
'y': '10011',
't': '11000',
'e': '0011010',
'c': '01110',
'f': '010010',
'{': '10001',
'a': '100101',
'd': '11011',
'g': '111011',
'h': '01000',
'i': '10110',
'j': '00110111',
'k': '1111010',
'l': '110010',
'm': '00011',
'n': '10000',
'o': '10100011101',
'p': '0110011',
'q': '011000',
'r': '111110',
's': '01011',
'u': '11110110',
'v': '000001',
'w': '111000',
'x': '00101',
'z': '101000110',
'0': '1110010',
'1': '100100',
'2': '111111',
'3': '01101',
'4': '11010',
'5': '11110111',
'}': '1010001111',
'6': '001100',
'7': '111010',
'8': '00111',
'9': '10101',
'!': '00110110',
'@': '1110011',
'#': '101001',
'%': '00010',
'^': '01111',
'&': '10100011100',
'*': '0110010',
'(': '010011',
')': '111100',
'_': '01010',
'+': '10111',
'-': '10100010',
'=': '000000',
'[': '110011',
']': '00100',
';': '1010000'
}
assert(charset == tree.charset)
new_dic = dict(zip(tree.charset.values(), tree.charset.keys()))
# d869f00c,62fb324a,ccca48e,e56322c0,5e07fdb6,8dc6fee6,ad518dfd,14fa68e4,78
target = [0x0c, 0xf0, 0x69, 0xd8, 0x4a, 0x32, 0xfb, 0x62, 0x8e, 0xa4, 0xcc, 0x0c, 0xc0, 0x22, 0x63,
0xe5, 0xb6, 0xfd, 0x07, 0x5e, 0xe6, 0xfe, 0xc6, 0x8d, 0xfd, 0x8d, 0x51, 0xad, 0xe4, 0x68,
0xfa, 0x14, 0x78]
f = ''.join([bin(i)[2:].rjust(8, '0') for i in target])
i = 0
j = 5
while j < len(f):
tmp = f[i:j]
if new_dic.get(tmp):
print(new_dic[tmp], end='')
i = j
j += 5
else:
j += 1
需要注意的是这里生成Huffman编码的时候,相同数值使用不同的排序算法可能会有不同的顺序,例如使用python的sort算法时,倒序的方式生成的编码与题目中生成的不同,有可能需要多尝试几次。