Reverse题记

有段时间没有写 CTF Reverse 题目了,在 AI 盛行的时代,手搓题目只能说是自己的兴趣了,在此记录一下以后手搓的 Reverse 题目,作为纪念吧
Attachments_505
找到 main 函数
1 | scanf("%s", Str); |
判断输入字符串是否为 49
此处 v3 和 v10 其实是一个变量,将对应的 Str[i] 减去 48 储存到 box 中,0 ASCII 码值为 48,实则为字符转数字,49长度的字符串转为每9个一行
check1
1 | for ( i = 0; i <= 8; ++i ) |
每一行,需要满足 1-9 的数字都出现一次
check2
1 | for ( i = 0; i <= 8; ++i ) |
同样的,每一列,需要满足 1-9 的数字都出现一次
check3
1 | for ( i = 0; i <= 8; i += 3 ) |
为了检查每个 3x3 的小方格是否包含 1-9 的数字
那么 box 实际为一个满足数独要求的 9x9 的二维数组,输入的字符串长度为 49
实际上可以从 puts_0("Enjoy the beauty of reverse and sudoku!"); 看出这里是数独
最后输出:
1 | for ( k = 0; k < strlen(Str); ++k ) |
magic 数组:
1 | .data:0000000000404080 magic db 6Bh, 2, 66h, 70h, 44h, 69h, 7Eh, 6Eh, 43h, 4Ah, 78h |
64 位,即
1 | 6B 02 66 70 44 69 7E 6E 43 4A 78 4A 6D 60 56 00 51 59 50 43 50 51 6D 74 02 55 50 52 6E 6F 79 40 5D 4B 1E 19 1C 74 03 54 07 4C 52 6A 60 50 58 40 58 0F 00 |
同时可查询到 box 数组默认值
1 | .data:0000000000404020 box db 2 dup(0), 5, 2 dup(0), 4, 3, 6, 5 dup(0), 5, 2 dup(0) |
即
1 | 0 0 5 | 0 0 4 | 3 6 0 |
解数独
1 | 8 2 5 | 9 1 4 | 3 6 7 |
对应应该输入的字符串为:
1 | 8291767138932581849755263447186268341129653538127 |
1 | # ./chall.exe |
1 | moectf{S0_As_I_prAy_Un1imited_B1ade_WOrks---E1m1ya_Shiro} |
Attachments_493
静态链接,C++ 代码编写
1 | std::operator<<<std::char_traits<char>>(&_TMC_END__, "Welcome to MoeCTF 2020!\nPlease input your flag here >>", envp); |
输入了一个长度为 22 的字符串
1 | __int64 __fastcall sub_0() |
进行了多次加密,顺序为
1 | sub_0 -> sub_1 -> sub_2 -> ... ->sub_1996 -> sub_1997 -> sub_1998 -> sub_1999 |
主函数主要是比较输入的字符串的和 key 字符串的每个字符是否相同,key 字符串为:
1 | .data:0000000000074060 key db 2, 0Ah, 6, 16h, 13h, 1Ch, 2, 4Dh, 41h, 28h, 3Dh, 56h |
即
1 | 02 0A 06 16 13 1C 02 4D 41 28 3D 56 52 39 19 70 51 1B 5A 59 7A 22 |
通过动态调试知道第 k 个输入只影响输出 k 和 k-1
我们知道 flag 字符串前面是固定的 meoctf{,所以可以尝试写 gdb_python 脚本爆破
1 | import gdb, string |
上面这个代码的逻辑就是不断尝试调用 sub0 来爆破 flag,如果下一位算出来的答案是 key[j] 那么就说明这个字符是正确的
1 | pwndbg> source solution.py |
得到 flag
1 | moectf{y0u_a2e_G0d~!} |
Attachments_494
搜索字符串发现可疑字符串
1 | .rdata:0000000140005470 unk_140005470 db 6Dh ; m ; DATA XREF: main+163↑o |
main+163 处有交叉引用,这边直接 UTF8 解码得到
1 | moectf{世界で一番幸せな女の子—CHTHOLLY—} |
main 函数有点复杂
先根据程序的输出对应一下
1 | # ./CxxIntro.exe |
找到 cout 对象
1 | sub_1400026E0(std::cout, &unk_1400054E0); |
&unk_1400054E0 就是那一串日文
最后 v32 = sub_140002EA0(std::cout, v31, v30[2]);
预估是将输入进行一系列变换之后的结果
根据汇编代码,缺失的 v21 为 cin 输入的字符串,v22 为字符串转为字节的字节串
修改相关变量:
1 | sub_1400026E0( |
random 函数查看:
1 | __int64 __fastcall random(unsigned int *a1) |
这个为 MT19937 实现的梅森旋转算法(随机数生成函数)
1 | _QWORD *__fastcall push_back(_QWORD *Src, __int64 a2, __int64 a3, char a4) |
v9 = (v4 + 1) | 0xF; 为新容量计算,v10 + v8 即容量+容量/2 为典型的动态数组扩容策略,则该函数为 std::vector 的 push_back 实现
那么上述流程即位将输入的字符串转为字节串,然后每个字节与 random 函数生成的随机数进行异或,暂存到 Src 中,调用 sub_140001380 进行处理,最后输出到 cout 中
这里的 Block 为上述构建的,sub_140001380 为自定义算法
1 | // Hidden C++ exception states: #wind=1 |
记 i 为字节索引,j 为累加器,每次加上上次取出的值 SBoxChar1
这个函数为取出之前生成的 Block 里面的内容,取出两个值 SBoxChar1=Block[i >> 4][j & 0xF] 和 SBoxChar2[i >> 4][(j + SBoxChar1) & 0xF],接着交换 SBoxChar1 和 SBoxChar2,然后计算key = Block[(SBoxChar1 + SBoxChar2) >> 4][(SBoxChar1 + SBoxChar2) & 0xF] ,与函数传入的 a3 进行逐字节异或,放到输出字符串中
接下来查看 Block 怎么初始化的
1 | for ( i = 0; i < 0x10; ++i ) |
每一行的第一个字符为 73 * (i + 1),即 49 92 135 178 221 8 51 94 137 180 223 66 109 152 195 238,然后经过了一系列变化,发现这个变化是固定的,动态拿到 Block 的值
编写 IDA-python 脚本
1 | import idc |
得到 SBOX
1 | const unsigned char SBOX[16][16] = { |
从 main 函数来看,输入 -> random 异或 -> sub_140001380 处理 -> 输出,这个序列是确定的,但是和 flag 似乎没有关系
但注意到在 .rodata 段下面有一个字符串:
1 | magic{75b1743a9200a5bc60c70b666eaad2911ddb2292eed4d3d2b7ebc7867b2aa5499949810031756dcdc1dc6500} |
这个字符串比较奇怪,通过尝试可以知道:
75b1743a9200a5bc60c70b666eaad2911ddb2292eed4d3d2b7ebc7867b2aa5499949810031756dcdc1dc6500 通过逆sub_140001380 处理后得到结果
编写脚本
1 | from pathlib import Path |
或者:
1 | magic = bytes.fromhex( |
输出:
1 | moectf{CHTHOLLY_the_best__sukasuka_the_best} |
flag 为:
1 | moectf{CHTHOLLY_the_best__sukasuka_the_best} |
Attachments_495
神秘 .lisp 文件,居然还有 .lisp 玩的
最后给了注释:
1 | ;;;;flagis"&Dx16Y!x3xYDlShWbQ5hmzWf3EZly6h8UwD#d-1-&#WlDHJaxM5qAzlPP" |
大致浏览一下,发现这是一个简单的 base57 编码器
定义 alphabet 为 AB#DEd@f&hi!klmnLMw3^5678N}PF|HIxyz012JKYZab%Q{SUVWX-pqrs
len 为长度,即 57
定义函数 divmod 返回 values 包括商和余数
定义加密函数 encode:字符串倒序转化为 256 进制大整数,再转为 57 进制数,最后输出密文
编写解密函数
1 | alphabet = "AB#DEd@f&hi!klmnLMw3^5678N}PF|HIxyz012JKYZab%Q{SUVWX-pqrs" |
不对!
查看官方仓库发现题出错了
实际上注释应该是
1 | ;;;;flagis"6dsJ6lbHyhBXHfKnYdEddMU@0JKA^hlW0MMyx^3rpMM{{&Q-lnLrZBbbKJy" |
无敌了
flag
1 | moectf{woO0Oow_Y0u-ar3_th3_g0D_0f_LIIIISP!} |
Attachments_512
查壳

UPX 壳,去掉
1 | # ./upx.exe -d Art.exe |
1 | scanf("%s", Str1); |
Str1 为输入,然后诸位运算得到结果
1 | Str1[i - 1] ^= (Str1[i - 1] % 17 + Str1[i]) ^ 0x19; |
那么
1 | Str1[i] = (Str1[i - 1] ^ 0x19) - (Str1[i - 1] % 17) |
1 | _BOOL8 __fastcall sub_401550(const char *a1) |
这个为计算 SHA-256 哈希
具体过程为:sub_401B66 初始化,sub_401BE0 处理字符串每 64 个就执行 sub_401710 压缩,具体压缩过程略过,最后调用 sub_401C7D
和最终储存的 &unk_404040 比较
储存为
1 | 40f53ff2b934da894ed3ff1f6e76c68a30ed92bcf764e839d5b31196d0899287 |
那这个主函数主要是进行了一个逐字节的异或运算,然后计算 SHA-256 哈希,分别和预设值比较,反过来只需要通过异或值反推答案再计算 SHA-256 查看是否正确即可
1 | import hashlib |
得到 flag
1 | moectf{Art_i5_b14s7ing!!!!!} |
Attachments_511
apk 文件,解压看看,存在 classes*.dex,这是 Android 的 Dalvik 字节码文件,使用 jadx 反编译工具可以将其反编译为 Java 源代码。
直接搜索发现存在 flag

flag 为
1 | moectf{Andr01d_1s_so00oo_e@sy_t0_cr4ck!!!} |
Attachments_513
自定义 base64
1 | strcpy(base64, "1wX/yRrA4RfR2wj72Qv52x3L5qa="); |
1 | .rdata:0000000140009000 aAbcdefghijklmn db 'abcdefghijklmnopqrstuvwxyz0123456789+/ABCDEFGHIJKLMNOPQRSTUVWXYZ',0 |
编写脚本
1 | import base64 |
得到 flag
1 | moectf{qwqbase_qwq} |
Attachments_515
和上面那题 hash 差不多,不过这个是 SHA-1,还是自定义的
1 | from pathlib import Path |
得到 flag
1 | moectf{F1nd_th3_SEH_7hen_B1a5t_My_Fla9_and_Y0u_Can_Get_A_Cup_Of_Milk_Tea_From_YunZh1Jun} |
Attachments_518
NET 6,反编译得到:
1 | internal class D_flate |
这个 ASCII 储存 flag,编写脚本
1 | arr = [ |
得到 flag
1 | moectf{D_flate_is_C_sharp!} |