DownUnderCTF Reverse WP

这次比赛没有安卓,打的很开心(,给最后一题hard难度的swift逆向折磨到凌晨三点多才AK,总体难度还行,python那题比较有新意,Swift纯是语言本身拔高的难度。

rocky

main函数将输入文本进行md5,然后与s1比对,若比对成功则解密文本输出。

alt text

直接网站碰撞md5,得到”emergencycall911”

alt text

运行输入即可得到Flag。

alt text

DUCTF{In_the_land_of_cubicles_lined_in_gray_Where_the_clock_ticks_loud_by_the_light_of_day}

skippy

stone函数和decryptor函数存在无效指针读取操作。

alt text

alt text

nop对应的两处汇编即可。

alt text

alt text

运行,自解密得到Flag。

alt text

DUCTF{There_echoes_a_chorus_enending_and_wild_Laughter_and_gossip_unruly_and_piled}

godot

直接拖入Godot RE Tools(GDRE)分析,发现无法解包,应该是pck被加密了。

IDA拖入程序分析,搜索encrypt,发现以下文本,应该是打开加密数据的一个Log文本。

alt text

在他的交叉调用函数下面就可以找到Godot加密数据解密代码,byte_143F78540就是AES key,填入GDRE设置,再拖入程序即可解包。

alt text

成功解包。

alt text

发现素材中有个没有在游戏中出现过的角色。

alt text

且可以看到是在godot_sprite引入了这张素材。

alt text

在src/shop.gd代码中可以看到这边引入了godotSprite,且在帧函数实时判断当前时间是否等于程序初始化时间-1天,然后显示godotSprite角色。

alt text

运行游戏,将电脑设置日期往前一天,分钟也往前一分钟,等一会到时间,这边就会刷新出这个角色,但似乎无法交互。

alt text

地图有向上的闯关,猜测在上面有其他东西,这边直接用CE将Y坐标改到-1000,飞到上面发现上面有另一个商店和角色,可交互。

alt text

和这个角色对话完,再回到下面的商店,在红色角色附近点下E就会被传送到背景墙由Flag构成的地图。

alt text

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,sys
PASSWORD='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的代码调用。

alt text

Check1很简单,就是取第一个字节,判断xor 0x43是否等于11,那么就可以得到第一个字符是11^0x43=0x48,’H’。

目前输入:

H234567890ab

alt text

Check2这边一个callback是返回password[i-3]+3,ida那边的Check2实现也能看到调用了a1传进的函数回调,那么就可以通过这边是判断式子计算出两个字节,p[5]=’p’,p[6]=’h’。

目前输入:

H2345ph890ab

alt text

alt text

Check3,这边先从Python那边获取到输入的password,以word储存。

alt text

alt text

下面这边生成几个条件语句,要特别注意第二个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

alt text

alt text

alt text

Check4比较复杂,这个函数参数1传入密文,参数2是密文长度,参数3是解密返回数据,参数4是密钥,参数5是密钥长度,参数6是判断数值。

alt text

传入先使用传入密钥进行rc4解密密文,然后进行hash,最后判断是否等于参数6的hash值。

alt text

alt text

由于第一个hash判断只用密钥前2字节,这两个字节也是Check1和Check2通过输入字节计算出来的,只要过了这两个Check,那么这两个字节就会是正确的,也就能通过这个hash校验。

第一个解密出的文本:

alt text

第二个校验密钥长度是8,下标3、4、5、6都分别赋值了,下标2和7为默认的0xCC,这边赋值是通过p[1] p[2] p[3]三个未知字节进行计算,由于是未知的,就只能爆破,三字节爆破最后hash值为0x69FA99D。

alt text

爆破代码:

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{}; // rax
int v3{}; // r8d
char *v4{}; // rax
char *i{}; // rdx
__int64 v6{}; // rdx

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;
}
}
// ydr
return 0;
}

目前输入:

Hydr0ph110a3

这边是校验p[9],通过爆破就可以得到p[9]

alt text

爆破代码:

1
2
3
4
5
6
for (int i = 32; i < 127; i++)
{
if ((i ^ 0xcb & 0x64) == 0x2e)
printf("%c", i);
}
// p[9] = 'n'

目前输入:

Hydr0ph11na3

只剩下p[7] p[8]以及p[11],但是已经知道p[7]==p[8]且只能是12345678其中一个,p[11]==p[7]-2,那么手动输入试试就可以得到最终的正确password。

最后尝试发现分别就是1、1、3。

最终输入:

Hydr0ph11na3

alt text

DUCTF{the_problem_with_dynamic_languages_is_you_cant_c_types}

SwiftPasswordManager-ClickMe

根据题目描述,可知窗口界面有一个Flag按钮,但是无法点击,应该是初始化控件的时候被禁用了。

尝试字符串搜索Flag,但是没搜到,猜测可能是因为4字节字符串比较短,直接被优化成4字节立即数在汇编里面,直接搜Flag字符串的字节,可以搜索到,反编译界面这边可以看到是Flag字符串。

alt text

alt text

上图可以看到下面有个,从命名不难猜出是禁用按钮的函数。

View.disabled(_:)(1, &type metadata for Button, &protocol witness table for Button);

