6.1 用 PyDbg 实现 Soft Hooking

第一个例子就是在应用层嗅探加密的网络传输。平时为了明白客户端和服务器之间的工 作流程,我们都会使用一个网络分析器列如 Wireshark。很不幸的是,Wireshark 获得的数据 经常都是加密过的,使得协议分析变得模糊。用 soft hooking 你能够在数据加密前或者接受 并解密后捕获它们。

实验目标就是最流行的开源浏览器 Mozilla Firefox。为了这次实验,我们假设 Firefox 是闭源的(否则会相当没趣)。我们的任务就是在 firefox.exe 进程加密数据前嗅探出数据。 现在最通用的网络加密协议就是 SSL,这次的主要目标就是解决她。

为了跟踪函数的调用(未加密数据的传递),需要使用记录模块间调用的技巧 http://forum.immunityinc.com/index.php?topic=35.0 )。现在首要解决的问题就是在什么地方 设置 hook。我们先假定将 hook 设置在 PR_Write 函数上(由 nspr4.dll.导出)。当这个函数被 执行的时候,堆栈[ ESP + 8 ]指向 ASCII 字符串(包含我们提交的但未加密的数据)。 ESP + 8 说明它是 PR_Write 的第二个函数,也是我们需要的,记录它,恢复程序。

首先打开 Firefox,输入网址 https://www.openrce.org/。一旦你接收了 SSl 证书,页面就 加载成功。接着 Immunity 附加到 firefox.exe 进程在 nspr4.PR_Write 设置断点。在 OpenRCE 网站右上角有一个登录窗口,设置用户名为 test 和密码 test,点击 Login 按钮。设置的断点 立刻被触发;再按 F9,断点再次触发。最后,你将在栈看到如下的内容:

[ESP + 8] => ASCII "username=test&password=test&remember_me=on"

很好,我们很清晰的看到了用户名和密码。但是如果从网络层看传输的数据,将是一堆 经过 SSL 加密的无意义的数据。这种方法不仅对 OpenRCE 有效。当你浏览任何一个需要传 输敏感数据的网站的时候,这些数据都将很容易的被捕捉到。现在再也不用手工操作调试器 去捕捉了,自动化才是王道。

在用 PyDbg 定义 soft hook 之前,需要先定义一个包含说有 hook 目标的容器。如下初始 化容器:

hooks = utils.hook_container()

使用 hook_container 类的 add()方法将我们定义的 hook 加进去。函数原型:

add( pydbg, address, num_arguments, func_entry_hook, func_exit_hook )

第一个参数设置成一个有效的 pydbg 目标,address 参数设置成要安装 hook 的地址, num_arguments 设置成传递给 hook 的参数。func_entry_hook 和 func_exit_hook 都是回调函数。 func_entry_hook 是 hook 被触发后立刻调用的,func_exit_hook 是被 hook 的函数将要退出之 前执行的。entry hook 用于得到函数的参数,exit hook 用于捕捉函数的返回值。

def entry_hook( dbg, args ):
    # Hook code here
    return DBG_CONTINUE

dbg 参数设置成有效的 pydbg 目标,args 接收一个列表,包含 hook 触发时接收到的参 数。

exit hook 回调函数有一点不同就是多了个 ret 参数,包含了函数的返回值(EAX 的值): def exit_hook( dbg, args, ret ):

# Hook code here
return DBG_CONTINUE

接下用实例看看如何用 entry hook 嗅探加密前的数据。

#firefox_hook.py from pydbg import *
from pydbg.defines import * 
import utils
import sys
dbg = pydbg()
found_firefox = False
# Let's set a global pattern that we can make the hook
# search for
pattern = "password"
# This is our entry hook callback function
# the argument we are interested in is args[1] 
def ssl_sniff( dbg, args ):
    # Now we read out the memory pointed to by the second argument
    # it is stored as an ASCII string, so we'll loop on a read until
    # we reach a NULL byte buffer = ""
    offset = 0
    while 1:
        byte = dbg.read_process_memory( args[1] + offset, 1 ) 
        if byte != "\x00":
            buffer += byte offset += 1 continue
        else:
            break
    if pattern in buffer:
        print "Pre-Encrypted: %s" % buffer 
    return DBG_CONTINUE
# Quick and dirty process enumeration to find firefox.exe 
for (pid, name) in dbg.enumerate_processes():
    if name.lower() == "firefox.exe": 
        found_firefox = True
        hooks = utils.hook_container() dbg.attach(pid)
        print "[*] Attaching to firefox.exe with PID: %d" % pid
        # Resolve the function address
        hook_address = dbg.func_resolve_debuggee("nspr4.dll","PR_Wri 
        if hook_address:
            # Add the hook to the container. We aren't interested
            # in using an exit callback, so we set it to None. 
            hooks.add( dbg, hook_address, 2, ssl_sniff, None )
            print "[*] nspr4.PR_Write hooked at: 0x%08x" % hook_address 
            break
        else:
            print "[*] Error: Couldn't resolve hook address." 
            sys.exit(-1)
if found_firefox:
    print "[*] Hooks set, continuing process." 
    dbg.run()
else:
    print "[*] Error: Couldn't find the firefox.exe process." 
    sys.exit(-1)

代码简洁明了:在 PR_Write 上设置 hook,当 hook 被触发的时候,我们尝试读出第二个 参数指向的字符串。如果有符合的数据就打印在命令行。启动一个新的 Firefox,接着运行 firefox_hook.py 脚本。重复之前的步骤,登录 https://www.openrce.org/,将看到输出如下:

[*] Attaching to firefox.exe with PID: 1344 
[*] nspr4.PR_Write hooked at: 0x601a2760 
[*] Hooks set, continuing process.
Pre-Encrypted: username=test&password=test&remember_me=on 
Pre-Encrypted: username=test&password=test&remember_me=on
Pre-Encrypted: username=jms&password=yeahright!&remember_me=on

Listing 6-1: How cool is that! 我们能看到未加密前的用户名密码

我们已经看到了 soft hook 的轻量级和强大能力。这种方法能被用于所有类型的调试和 逆向过程。在上面的例子中 soft hook 的工作还算正常,如果遇到有性能限制的函数调用时, 进程马上就会变得缓慢,行为异常,还可能崩溃。只是因为,当 INT3 被触发的时候,会将 执行权限交给我们的 hook 代码之后返回。这回花费非常多的事件,如果函数每秒钟执行数 千次。接下来让我们看看如何通过设置 hard hook 和 instrument low-level heap routines 以解决这个问题。