Systems/Unix / Linux2008.03.18 17:02

원래 글을 잘라서 쓰는 걸 좋아하지 않는데, 제가 번역이다 뭐다 좀 바빴습니다. 양해해 주세요. ^^; 오늘은 저번에 마무리 짓지 못한 나머지 부분을 써 볼까 합니다.

Kprobes에 세 가지 정도의 kernel probe가 준비되어 있다는 이야기는 했습니다. 이 중 커널로부터 뭔가 쓸만한 정보를 알아내는 데 가장 활용도가 높은 것은 Jprobe입니다. 커널 함수를 Hooking할 수 있을 뿐 아니라, 그 함수에 전달되는 인자들까지 살펴볼 수가 있거든요. Jprobe는 다음과 같이 선언합니다. 구조체죠.

#include <linux/kprobes.h>

...

static struct jprobe my_probe = {
    .kp.addr = (kprobe_opcode_t *) udp_sendmsg,
    .entry = (kprobe_opcode_t *) inst_udp_sendmsg
};

udp_sendmsg는 커널 함수의 이름입니다. inst_udp_sendmsg는 후킹 함수의 이름입니다. inst_udp_sendmsg는 udp_sendmsg가 받는 인자들을 똑같이 받도록 정의되어야 합니다. 가령 udp_sendmsg는 다음과 같이 선언되어 있습니다.

// linux/udp.h
void udp_sendmsg(struct kiocb* iocb, struct sock* sk, struct msghdr* msg, size_t len);

그러므로 inst_udp_sendmsg는 다음과 같이 정의되어야 합니다.

static void inst_udp_sendmsg(
    struct kiocb* iocb,
    struct sock* sk,
    struct msghdr* msg,
    size_t len)
{
    ...
    jprobe_return();
}

이 함수의 제일 마지막에서 jprobe_return()을 호출하고 있다는 것을 유의해 보십시다. 이 함수를 호출하지 않으면, 후킹 함수가 1회 실행된 뒤에는 (그러니까 inst_udp_sendmsg가 실행된 뒤에) 커널이 hang되어 아무 작업도 할 수 없는 상태가 되므로 주의해야 합니다.

또 한 가지 주의할 것이 있군요. Jprobe를 정의할 때 구조체의 .kp.addr에 커널 함수의 이름(다시 말해, 커널 함수의 주소, 즉 포인터)을 저장하고 있음을 보실 수 있는데요. 커널 모듈을 컴파일하다 보면 컴파일러가 "그 함수의 정의를 찾을 수 없다"는 오류 메시지를 출력하는 경우를 간혹 보게 됩니다.

그런 경우에는 함수의 이름 대신 함수의 주소를 직접 넘겨주는 방법을 쓰는 것이 제일 속편합니다. 커널에 정의되어 있는 함수들의 주소는 어디 있냐면 (inline 함수 제외) /boot 아래에 있습니다. 이 아래에 보면 System.map-2.6.18-1.2798.fc6과 같은 파일이 있는데, 이 파일을 열어보면 커널 함수들과, 그 함수들의 주소가 전부 적혀 있습니다.

그러니 사실 jprobe를 정의할 때 이렇게 해도 됩니다.

static struct jprobe my_probe = {
    .kp.addr = (kprobe_opcode_t *) 0xc05e7c29,            // 정확한 값은 아닙니다. 대충 적었습니다.
    .entry = (kprobe_opcode_t *) inst_udp_sendmsg
};

이 방법보다 좀 더 우아한 방법으로는 kallsyms_lookup_name()이라는 다른 커널 함수를 사용하는 방법도 있는데, 간혹가다가 컴파일해 보면 이 함수의 정의를 찾을 수 없다고 불평하는 메시지가 나올 때도 있습니다. -_-; 그러니 그냥 속편하게, System.map 파일을 참고하는 것이 제일 나을 것입니다.

아무튼 이렇게 하면, (1) 커널을 후킹하는 함수를 정의하고 (2) 그 함수가 특정 커널 함수 실행 직전에 실행되도록 하여 (3) 그 함수에 전달되는 인자의 값을 검사할 수 있습니다.

가령 위에서 예로 든 예제 함수의 경우, UDP 패킷의 목적지 주소를 알아내고 싶다면 다음과 같이 하면 됩니다.

