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!