4.3 进程快照

PyDbg 提供了一个非常酷的功能,进程快照。使用进程快照的时候,我们就能够冰冻进 程,获取进程的内存数据。以后我们想要让进程回到这个时刻的状态,只要使用这个时刻的 快照就行了。

4.3.1 获得进程快照

第一步,在一个准确的时间获得一份目标进程的精确快照。为了使得快照足够精确,需 要得到所有线程以及 CPU 上下文,还有进程的整个内存。将这些数据存储起来,下次我们 需要恢复快照的时候就能用的到。

为了防止在获取快照的时候,进程的数据或者状态被修改,需要将进程挂起来,这个任 务由 suspend_all_threads()完成。挂起进程之后,可以用 process_snapshot()获取快照。快照完 成之后,用 resume_all_threads()恢复挂起的进程,让程序继续执行。当某个时刻我们需要将 进程恢复到从前的状态,简单的 process_restore()就行了。这看起来是不是太简单了?

现在新建个 snapshot.py 试验下,代码的功能就是我们输入"snap"的时候创建一个快照, 输入"restore"的时候将进程恢复到快照时的状态。

#snapshot.py
from pydbg import *
from pydbg.defines import * 
import threading
import time 
import sys
class snapshotter(object):
    def init (self,exe_path):
        self.exe_path = exe_path
        self.pid = None
        self.dbg = None
        self.running = True
        # Start the debugger thread, and loop until it sets the PID
        # of our target process
        pydbg_thread = threading.Thread(target=self.start_debugger) pydbg_thread.setDaemon(0)
        pydbg_thread.start()
        while self.pid == None: 
            time.sleep(1)
        # We now have a PID and the target is running; let's get a
        # second thread running to do the snapshots
        monitor_thread = threading.Thread(target=self.monitor_debugger) monitor_thread.setDaemon(0)
        monitor_thread.start() 
    def monitor_debugger(self):
        while self.running == True:
            input = raw_input("Enter: 'snap','restore' or 'quit'") 
            input = input.lower().strip()
            if input == "quit":
                print "[*] Exiting the snapshotter." 
                self.running = False self.dbg.terminate_process()
            elif input == "snap":
                print "[*] Suspending all threads." self.dbg.suspend_all_threads()
                print "[*] Obtaining snapshot." 
                self.dbg.process_snapshot()
                print "[*] Resuming operation." 
                self.dbg.resume_all_threads()
            elif input == "restore":
                print "[*] Suspending all threads." self.dbg.suspend_all_threads()
                print "[*] Restoring snapshot." 
                self.dbg.process_restore()
                print "[*] Resuming operation." 
                self.dbg.resume_all_threads()
    def start_debugger(self): self.dbg = pydbg()
        pid = self.dbg.load(self.exe_path) 
        self.pid = self.dbg.pid
        self.dbg.run() 
        exe_path = "C:\\WINDOWS\\System32\\calc.exe" 
snapshotter(exe_path)

那么第一步就是在调试器内部创建一个新线程,并用此启动目标进程。通过使用分开的线程,就能将被调试的进程和调试器的操作分开,这样我们输入不同的快照命令进行操作的 时候,就不用强迫被调试进程暂停。当创建新线程的代码返回了有效的 PID,我们就创建另 一个线程,接受我们输入的调试命令。之后这个线程根据我们输入的命令决定不同的操作(快 照,恢复快照,结束程序)。

我们之所以选择计算器作为例子,是因为通过操作图形界面 ,可以更清晰的看到,快 照的作用。先在计算器里输入一些数据,然后在终端里输入"snap"进行快照,之后再在计算器 里进行别的操作。最后就当的输入"restore",你将看到,计算器回到了最初时快照的状态。 使用这种方法我们能够将进程恢复到任意我们希望的状态。

现在让我们将所有的新学的 PyDbg 知识,创建一个 fuzz 辅助工具,帮助我们找到软件 的漏洞,并自动处理奔溃事件。

4.3.2 组合代码

我们已经介绍了一些 PyDbg 非常有用的功能,接下来要构建一个工具用来根除应用程 序中出现的可利用的漏洞。在我们平常的开发过程中,有些函数是非常危险的,很容易造成 缓冲区溢出,字符串问题,以及内存出错,对这些函数需要重点关注。

工具将定位于危险函数,并跟踪它们的调用。当我们认为函数被危险调用了,就将 4 堆栈中的 4 个参数接触引用,弹出栈,并且在函数产生溢出之前对进程快照。如果这次访问 违例了,我们的脚本将把进程恢复到,函数被调用之前的快照。并从这开始,单步执行,同 时 反 汇 编 每 个 执 行 的 代 码 , 直 到 我 们 也 抛 出 了 访 问 违 例 , 或 者 执 行 完 了 MAX_INSTRUCTIONS(我们要监视的代码数量)。无论什么时候当你看到一个危险的函数 在处理你输入的数据的时候,尝试操作数据 crash 数据都似乎值得。这是创造出我们的漏洞 利用程序的第一步。

