DownUnderCTF Reverse WP 这次比赛没有安卓,打的很开心(,给最后一题hard难度的swift逆向折磨到凌晨三点多才AK,总体难度还行,python那题比较有新意,Swift纯是语言本身拔高的难度。
rocky main函数将输入文本进行md5,然后与s1比对,若比对成功则解密文本输出。
直接网站碰撞md5,得到”emergencycall911”
运行输入即可得到Flag。
DUCTF{In_the_land_of_cubicles_lined_in_gray_Where_the_clock_ticks_loud_by_the_light_of_day}
skippy stone函数和decryptor函数存在无效指针读取操作。
nop对应的两处汇编即可。
运行,自解密得到Flag。
DUCTF{There_echoes_a_chorus_enending_and_wild_Laughter_and_gossip_unruly_and_piled}
godot 直接拖入Godot RE Tools(GDRE)分析,发现无法解包,应该是pck被加密了。
IDA拖入程序分析,搜索encrypt,发现以下文本,应该是打开加密数据的一个Log文本。
在他的交叉调用函数下面就可以找到Godot加密数据解密代码,byte_143F78540就是AES key,填入GDRE设置,再拖入程序即可解包。
成功解包。
发现素材中有个没有在游戏中出现过的角色。
且可以看到是在godot_sprite引入了这张素材。
在src/shop.gd代码中可以看到这边引入了godotSprite,且在帧函数实时判断当前时间是否等于程序初始化时间-1天,然后显示godotSprite角色。
运行游戏,将电脑设置日期往前一天,分钟也往前一分钟,等一会到时间,这边就会刷新出这个角色,但似乎无法交互。
地图有向上的闯关,猜测在上面有其他东西,这边直接用CE将Y坐标改到-1000,飞到上面发现上面有另一个商店和角色,可交互。
和这个角色对话完,再回到下面的商店,在红色角色附近点下E就会被传送到背景墙由Flag构成的地图。
DUCTF{THE_BOY_WILL_NEVER_REMEMBER}
bilingual python代码从DATA加载了一段Base64后的文件,解密输出到”hello.bin”,然后要求输入password,初步判断长度是否为12,通过调用hello.bin的四个Check函数校验password,校验成功则使用password进行Flag解密。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 DATA="..." import argparse,base64,ctypes,zlib,pathlib,sysPASSWORD='cheese' FLAG='jqsD0um75+TyJR3z0GbHwBQ+PLIdSJ+rojVscEL4IYkCOZ6+a5H1duhcq+Ub9Oa+ZWKuL703' KEY='68592cb91784620be98eca41f825260c' HELPER=None def decrypt_flag (password ):A='utf-8' ;flag=bytearray (base64.b64decode(FLAG));buffer=(ctypes.c_byte*len (flag)).from_buffer(flag);key=ctypes.create_string_buffer(password.encode(A));result=get_helper().Decrypt(key,len (key)-1 ,buffer,len (buffer));return flag.decode(A)def get_helper (): global HELPER if HELPER:return HELPER data=globals ().get('DATA' ) if data: dll_path=pathlib.Path(__file__).parent/'hello.bin' if not dll_path.is_file(): with open (dll_path,'wb' )as dll_file:dll_file.write(zlib.decompress(base64.b64decode(data))) HELPER=ctypes.cdll.LoadLibrary(dll_path) else :0 return HELPER def check_three (password ):return check_ex(password,'Check3' )def check_four (password ):return check_ex(password,'Check4' )def check_ex (password,func ): GetIntCallbackFn=ctypes.CFUNCTYPE(ctypes.c_int,ctypes.c_wchar_p) class CallbackTable (ctypes.Structure):_fields_=[('E' ,GetIntCallbackFn)] @GetIntCallbackFn def eval_int (v ):return int (eval (v)) table=CallbackTable(E=eval_int);helper=get_helper();helper[func].argtypes=[ctypes.POINTER(CallbackTable)];helper[func].restype=ctypes.c_int;return helper[func](ctypes.byref(table)) def check_two (password ): @ctypes.CFUNCTYPE(ctypes.c_int,ctypes.c_int ) def callback (i ):return ord (password[i-3 ])+3 return get_helper().Check2(callback) def check_one (password ): if len (password)!=12 :return False return get_helper().Check1(password)!=0 def check_password (password ): global PASSWORD;PASSWORD=password;checks=[check_one,check_two,check_three,check_four];result=True for check in checks:result=result and check(password) return result def main (): parser=argparse.ArgumentParser(description='CTF Challenge' );parser.add_argument('password' ,help ='Enter the password' );args=parser.parse_args() if check_password(args.password):flag=decrypt_flag(args.password);print ('Correct! The flag is DUCTF{%s}' %flag);return 0 else :print ('That is not correct' );return 1 if __name__=='__main__' :sys.exit(main())
直接改代码在main最开始调用get_helper,得到hello.bin文件,进行IDA分析,动调设置这样就可以调试python的代码调用。
Check1很简单,就是取第一个字节,判断xor 0x43是否等于11,那么就可以得到第一个字符是11^0x43=0x48,’H’。
目前输入:
H234567890ab
Check2这边一个callback是返回password[i-3]+3,ida那边的Check2实现也能看到调用了a1传进的函数回调,那么就可以通过这边是判断式子计算出两个字节,p[5]=’p’,p[6]=’h’。
目前输入:
H2345ph890ab
Check3,这边先从Python那边获取到输入的password,以word储存。
下面这边生成几个条件语句,要特别注意第二个and后面那个减数是%c不是%d,所以98 - 5的5是’5’,也就是p[4]字节。
从数据可以总结以下条件:
p[8] + 2 == p[11] & p[7] == p[8] & (p[11] - (p[4] - ‘0’)) == p[11]
p[7]>’0’ & p[7]<’9’
所以p[4]=’0’,p[7]==p[8]==p[11]-2,p[7] p[8]都在’0’到’9’范围内。
目前输入(p[7] p[8] p[11]随便取一组符合条件的先):
H2340ph110a3
Check4比较复杂,这个函数参数1传入密文,参数2是密文长度,参数3是解密返回数据,参数4是密钥,参数5是密钥长度,参数6是判断数值。
传入先使用传入密钥进行rc4解密密文,然后进行hash,最后判断是否等于参数6的hash值。
由于第一个hash判断只用密钥前2字节,这两个字节也是Check1和Check2通过输入字节计算出来的,只要过了这两个Check,那么这两个字节就会是正确的,也就能通过这个hash校验。
第一个解密出的文本:
第二个校验密钥长度是8,下标3、4、5、6都分别赋值了,下标2和7为默认的0xCC,这边赋值是通过p[1] p[2] p[3]三个未知字节进行计算,由于是未知的,就只能爆破,三字节爆破最后hash值为0x69FA99D。
爆破代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 #include <iostream> #include <windows.h> unsigned char sbox[256 ] = {0 };void swap (unsigned char *a, unsigned char *b) { unsigned char tmp = *a; *a = *b; *b = tmp; } void init_sbox (unsigned char key[]) { for (unsigned int i = 0 ; i < 256 ; i++) sbox[i] = i; unsigned int keyLen = strlen ((char *)key); unsigned char Ttable[256 ] = {0 }; for (int i = 0 ; i < 256 ; i++) Ttable[i] = key[i % keyLen]; for (int j = 0 , i = 0 ; i < 256 ; i++) { j = (j + sbox[i] + Ttable[i]) % 256 ; swap (sbox + i, sbox + j); } } void RC4 (unsigned char data[], unsigned char key[]) { unsigned char k, i = 0 , j = 0 , t; init_sbox (key); unsigned int dataLen = 32 ; for (unsigned h = 0 ; h < dataLen; h++) { i = (i + 1 ) % 256 ; j = (j + sbox[i]) % 256 ; swap (sbox + i, sbox + j); t = (sbox[i] + sbox[j]) % 256 ; k = sbox[t]; data[h] ^= k; } } __int64 __fastcall sub_7FF9EFF01630 (char *a1, __int64 a2) { __int64 result{}; int v3{}; char *v4{}; char *i{}; __int64 v6{}; v4 = &a1[2 * a2]; for (i = a1; *(WORD *)i; i += 2 ) { if (i >= v4) break ; } v6 = 2 * ((i - a1) >> 1 ); for (result = 5381 ; v6; --v6) { v3 = *a1++; result = v3 ^ (unsigned int )(33 * result); } return result; } int main () { unsigned char key[9 ] = { 0x7A , 0x6D , 0xCC , 0xCC , 0xCC , 0xCC , 0xCC , 0xCC , 0x00 }; for (int c = 0 ; c < 0xffffff ; c++) { unsigned char data2[32 ] = { 0xD0 , 0xE9 , 0xC1 , 0x5A , 0x9E , 0x0C , 0x28 , 0x31 , 0x58 , 0x24 , 0x5D , 0x68 , 0x54 , 0x8D , 0x6F , 0xE7 , 0xF6 , 0xDB , 0xD7 , 0xE5 , 0xC0 , 0x4B , 0x28 , 0x46 , 0xE7 , 0xA4 , 0x7E , 0xCD , 0x07 , 0xF8 , 0xF4 , 0x41 }; key[4 ] = c & 0xff ; key[5 ] = (c >> 8 ) & 0xff ; key[3 ] = (0x1ACB >> 3 ) ^ 0x36 ; key[6 ] = (c & 0xff ) ^ ((c >> 8 ) & 0xff ) ^ ((c >> 16 ) & 0xff ) ^ 0x10 ; RC4 (data2, key); int result = sub_7FF9EFF01630 ((char *)data2, 32 >> 1 ); if (result == 0x69FA99D ) { printf ("%c%c%c\n" , c & 0xff , (c >> 8 ) & 0xff , (c >> 16 ) & 0xff ); break ; } } return 0 ; }
目前输入:
Hydr0ph110a3
这边是校验p[9],通过爆破就可以得到p[9]
爆破代码:
1 2 3 4 5 6 for (int i = 32 ; i < 127 ; i++){ if ((i ^ 0xcb & 0x64 ) == 0x2e ) printf ("%c" , i); }
目前输入:
Hydr0ph11na3
只剩下p[7] p[8]以及p[11],但是已经知道p[7]==p[8]且只能是12345678其中一个,p[11]==p[7]-2,那么手动输入试试就可以得到最终的正确password。
最后尝试发现分别就是1、1、3。
最终输入:
Hydr0ph11na3
DUCTF{the_problem_with_dynamic_languages_is_you_cant_c_types}
SwiftPasswordManager-ClickMe 根据题目描述,可知窗口界面有一个Flag按钮,但是无法点击,应该是初始化控件的时候被禁用了。
尝试字符串搜索Flag,但是没搜到,猜测可能是因为4字节字符串比较短,直接被优化成4字节立即数在汇编里面,直接搜Flag字符串的字节,可以搜索到,反编译界面这边可以看到是Flag字符串。
上图可以看到下面有个,从命名不难猜出是禁用按钮的函数。
View.disabled(_:)(1, &type metadata for Button, &protocol witness table for Button);
断点函数调用处,写断点命令,将edi也就是参数1改成0即可。
调试运行程序,虽然Flag按钮样式还是禁用的,但是可以点击,点击就弹出Flag。
DUCTF{just_because_the_button_is_greyed_out_doesnt_mean_you_cant_use_it}
SwiftPasswordManager-LoadMe 点击Load按钮,提示没有按钮事件实现,那么只能从Save按钮事件逆向出保存的文件结构,然后写解密,利用题目提供的”DUCTF2025!”密码进行解密题目给的加密文件。
函数可以搜到saveFile的函数实现。
里面调用的一个函数就是核心函数,加密数据导出到文件的。
由于函数代码量大难读懂,直接将整个函数丢给Gemini2.5 Pro进行分析,得到以下文件结构。
字节数
描述
4
“SMP1”
4
0x01 0x00 0x00 0x00
2
Salt长度
32
Salt数据
2
Nonce长度
可变
Nonce数据
2
Tag长度
可变
Tag数据
4
密文长度
可变
密文
大致代码流程:
获取32个随机数字节
将32个随机数字节作为Salt对密码进行0xAAAA次hash
生成Nonce、Tag,将hash后的数据当作AES密钥。
将Title、Username、Password、Notes进行AES GCM加密
按以上表格结构写到文件。
那么从加密文件读取Salt、Nonce、Tag、密文,就可以进行密钥生成+AES解密,得到明文。
解密代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 import sysimport structimport hashlibfrom cryptography.hazmat.primitives.ciphers.aead import AESGCMfrom cryptography.exceptions import InvalidTagPASSWORD = "DUCTF2025!" def derive_key (password: bytes , salt: bytes ) -> bytes : derived_key = password for i in range (0xAAAA ): hasher = hashlib.sha256() hasher.update(derived_key) hasher.update(salt) derived_key = hasher.digest() return derived_key def decrypt_file (file_path: str ): with open (file_path, 'rb' ) as f: magic_number = f.read(4 ) magic_number2 = struct.unpack('<HH' , f.read(4 )) salt_len, = struct.unpack('<H' , f.read(2 )) salt = f.read(salt_len) nonce_len, = struct.unpack('<H' , f.read(2 )) nonce = f.read(nonce_len) tag_len, = struct.unpack('<H' , f.read(2 )) tag = f.read(tag_len) ciphertext_len, = struct.unpack('<I' , f.read(4 )) ciphertext = f.read(ciphertext_len) password_bytes = PASSWORD.encode('utf-8' ) encryption_key = derive_key(password_bytes, salt) aesgcm = AESGCM(encryption_key) encrypted_data_with_tag = ciphertext + tag decrypted_data = aesgcm.decrypt(nonce, encrypted_data_with_tag, None ) print (decrypted_data) if __name__ == "__main__" : encrypted_file = r"passwords.spm" decrypt_file(encrypted_file)
DUCTF{the_password_is_cool_but_the_flag_is_even_cooler}
SwiftPasswordManager-CrackMe 题目说如果输入一个好的Password,就会获得一个Flag,那么应该就是在编辑框输入事件做了什么校验。
函数窗口可以搜到onChange事件,查看交叉调用发现就这一处调用了,参数1函数就是OnChange事件的回调函数。
开头是检测输入的字符串的Prefix和Suffix,也就是前缀和后缀是否为”DU”和”.}”。
下面有多次单字节校验,以这个为例子。
先调用_sSS5index_8offsetBySS5IndexVAD_SitF获取一个实例,参数2的2指的是输入字符串的下标2,然后调用_sSSySJSS5IndexVcig获取字符串下标2的字节,然后xor ‘C’,要让条件不满足,也就是下标2的字符要是’C’,这样就得到了一个已知明文字节。
从前面的这几个相同的判断代码,可以得到完整的开头和结尾,开头是”DUCTF{“,结尾是”.}”。
下面接着判断去掉前缀”DUCTF{“和后缀”.}”剩下的字符串长度是否为0x1D。
那么先构造一个字符串方便动调观察数据。
DUCTF{1234567890abcdefghijklmnopqrs.}
获取去掉前后缀的字符串,获取最后一位是否为”.”。
然后是两次hash判断,第一次是将去前后缀字符串进行hash,判断低32位,第二次是将去前后缀字符串翻转,再进行hash,判断高32位,两次判断校验,这边目前就只能将这两个校验绕过,因为不知道完整的输入。
都在cmp处将对应比对值改成目标值就可以绕过。
这边将字符串重新打乱,下面通过下标取字符的时候要注意,动调看实际取出数据是多少,然后去看对应输入的原字符串下标是多少。
下面跟着的都是差不多的xor比对,有单字节有多字节,都是一样的做法得到对应位置的明文字节。
有个特殊的,是方程组,下面注释的下标是对应原完整字符串的下标,包括前后缀的,使用Z3求解即可得到三个字符。
z3脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from z3 import *p20 = Int('p20' ) p21 = Int('p21' ) p30 = Int('p30' ) s = Solver() s.add(p20 + 2 *p21 + 3 *p30 == 383 ) s.add(5 *p21 + 4 *p20 + 6 *p30 == 959 ) s.add(9 *(p20 + p30) + 8 *p21 == 1641 ) if s.check() == sat: m = s.model() print (f"p20 = {m[p20]} " ) print (f"p21 = {m[p21]} " ) print (f"p30 = {m[p30]} " ) else : print ("No solution found." )
下面有个大循环,这边获取第i个和i+1个下标,i从0开始,步长为2,但对应的字符串不是原始的,也是打乱后的,所以还是动调看到底取的字符是对应原字符串的哪个下标。
动调发现是取原完整字符串p[22] p[23] p[24] p[25]四个字符串,两个字符一组进行一次循环。
这边第一处取p[i+1]然后-1,第二处取p[i]然后+1。
将两个数据先后往后加入字节,如:v212=0x31,v282=0x71,那么最后往后添加得到0x3171。
一共四个字节,分成两组,最后得到四字节,这边进行比较,那么按+1-1就可以从这边还原出原来的四个明文字节。
“1q”是第一组,”e^”是第二组,分别-1+1得到”0r”,”d_”,合起来就是”0rd_”,对应原完整字符串p[22] p[23] p[24] p[25]四个字节。
最后一个校验还是z3,同样写脚本解方程即可。
z3脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from z3 import *p8 = Int('p8' ) p12 = Int('p12' ) p14 = Int('p14' ) s = Solver() s.add(p8 + 2 *p12 + 3 *p14 == 552 ) s.add(5 *p12 + 4 *p8 + 6 *p14 == 1404 ) s.add(6 *p8 + 8 *p12 + 9 *p14 == 2145 ) if s.check() == sat: m = s.model() print (f"p8 = {m[p8]} " ) print (f"p12 = {m[p12]} " ) print (f"p14 = {m[p14]} " ) else : print ("No solution found." )
得到”oN_”三个明文字节,根据上面所有校验,目前可以得到以下字符串:
DUCTF{cho05iNg_?_p4s5W0rd_15_h4?d…}
问号是未知字节,这两个未知的字节在全程没有被校验过,但是开头有对去前后缀字符串进行的两次hash校验,可以根据这个点来爆破出剩下的两个字节。
爆破脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 #include <iostream> #include <windows.h> template <class T >T __ROL__(T value, int count) { const uint32_t nbits = sizeof (T) * 8 ; if (count > 0 ) { count %= nbits; T high = value >> (nbits - count); if (T (-1 ) < 0 ) high &= ~((T (-1 ) << count)); value <<= count; value |= high; } else { count = -count % nbits; T low = value << (nbits - count); value >>= count; value |= low; } return value; } inline DWORD64 __ROL8__(DWORD64 value, int count) { return __ROL__((DWORD64)value, count); }DWORD64 hash (uint8_t *input) { DWORD64 v0 = 0x811C9DC5LL ; DWORD64 i{}; uint8_t v2 = input[0 ]; int c{}; for (i = 16777619 ; c < 29 ; v2 = input[c]) { auto v5 = c + 1 ; v0 = v2 ^ __ROL8__(v0, 7 ); i = 33 * i + v2 * v5; c++; } return v0 ^ __ROL8__(i, 32 ); } int main () { for (int i = 0 ; i < 0xffff ; i++) { auto a1 = i & 0xff , a2 = (i >> 8 ) & 0xff ; uint8_t input1[] = "cho05iNg_?_p4s5W0rd_15_h4?d.." ; input1[9 ] = a1; input1[25 ] = a2; auto hash_result1 = hash (input1) & 0xffffffff ; if (hash_result1 == 0xD890BAB5 ) { printf ("hash1 correct!\n" ); uint8_t input2[] = "..dz4h_51_dr0W5s4p_j_gNi50ohc" ; input2[19 ] = a1; input2[3 ] = a2; auto hash_result2 = hash (input2) >> 0x20 ; if (hash_result2 == 0x80DD5386 ) { printf ("hash2 correct!\n" ); printf ("DUCTF{%.29s.}\n" , input1); } } } return 0 ; }
DUCTF{cho05iNg_A_p4s5W0rd_15_h4Rd…}