项目历史
https://github.com/r0ysue/r0capture
是基于谷歌的项目而来的
https://github.com/google/ssl_logger
可以先看看这个项目,比较纯粹,就一个python文件。
源码分析
r0capture.py 分析
if __name__ == "__main__":
show_banner()
class ArgParser(argparse.ArgumentParser):
...
parser = ArgParser()
args = parser.add_argument_group("Arguments")
args.add_argument("-pcap", '-p', metavar="<path>", required=False,
help="Name of PCAP file to write")
...
parsed = parser.parse_args()
ssl_log(
int(parsed.process) if parsed.process.isdigit() else parsed.process,
parsed.pcap,
parsed.host,
parsed.verbose,
isUsb=parsed.isUsb,
isSpawn=parsed.isSpawn,
ssllib=parsed.ssl,
wait=parsed.wait
)
使用 ArgumentParser 来做命令行参数的解析。最后调用 ssl_log 函数。
ssl_log 分下面几步:
-
连接设备
if isUsb:
try:
device = frida.get_usb_device()
except:
device = frida.get_remote_device()
else:
if host:
manager = frida.get_device_manager()
device = manager.add_remote_device(host)
else:
device = frida.get_local_device()
if isSpawn:
pid = device.spawn([process])
time.sleep(1)
session = device.attach(pid)
time.sleep(1)
device.resume(pid)
else:
print("attach")
session = device.attach(process)
if wait > 0:
print(f"wait for {wait} seconds")
time.sleep(wait)
-
如果命令参数有 pcap,就创建 pcap 文件,将hook到的数据储存进去。这里只是先写了一个文件头。pcap 的格式可看:
https://blog.csdn.net/in7deforever/article/details/6460595
if pcap:
pcap_file = open(pcap, "wb", 0)
for writes in (
("=I", 0xa1b2c3d4), # Magic number
("=H", 2), # Major version number
("=H", 4), # Minor version number
("=i", time.timezone), # GMT to local correction
("=I", 0), # Accuracy of timestamps
("=I", 65535), # Max length of captured packets
("=I", 228)): # Data link type (LINKTYPE_IPV4)
pcap_file.write(struct.pack(writes[0], writes[1]))
-
加载脚本,调用 rpc 方法
with open(Path(__file__).resolve().parent.joinpath("./script.js"), encoding="utf-8") as f:
_FRIDA_SCRIPT = f.read()
# _FRIDA_SCRIPT = session.create_script(content)
# print(_FRIDA_SCRIPT)
script = session.create_script(_FRIDA_SCRIPT)
script.on("message", on_message)
script.load()
if ssllib != "":
script.exports.setssllib(ssllib)
-
监听中断信号
signal.signal(signal.SIGINT, stoplog)
signal.signal(signal.SIGTERM, stoplog)
sys.stdin.read()
script.js分析
加载该脚本的时候,一些方法就开始执行了:
initializeGlobals()
它还有另一个触发入口。
上面分析过,从 py 文件 rpc 过来的方法叫 setssllib,当然这个方法我们也可以手动调用。
rpc.exports = {
setssllib: function (name) {
console.log("setSSLLib => " + name);
libname = name;
initializeGlobals();
return;
}
};
该方法主要是先初始化一些环境变量,比如 so 的名字以及符号地址。
这里使用了一个 frida 的 api,ApiResolver。
var resolver = new ApiResolver("module");
var exps = [
[Process.platform == "darwin" ? "*libboringssl*" : "*libssl*", ["SSL_read", "SSL_write", "SSL_get_fd", "SSL_get_session", "SSL_SESSION_get_id"]], // for ios and Android
[Process.platform == "darwin" ? "*libsystem*" : "*libc*", ["getpeername", "getsockname", "ntohs", "ntohl"]]
];
还兼容了 ios。ApiResolver 会自动搜索符合匹配规则的符号地址,比如:
const resolver = new ApiResolver('module');
const matches = resolver.enumerateMatches('exports:*!open*');
const first = matches[0];
上面的搜索规则是:在导出符号表里面搜索包含 !open
字符串的符号,就会搜索到这样的一个结果:
name: '/usr/lib/libSystem.B.dylib!opendir$INODE64',
address: ptr('0x7fff870135c9')
回到源码,看它的搜索规则:
var matches = resolver.enumerateMatchesSync("exports:" + lib + "!" + name);
至于为啥要这样写,找个系统 ssl 库,拖到 ida 里面看一下就知道了。而且,上一篇文章我们也分析过调用链了。
搜索完成之后,保存这些符号的地址:
if (addresses["SSL_get_fd"] == 0) {
SSL_get_fd = return_zero;
} else {
SSL_get_fd = new NativeFunction(addresses["SSL_get_fd"], "int", ["pointer"]);
}
SSL_get_session = new NativeFunction(addresses["SSL_get_session"], "pointer", ["pointer"]);
SSL_SESSION_get_id = new NativeFunction(addresses["SSL_SESSION_get_id"], "pointer", ["pointer", "pointer"]);
getpeername = new NativeFunction(addresses["getpeername"], "int", ["int", "pointer", "pointer"]);
getsockname = new NativeFunction(addresses["getsockname"], "int", ["int", "pointer", "pointer"]);
ntohs = new NativeFunction(addresses["ntohs"], "uint16", ["uint16"]);
ntohl = new NativeFunction(addresses["ntohl"], "uint32", ["uint32"]);
有了这些符号地址之后,我们就可以做Hook操作了,拿 SSL_read 来举例:
Interceptor.attach(addresses["SSL_read"],
{
onEnter: function (args) {
var message = getPortsAndAddresses(SSL_get_fd(args[0]), true);
message["ssl_session_id"] = getSslSessionId(args[0]);
message["function"] = "SSL_read";
message["stack"] = SSLstackread;
this.message = message;
this.buf = args[1];
},
onLeave: function (retval) {
retval |= 0; // Cast retval to 32-bit integer.
if (retval <= 0) {
return;
}
send(this.message, Memory.readByteArray(this.buf, retval));
}
});
在 SSL_read 函数返回之前,将数据发送到 py 脚本。
其中 ssl_session_id 等值的获取API需要阅读系统源码,就不展开了。
r0capture.py 分析
再回到 py 文件的 on_message 方法。
支持两种模式,一种是 verbose,一种是 pcap。verbose 就是直接将数据打印出来。pcap 是将数据储存成文件。我们分析 pcap 模式。
-
随机 sessionId,我们并不关心数据传输会话序号
if ssl_session_id not in ssl_sessions:
ssl_sessions[ssl_session_id] = (random.randint(0, 0xFFFFFFFF),
random.randint(0, 0xFFFFFFFF))
client_sent, server_sent = ssl_sessions[ssl_session_id]
-
先写数据包的头部,再写数据
for writes in (
# PCAP record (packet) header
("=I", int(t)), # Timestamp seconds
("=I", int((t * 1000000) % 1000000)), # Timestamp microseconds
("=I", 40 + len(data)), # Number of octets saved
("=i", 40 + len(data)), # Actual length of packet
# IPv4 header
(">B", 0x45), # Version and Header Length
(">B", 0), # Type of Service
(">H", 40 + len(data)), # Total Length
(">H", 0), # Identification
(">H", 0x4000), # Flags and Fragment Offset
(">B", 0xFF), # Time to Live
(">B", 6), # Protocol
(">H", 0), # Header Checksum
(">I", src_addr), # Source Address
(">I", dst_addr), # Destination Address
# TCP header
(">H", src_port), # Source Port
(">H", dst_port), # Destination Port
(">I", seq), # Sequence Number
(">I", ack), # Acknowledgment Number
(">H", 0x5018), # Header Length and Flags
(">H", 0xFFFF), # Window Size
(">H", 0), # Checksum
(">H", 0)): # Urgent Pointer
pcap_file.write(struct.pack(writes[0], writes[1]))
pcap_file.write(data)
欢迎关注我的微信公众号:二手的程序员。