断点函数调用处,写断点命令,将edi也就是参数1改成0即可。

alt text

调试运行程序,虽然Flag按钮样式还是禁用的,但是可以点击,点击就弹出Flag。

alt text

DUCTF{just_because_the_button_is_greyed_out_doesnt_mean_you_cant_use_it}

SwiftPasswordManager-LoadMe

点击Load按钮,提示没有按钮事件实现,那么只能从Save按钮事件逆向出保存的文件结构,然后写解密,利用题目提供的”DUCTF2025!”密码进行解密题目给的加密文件。

alt text

函数可以搜到saveFile的函数实现。

alt text

里面调用的一个函数就是核心函数,加密数据导出到文件的。

alt text

由于函数代码量大难读懂,直接将整个函数丢给Gemini2.5 Pro进行分析,得到以下文件结构。

字节数 描述
4 “SMP1”
4 0x01 0x00 0x00 0x00
2 Salt长度
32 Salt数据
2 Nonce长度
可变 Nonce数据
2 Tag长度
可变 Tag数据
4 密文长度
可变 密文

大致代码流程:

  1. 获取32个随机数字节
  2. 将32个随机数字节作为Salt对密码进行0xAAAA次hash
  3. 生成Nonce、Tag,将hash后的数据当作AES密钥。
  4. 将Title、Username、Password、Notes进行AES GCM加密
  5. 按以上表格结构写到文件。

那么从加密文件读取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 sys
import struct
import hashlib
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.exceptions import InvalidTag

PASSWORD = "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)

# b'\x01\x00\x00\x00\x1a\x1c\x86\xd2\x7f\xa8EQ\xbe\xd1\xcc\n\x04e\x8f<\x08\x00\x00\x00computer\x04\x00\x00\x00user\r\x00\x00\x00cool password7\x00\x00\x00DUCTF{the_password_is_cool_but_the_flag_is_even_cooler}\xd4\x82^h\x00\x00\x00\x00\xe7\x82^h\x00\x00\x00\x00'

DUCTF{the_password_is_cool_but_the_flag_is_even_cooler}

SwiftPasswordManager-CrackMe

题目说如果输入一个好的Password,就会获得一个Flag,那么应该就是在编辑框输入事件做了什么校验。

函数窗口可以搜到onChange事件,查看交叉调用发现就这一处调用了,参数1函数就是OnChange事件的回调函数。

alt text

alt text

开头是检测输入的字符串的Prefix和Suffix,也就是前缀和后缀是否为”DU”和”.}”。

alt text

下面有多次单字节校验,以这个为例子。

先调用_sSS5index_8offsetBySS5IndexVAD_SitF获取一个实例,参数2的2指的是输入字符串的下标2,然后调用_sSSySJSS5IndexVcig获取字符串下标2的字节,然后xor ‘C’,要让条件不满足,也就是下标2的字符要是’C’,这样就得到了一个已知明文字节。

alt text

从前面的这几个相同的判断代码,可以得到完整的开头和结尾,开头是”DUCTF{“,结尾是”.}”。

下面接着判断去掉前缀”DUCTF{“和后缀”.}”剩下的字符串长度是否为0x1D。

那么先构造一个字符串方便动调观察数据。

DUCTF{1234567890abcdefghijklmnopqrs.}

alt text

获取去掉前后缀的字符串,获取最后一位是否为”.”。

alt text

然后是两次hash判断,第一次是将去前后缀字符串进行hash,判断低32位,第二次是将去前后缀字符串翻转,再进行hash,判断高32位,两次判断校验,这边目前就只能将这两个校验绕过,因为不知道完整的输入。

alt text

alt text

都在cmp处将对应比对值改成目标值就可以绕过。

alt text

alt text

这边将字符串重新打乱,下面通过下标取字符的时候要注意,动调看实际取出数据是多少,然后去看对应输入的原字符串下标是多少。

alt text

下面跟着的都是差不多的xor比对,有单字节有多字节,都是一样的做法得到对应位置的明文字节。

有个特殊的,是方程组,下面注释的下标是对应原完整字符串的下标,包括前后缀的,使用Z3求解即可得到三个字符。

alt text

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]四个字符串,两个字符一组进行一次循环。

alt text

这边第一处取p[i+1]然后-1,第二处取p[i]然后+1。

alt text

alt text

将两个数据先后往后加入字节,如:v212=0x31,v282=0x71,那么最后往后添加得到0x3171。

alt text

一共四个字节,分成两组,最后得到四字节,这边进行比较,那么按+1-1就可以从这边还原出原来的四个明文字节。

“1q”是第一组,”e^”是第二组,分别-1+1得到”0r”,”d_”,合起来就是”0rd_”,对应原完整字符串p[22] p[23] p[24] p[25]四个字节。

alt text

最后一个校验还是z3,同样写脚本解方程即可。

alt text

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校验,可以根据这个点来爆破出剩下的两个字节。

alt text

爆破脚本

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); }

// String.h()函数
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");

// 第二次是翻转后hash
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);
}
}
}
// DUCTF{cho05iNg_A_p4s5W0rd_15_h4Rd...}
return 0;
}

DUCTF{cho05iNg_A_p4s5W0rd_15_h4Rd…}