static void inst_udp_sendmsg(
    struct kiocb* iocb,
    struct sock* sk,
    struct msghdr* msg,
    size_t len)
{
    struct inet_sock* up = (struct inet_sock*)sk;
    printk("destination address = %u\n", ntohl(up->daddr) );

    jprobe_return();
}

자. 그런데 JProbe를 하나 정의한 것만으로는 사실 불충분하구요. 이 프로브를 커널에 명시적으로 심어주는 register_jprobe를 호출해 주어야 비로서 프로브가 커널 함수 호출을 hooking할 수 있게 됩니다. 이 함수 호출 형식은 모든 JProbe 프로브들에 대해서 동일하니까, 다음과 같이 매크로를 정의해두면 좀 편리하게 써먹을 수 있을 것 같습니다.

#define REGISTER_JPROBE(obj) \
    do { \
        if ( register_jprobe(&obj) < 0 ) { \
            printk("registration operation for " #obj " has failed\n"); \
        } \
    } while ( 0 )

#define UNREGISTER_JPROBE(obj) unregister_jprobe(&obj)

이 매크로들은 이전 글에서 설명했던 init_module() 함수와 cleanup_module() 함수에서 다음과 같이 사용합니다.

int init_module(void)
{
    REGISTER_JPROBE(my_probe);
    ...
    return 0;
}

void cleanup_module(void)
{
    UNREGISTER_JPROBE(my_probe);
}

자. 이렇게 하면 커널 프로브를 심는 모듈을 정의하고, 사용할 수 있습니다. 생각보다 어렵지 않습니다. KProbe나 Kretprobe도 이와 비슷한 방식으로 사용이 가능합니다. 나중에 그에 관해서는 따로 소개할 기회가 있을것 같군요. 오늘은 이정도로 마무리 하겠습니다.


신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

  1. 안녕하세요 ~!
    kprobe에 관한 흥미있는 글 잘 읽었습니다 :D
    궁금한게 있는데요 kprobe로 후킹을 하다보니 후킹이 되는 함수가 있는가 하면
    안되는 함수들도 있더라구요 특히 register_jprobe()를 사용할 때 등록이 안되는 경우가 많습니다.

    Sysmap으로 확인할 때 코드가 T 인 경우와 t 인경우에 따라서 안되는 경우가 있나요???
    예로 sys_read나 do_fork는 잘 후킹이 되는데 put_queue와 같은 함수는 잘 안되네요 .. ^^;

    2008.08.28 13:33 신고 [ ADDR : EDIT/ DEL : REPLY ]
    • 안녕하세요? 코드가 T인 경우와 t인 경우에 차이가 있는지는 여지껏 생각해보지 않은 문제인데... 새로운 숙제를 주시는군요. ^^;; 제가 요즘 이쪽일을 안하고 있어서.. 시간나는대로 한번 살펴보겠습니다. 좋은 질문 감사해요~

      2008.08.29 09:41 신고 [ ADDR : EDIT/ DEL ]
    • http://unixhelp.ed.ac.uk/CGI/man-cgi?nm 여기를 찾아보니 T는 text section에 있는 global symbol이고 t는 local symbol이라는군요. 제 생각에 local symbol은 참조가 불가능할 것 같습니다. 그 scope가 해당 symbol이 정의된 파일 안으로 한정될것 같거든요. (C에서 static 키워드가 하는 일을 생각해보시면 쉬울듯) 실제로 lxr.linux.no사이트에서 검색해보니 put_queue는 static으로 정의되어 있네요.

      2008.08.29 10:00 신고 [ ADDR : EDIT/ DEL ]
  2. bin00pang

    앗 .. 이렇게나 빠른 답변을 ^^
    정말 감사합니다 ~! 대소문자가 전역과 지역을 나타내는 거였군요
    local symbol에서는 참조가 안된다니 아쉽네요 ㅋ
    다른 방법을 강구해봐야 겠군요 ~
    커널 2.6.x에서 IDT 후킹을 하고 있는데 이게 잘 안되서 지금 죽을맛이에요 ㅎㅎ

    좋은 답변 감사 드립니다 ^^!

    2008.08.29 12:01 신고 [ ADDR : EDIT/ DEL : REPLY ]