[Frida] Hooking send(), recv()

[분석환경]
Ubuntu 18.04 LTS

[분석도구]
FRIDA 15

 

Frida는 바이너리 내 어떤 함수가 호출되는지 보여주고, 조작이 가능하도록 한다.

동적 바이너리 조사(DBI, Dynamic Binary Instrumentation)를 가능하게 해주는 프레임워크이다.

다양한 운영체제를 지원하고 있으며 파이썬 기반의 프레임워크로 다양한 API를 지원한다.

Frida Tutorial

Frida를 사용하여 호출된 함수를 검사하고, 인수를 수정하고, 대상 프로세스 내의 함수에 대한 사용자 지정 호출을 수행하는 방법을 보여준다

https://frida.re/docs/quickstart/

 

Quick-start guide

Inject JavaScript to explore native apps on Windows, macOS, GNU/Linux, iOS, Android, and QNX

frida.re

1. sockaddr_in 구조체 후킹

네트워크 프로그래밍을 해봤다면 가장 일반적으로 사용되는 데이터 유형 중 하나가 C의 구조체라는 것을 안다. 다음은 네트워크 소켓을 만들고 포트 5000을 통해 서버에 연결하고 연결을 통해 "Hello there!" 문자열을 전송하여 자신을 알리는 프로그램의 간단한 예제이다.

#include <arpa/inet.h>
#include <errno.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

int
main (int argc,
      char * argv[])
{
  int sock_fd, i, n;
  struct sockaddr_in serv_addr;
  unsigned char * b;
  const char * message;
  char recv_buf[1024];

  if (argc != 2)
  {
    fprintf (stderr, "Usage: %s <ip of server>\n", argv[0]);
    return 1;
  }

  printf ("connect() is at: %p\n", connect);

  if ((sock_fd = socket (AF_INET, SOCK_STREAM, 0)) < 0)
  {
    perror ("Unable to create socket");
    return 1;
  }

  bzero (&serv_addr, sizeof (serv_addr));

  serv_addr.sin_family = AF_INET;
  serv_addr.sin_port = htons (5000);

  if (inet_pton (AF_INET, argv[1], &serv_addr.sin_addr) <= 0)
  {
    fprintf (stderr, "Unable to parse IP address\n");
    return 1;
  }
  printf ("\nHere's the serv_addr buffer:\n");
  b = (unsigned char *) &serv_addr;
  for (i = 0; i != sizeof (serv_addr); i++)
    printf ("%s%02x", (i != 0) ? " " : "", b[i]);

  printf ("\n\nPress ENTER key to Continue\n");
  while (getchar () == EOF && ferror (stdin) && errno == EINTR)
    ;

  if (connect (sock_fd, (struct sockaddr *) &serv_addr, sizeof (serv_addr)) < 0)
  {
    perror ("Unable to connect");
    return 1;
  }

  message = "Hello there!";
  if (send (sock_fd, message, strlen (message), 0) < 0)
  {
    perror ("Unable to send");
    return 1;
  }

  while (1)
  {
    n = recv (sock_fd, recv_buf, sizeof (recv_buf) - 1, 0);
    if (n == -1 && errno == EINTR)
      continue;
    else if (n <= 0)
      break;
    recv_buf[n] = 0;

    fputs (recv_buf, stdout);
  }

  if (n < 0)
  {
    perror ("Unable to read");
  }

  return 0;
}

gcc -Wall ./client.c -o client

./client 127.0.0.1

첫 번째 인자로 IP주소를 넣고 실행한다. 수신측 프로그램은 준비되지 않았기 때문에 nc(netcat)를 사용한다.

nl -lp 5000 을 통해서 상호 통신을 수행할 수 있다. 송수신측 호스트는 하나의 기기에서 수행하며 두 개의 별도의 셸을 이용하여 실습한다.

serv_addr 구조체 주소를 참조하여 값 확인 가능

Frida Hooking 1, 2 게시글에서 작성했듯 문자열(string)과 포인터(pointer)를 프로세스에 주입할 수 있다.

serv_addr의 값을 보면 0x1388 의 값은 10진수로 5000이며 이는 포트 번호를 의미한다(바로 뒤의 4byte는 16진수 IP 주소). 이 값을 0x1389(5000)으로 변조하여 후킹한다. 다음 4byte를 변경하면 IP주소의 변조도 가능하다.

2. Hooking Script(struct.py)

from __future__ import print_function
import frida
import sys

session = frida.attach("client")
script = session.create_script("""
// First, let's give ourselves a bit of memory to put our struct in:
send('Allocating memory and writing bytes...');
var st = Memory.alloc(16);
// Now we need to fill it - this is a bit blunt, but works...
st.writeByteArray([0x02, 0x00, 0x13, 0x89, 0x7F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
// Module.getExportByName() can find functions without knowing the source
// module, but it's slower, especially over large binaries! YMMV...
Interceptor.attach(Module.getExportByName(null, 'connect'), {
    onEnter: function(args) {
        send('Injecting malicious byte array:');
        args[1] = st;
    }
    //, onLeave: function(retval) {
    //   retval.replace(0); // Use this to manipulate the return value
    //}
});
""")

# Here's some message handling..
# [ It's a little bit more meaningful to read as output :-D
#   Errors get [!] and messages get [i] prefixes. ]
def on_message(message, data):
    if message['type'] == 'error':
        print("[!] " + message['stack'])
    elif message['type'] == 'send':
        print("[i] " + message['payload'])
    else:
        print(message)
script.on('message', on_message)
script.load()
sys.stdin.read()

이 스크립트는 Module.getExportByName() API를 사용하여 대상에 이름으로 내보내기(exported)된 함수를 찾을 수 있다.

./client 127.0.0.1을 실행하고 nc -lp 5001을 실행하는 다른 터미널에서 ./struct_mod.py 를 실행한다.

스크립트가 실행되고 클라이언트 터미널에서 엔터를 누르면 netcat 클라이언트에서 보낸 문자열을 표시한다.

프리다를 통해서 데이터 개체를 메모리에 주입(Inject)하고 Frida와 프로세스를 연결해서 기능을 조작하는 데에 성공한다.

반응형

'Reversing' 카테고리의 다른 글

[C++] Windows Socket Programming 예제 2  (0) 2022.05.18
[C++] Windows Socket Programming 예제  (0) 2022.05.13
[Frida] Binary Hooking 2  (0) 2022.05.03
[Frida] Binary Hooking 1  (1) 2022.04.28
DLL Injection 실습  (0) 2020.12.14