开动代码,建立 danger_track.py,输入下面的代码。

#danger_track.py
from pydbg import *
from pydbg.defines import * 
import utils
# This is the maximum number of instructions we will log
# after an access violation MAX_INSTRUCTIONS = 10
# This is far from an exhaustive list; add more for bonus points dangerous_functions = {
    "strcpy" : "msvcrt.dll",
    "strncpy" : "msvcrt.dll",
    "sprintf" : "msvcrt.dll", "vsprintf": "msvcrt.dll"
}
dangerous_functions_resolved = {} 
crash_encountered = False
instruction_count = 0 
def danger_handler(dbg):
    # We want to print out the contents of the stack; that's about it
    # Generally there are only going to be a few parameters, so we will
    # take everything from ESP to ESP+20, which should give us enough
    # information to determine if we own any of the data esp_offset = 0
    print "[*] Hit %s" % dangerous_functions_resolved[dbg.context.Eip]
    print "================================================================="
    while esp_offset <= 20:
        parameter = dbg.smart_dereference(dbg.context.Esp + esp_offset)
        print "[ESP + %d] => %s" % (esp_offset, parameter)
        esp_offset += 4
    print "=================================================================\n
    dbg.suspend_all_threads() 
    dbg.process_snapshot() 
    dbg.resume_all_threads()
    return DBG_CONTINUE
def access_violation_handler(dbg): 
    global crash_encountered
    # Something bad happened, which means something good happened :)
    # Let's handle the access violation and then restore the process
    # back to the last dangerous function that was called 
    if dbg.dbg.u.Exception.dwFirstChance:
        return DBG_EXCEPTION_NOT_HANDLED
    crash_bin = utils.crash_binning.crash_binning() 
    crash_bin.record_crash(dbg)
    print crash_bin.crash_synopsis()
    if crash_encountered == False: 
        dbg.suspend_all_threads() 
        dbg.process_restore() 
        crash_encountered = True
    # We flag each thread to single step
        for thread_id in dbg.enumerate_threads():
            print "[*] Setting single step for thread: 0x%08x" % thread_id 
        h_thread = dbg.open_thread(thread_id)
        dbg.single_step(True, h_thread) 
        dbg.close_handle(h_thread)
    # Now resume execution, which will pass control to our
    # single step handler 
    dbg.resume_all_threads() 
    return DBG_CONTINUE
    else:
        dbg.terminate_process()
    return DBG_EXCEPTION_NOT_HANDLED
def single_step_handler(dbg): 
    global instruction_count 
    global crash_encountered
    if crash_encountered:
        if instruction_count == MAX_INSTRUCTIONS: 
            dbg.single_step(False)
            return DBG_CONTINUE
        else:
            # Disassemble this instruction
            instruction = dbg.disasm(dbg.context.Eip)
            print "#%d\t0x%08x : %s" % (instruction_count,dbg.context.Eip, instruction)
            instruction_count += 1 
            dbg.single_step(True)
            return DBG_CONTINUE
dbg = pydbg()
pid = int(raw_input("Enter the PID you wish to monitor: "))
dbg.attach(pid)
# Track down all of the dangerous functions and set breakpoints 
for func in dangerous_functions.keys():
    func_address = dbg.func_resolve( dangerous_functions[func],func )
    print "[*] Resolved breakpoint: %s -> 0x%08x" % ( func, func_address ) 
    dbg.bp_set( func_address, handler = danger_handler ) 
    dangerous_functions_resolved[func_address] = func
dbg.set_callback( EXCEPTION_ACCESS_VIOLATION, access_violation_handler ) 
dbg.set_callback( EXCEPTION_SINGLE_STEP, single_step_handler )
dbg.run()

通过之前对 PyDbg 的诸多讲解,这段代码应该看起来不那么难了吧。测试这个脚本的 最好方法,就是运行一个有漏洞价格的程序,然后让脚本附加到进程,和程序交互,尝试 crash 程序。

我们已经对 PyDbg 有了一定的了解,不过这只是它强大功能的一部分,还有更多的东 西,需要你自己去挖掘。再好的东西也满足不了那些"懒惰"的 hacker。PyDbg 固然强大,方 便的扩展,自动化调试。不过每次要完成任务的时候,都要自己动手编写代码。接下来介绍 的 Immunity Debugger 弥补了这点,完美的结合了图形化调试和脚本调试。它能让你更懒, 哈。让我们继续。