The code bellow illustrates how we might go about hooking the connect function to determine what a specific application is connecting to. It is restricted to Windows, but with some changes can be made to work on *nix based systems as well.
// These constants might change on *nix
const AF_INET = 2;
const AF_INET6 = 23; // 30 on *nix
const BUFFFER_SIZE = 64;
const OFF_SIN_ADDR6 = 8; // The offset into the sock_addr structure where the ip is stored for ipv6
const OFF_SIN_ADDR = 4; // The offset into the sock_addr structure where the ip is stored for ipv4
const inet_ntop = new NativeFunction(Module.findExportByName(null, 'inet_ntop'), 'pointer', ['int', 'pointer', 'pointer', 'int']);
Interceptor.attach(Module.findExportByName(null, 'connect'), {
onEnter: function (args) {
const sockaddr_in = ptr(args[1])
const family = sockaddr_in.readShort()
// Determine the family type, so we know what offset to use
if (family == AF_INET) {
const buffer = Memory.alloc(BUFFFER_SIZE)
// Use inet_ntop to parse the byte representation to a string
inet_ntop(AF_INET, sockaddr_in.add(OFF_SIN_ADDR),
buffer, BUFFFER_SIZE)
console.log(buffer.readCString())
} else if (family == AF_INET6) {
const buffer = Memory.alloc(BUFFFER_SIZE)
inet_ntop(AF_INET6, sockaddr_in.add(OFF_SIN_ADDR6),
buffer, BUFFFER_SIZE)
console.log(buffer.readCString())
} else {
console.log("Uknown family => ", family)
}
},
onLeave: function (retval) {
}
})
Using this approach it is possible to create a mapping of socket file descriptors to ip addresses and ports. That way we can hook functions like recv & write, and enrich the hooks to output something like:
write(10.0.0.2:8080, "Raw socket test") => 15
recv(10.0.0.2:8080, &buffer) => "OK"
...
I actually forgot about this post entirely and never wrote a part 2, we’ll get to that!