个人感觉reverse题目里面解数最少的index反而不是最阴间的,起码没有conforand阴间
在这里仅给出我个人解出的题目的WriteUp.
Crypto方向:
费克特尔:
直接上factordb分解n即可
1 | from Crypto.Util.number import * |
TGCTF{f4888_6abdc_9c2bd_9036bb}.
宝宝rsa:
前半段爆破e,后半段直接开方
1 | import gmpy2 |
TGCTF{!!3xP_Is_Sm@ll_But_D@ng3r0}.
AAAAAAAA·真·签到:
注意到是变位凯撒,偏移量为-1,0,1…且仅对大写字母生效,编写脚本解密:
1 | def decrypt_flag(encrypted_flag): |
TGCTF{WO0O!Y04_5R3_GOOD_AT_MOVE}.
mm不躲猫猫:
多组nc:
1 | from gmpy2 import * |
TGCTF{ExcePt10n4lY0u_Fl4gF0rY0u_555b0nus}.
Misc:
next is the end:
编写脚本提取嵌套文件夹中的文件:
1 | import os |
flag{so_great!}.
你的运气是好是坏?:
无脑猜114514,居然一下子就猜对了
但是我想问问出题人,这样没活烂整真的好么?不知道114514这个梗的就要被枪毙么?我也不是没在群里见过有人死活猜不到的,希望以后不要这么出了,没必要拿一个没意义的大粪来凑题目数量。
这是啥o_o:
发现是一个GIF,提取帧间隔得到:[840, 710, 670, 840, 700, 1230, 890, 1110, 1170, 950, 990, 970, 1170, 1030, 1040, 1160, 950, 1170, 1120, 950, 1190, 1050, 1160, 1040, 950, 1160, 1050, 1090, 1010, 330, 1250],除以10得到flag:TGCTF{You_caught_up_with_time!}.
你能发现图中的秘密吗?:
查看图片,发现没有额外数据块,考虑隐写,用zsteg得到:
1 | imagedata .. text: "{|{@C@M@L" |
打开压缩包,发现一个pdf和一个png,其中pdf用ps打开,发现有一个隐藏图层,得到flag2:flag2=_attentive_and_conscientious},png文件最后有一个比别的IDAT块都要大的数据块,单独提取得到一个图片,发现其宽度是375的整数倍时好像有模糊不清的英文,爆破遍历宽高发现为1125时是正确的,flag1:flag{you_are_so,拼合得到flag{you_are_so_attentive_and_conscientious}.
Reverse方向:
Base64:
查看base64函数:
1 | _BYTE *__fastcall base64(__int64 a1) |
换表base64,又多了一些额外的加减逻辑,编写脚本解密:
1 | import base64 |
HZNUCTF{ad162c-2d94-434d-9222-b65dc76a32}.
水果忍者:
是unity引擎游戏,用dnspy打开assembly-csharp.dll,在gamemanager找到:
1 | // Token: 0x04000015 RID: 21 |
AES解密得到HZNUCTF{de20-70dd-4e62-b8d0-06e}.
XTEA:
查看main:
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
发现有一个原版大端序XTEA加密,其中delta要自己输入,cipher根据rand生成,动态调试绕过debugger得到密钥[6648,4542,2449,13336],猜测delta还是0x9E3779B9
,编写脚本解密:
1 | def encrypt(dwordenc, dwordkey, delta): |
输出:0x485a4e55, 0x4354467b, 0x6165362d, 0x39663537, 0x2d346237, 0x342d6234, 0x32332d39, 0x3865627d
HZNUCTF{ae6-9f57-4b74-b423-98eb}.
蛇年的本命语言:
Python解包反编译:
1 | from collections import Counter |
先用z3一把梭掉后面的矩阵:
1 | from z3 import * |
输出[72, 49, 90, 49, 78, 49, 85, 49, 67, 49, 84, 49, 70, 49, 123, 49, 97, 54, 100, 50, 55, 53, 102, 55, 45, 52, 54, 51, 125, 49],即为H1Z1N1U1C1T1F1{1a6d275f7-463}1,再解出现次数:
1 | lists = [['H', 1], ['Z', 1], ['N', 1], ['U', 1], ['C', 1], ['T', 1], ['F', 1], ['{', 1], ['a', 6], ['d', 2], ['7', 5], ['f', 7], ['-', 4], ['6', 3], ['}', 1]] |
HZNUCTF{ad7fa-76a7-ff6a-fffa-7f7d6a}.
randomsystem:
发现有很多花,去花,查看main:
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
查看func1:
1 | _DWORD *__cdecl func1(int a1, _DWORD *a2) |
观察汇编发现了一些没有反编译出来的逻辑,比如cmp 输入值, “1”,经过分析这是提取64位输出值中的二进制,将其写入64个二进制位(1个qword)的过程,接着查看func2:
1 | int __cdecl func2(__int64 a1, int a2) |
同理发现这是一个类似于to hex的功能,只不过是基于ascii 0x30~0x3F的,那么目标值就是5265566552655365,还原得到要输入的值为0101001001100101010101100110010101010010011001010101001101100101,代入验证发现正确,继续往后看:
1 | printf("good_job!!!\n", v9); |
编写脚本同构加密过程,有flag矩阵经过了打乱、相乘和异或,编写脚本解密:
1 | import numpy as np |
HZNUCTF{3zfb899ac5c256d-7a8r59f0tccd-4fa6b8vfd111-a44ffy4r0-6dce5679da58}.
conforand:
查看main,发现有严重的ollvm,将函数整体复制到源代码,找到其中的有效执行流并包裹上打印,修掉报错,执行程序,得到main函数大致执行流:
1 | init(v155, (const char *)v3, v4); |
可以看出是一个rc4加密,密文以十六进制形式输出,flag长度为2A,密钥长度为9,尝试解析init函数:
1 | __int64 __fastcall init(char *a1, const char *a2, __int64 a3) |
找到了9字节密钥JustDoIt!,查看sub_5s5s5s,发现了以下几行:
1 | *v205 = time(0LL); |
说明有某个东西会基于时间的随机数生成,继续往下查看rc4函数,找了很久发现了这些rc4特征,将其有关联的变量重命名并查找xref,尽可能地恢复出所有调用:
323~325行:
1 | *sbox_j = sbox__i_sbox_j % 257; |
339~341行:
1 | sboxi = *sbox_i; |
365~366行:
1 | sbox_i = sboxinited; |
372行:
1 | v244 = v242 < lenm_3; |
384~403行:
1 | v113 = sbox__i_sbox_j; |
408行:
1 | v242 = *sboxinited_1; |
438~445行:
1 | inited = init_sbox((__int64)sbox_1, key_3); |
580行:
1 | if ( v244 ) |
587~596行:
1 | sboxinit = init_sbox((__int64)sbox_1, key_3); |
634~678行:
1 | sbox[*sbox_j] = *sbox__j; |
721行:
1 | sbox__i_sbox_j = (unsigned __int8)sbox[*sbox_i] + *sbox_j; |
729~736行:
1 | sbox_t = sboxinited; |
775行:
1 | v247 = *sboxinited_1; |
783行:
1 | *sboxinited_1 = v248; |
将这些行打上断点调试,恢复出PRGA的执行流:
Init_sbox → 初始化I, j, t等参数为0 → 在密文长度内循环{i自增模257 → j = si + j模257 → si = sj → sj = si → t = si + sj模257 → 对明文和st进行操作(结合动调发现还是异或不变)}
再观察KSA部分,也用上面的方法恢复出执行流:
初始化各种数据(包括一个额外的s盒) → 初始化第一个s盒为1~257 → 初始化第二个s盒为密钥的当前索引取模密钥长度再异或(推测)rand值,接下来在257轮循环内:{j = 当前的j与两个s盒的加和取模257 → 交换第一个s盒的第ij项}
动态调试注意到这个rand值只获取了一次,随后便被一直使用,所以不需要爆破时间戳,仅需爆破异或值即可,综上,结合所给密文,编写同构加解密脚本并爆破:
1 | def rc4(m, key, xor): |
HZNUCTF{489b88-1305-411e-b1f4-88a3070a73}.
exchange:
先脱upx,发现可以运行了,查看main:
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
发现会按ascii读取flag包裹的内容,并每2字节进行了交换,查看check:
1 | __int64 __fastcall sub_7FF76FC77F30(int a1, int a2) |
发现了一个DES加密,密钥为HZNUCTF{
,查看该DES:
1 | __int64 __fastcall DES_0(__int64 key, char a2, const void *m, void *out, unsigned int a5) |
经过观察发现该DES是被魔改的,但是由于下面是3DES,所以Func3一定是解密函数,不妨把上面的DES的call Func2 patch为call Func3(毕竟参数都一样),E8 FC EF FF FF改为E8 BD ED FF FF,成功patch为Func3,进行动态调试,将密文patch进v12,观察执行后的字段:333936147332632923d96353321d3345636826d26314621d3349330463126348,可以注意到成功解密,换位后得到33393164733262392d396533312d343566382d626134612d3439303461326438,fromhex得到391ds2b9-9e31-45f8-ba4a-4904a2d8,flag:HZNUCTF{391ds2b9-9e31-45f8-ba4a-4904a2d8}.
index:
发现是wasm文件,但是前四字节的magic被改成了大写,改回小写后用ghidra解析:
1 | undefined4 export::main(undefined4 param1,undefined4 param2) |
查看main0(函数名称已手动恢复):
1 | undefined4 main_0(void) |
发现是首先读取输入存储在v0+64,然后与TGCTF404进行比较,如果相等,进入异或0x51的函数:
1 | void xor51(int param1) |
发现是将TGCTF404循环异或0x51,继续分析main0,接下来输入flag,存储在80,且要求长度为32,接下来进行了一个类似于randomize的函数,将打乱的结果存储在32,循环用encrypt2和异或后的key进行加密,之后将密文与0x10fa0位置的dword密文进行比较,输出结果,先提取密文,得到841c6bf74922d642507b42f446a98362d13280426a10a3f2e2b80b76b0dc0251,继续分析加密逻辑,先查看encrypt1:
1 | void encrypt1(int param1,int param2) |
1 | void unnamed_function_15(int param1) |
1 | void unnamed_function_8(int param1,int param2) |
1 | uint unnamed_function_16(void) |
发现是将输入的明文随机打乱,随机数种子为0x194(404),再分析encrypt2,发现这个函数是被循环调用的,一次只加密了输入值的一个dword,循环8次,查看encrypt2:
1 | void encrypt2(int param1,int param2) |
其中0x10ea0的位置是SM4的S盒,0x10da0的位置是AES的S盒,继续查看encrypt3:
1 | void encrypt3(int param1,undefined4 param2) |
encrypt3对SM4的S盒进行异或AES的S盒的指定索引的值,构成新的S盒,回到encrypt2,发现是将字符异或新的S盒的指定索引,然后再异或被异或后的密钥,综上所述已经分析完毕,编写脚本解密:
1 | AES_sbox = [0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, |
得到中间密文Z49H539c{–6}d4888bTUCf8NeFe–e9,继续解密一阶段的洗牌算法,编写脚本同构加密过程:
1 |
|
得到对照表:
1 | before -> after: |
编写脚本解密:
1 |
|
HZNUCTF{f898-de85-46e-9e43-b9c8}.
总结:
本次比赛费了很大劲终于把re ak了…
对三道压轴题的评价:
conforand的ollvm是真的辣眼睛
exchange的patch call解密函数实际上是非预期,不过魔改DES我是真逆不太动(x)
index还好,换个工具解析就行,就是随机数部分有点不稳定,直接用反编译出来的貌似不太行,用库函数反而出了…