직접 릭
- 커널 코드의 주소를 릭할 수 있는 가장 간단한 방법은 바로 주소를 직접 릭 시키는 취약점을 찾아 익스플로잇 하는 것이다.
- 한 예로, 2.6.29이하 버전의 커널에서는 /proc/[PID]/stat 또는 /proc/[PID]/wchan 가상 파일을 통해 커널 주소를 손쉽게 획득할 수 있었다.
- wchan필드는 본래 태스크가 대기상태에 있을 때 마지막으로 실행한 커널 함수의 주소를 출력하여 태스크의 대기 사유를 알 수 있도록 한 것인데, KASLR이 도입되면서 사용자 공간에서 알지 못해야 할 베이스 주소가 릭되는 문제가 생긴 것이다.
- 최신 버전 커널의 경우 관리자 권한을 가지고 있어야만 wchan필드에 접근할 수 있으며, 커널 주소 대신 항상 함수명 심볼 또는 0을 출력하도록 변경되었다.
dmesg(커널 로그) 출력
- 커널 코드에서 printk라는 함수를 이용하면 커널 로그에 메시지를 남길 수 있다.
- 사용법은 printf와 유사하다.
- 만약 커널 또는 커널 모듈에서 디버깅용 등으로 커널 포인터 값을 출력한다면, 커널 로그에 접근할 권한이 있는 사용자는 커널 주소를 획득할 수 있게 된다.
- 리눅스 커널 개발진들은 이를 방지하기 위해 %p 형식으로 주소를 출력하면 주소에 해시 연산을 적용하여 출력하도록 printk를 구현하였다.
- 이렇게 하면 커널 주소 릭을 방지할 수 있다.
- 한편 같은 주소의 경우 해시 값도 같기 때문에 같은 주소가 여러번 출력되는 경우를 구분할 수는 있다.
- 만약 커널 코드가 printk를 통해 커널 주소를 릭하게 된다면, 일반 사용자는 dmesg(/dev/kmsg)커널 로그를 접근할 수 없지만 충분한 권한이 있는 사용자는 이를 읽어들여 KASLR을 무력화할 수 있게 된다.
형식 지정자 | 설명 | 예시(64비트) |
%p | 포인터를 출력한다. 단, 주소가 커널 공간 내에 위치할 경우 커널 릭을 방지하기 위해 해쉬하여 출력한다. | 0x0 => 0000000000000000 0xffffffff8108157b => 0x0000000070d2fc92 |
%p | 포인터를 해쉬 처리 없이 그대로 출력한다. | 0x0 => 0000000000000000 0xffffffff8108157b => 0xffffffff8108157b |
%pS, %ps, %pSR, %pB | 포인터를 심볼로 출력한다. 주소에 인접한 심볼이 없는 경우 커널 주소가 그대로 출력된다. | 0xffffffff8108157b => commit_creds+0x0/0x14e(%pS) 0xffffffffffc8c7d9 => 0xffffffffffc8c7d9 |
%pK | 포인터를 출력한다. kptr_restrict커널 파라미터가 1이 아니거나 현재 프로세스가 CAP_SYSLOG 권한이 없는 경우 %p와 같이 해쉬하여 출력한다. | 0xffffffff8108157b => 0xffffffff8108157b(kptr_restrict=1이고 CAP_SYSLOG권한이 있을때) 0xffffffff8108157b => 0x0000000070d2fc92(그 외) |
초기화되지 않은 메모리
- 메모리상의 버퍼가 초기화되기 전에는 스택 및 힙 주소가 포함되어 있을 수 있는데, 만약 버퍼가 초기화 없이 출력되면 공격자는 이를 바탕으로 프로그램의 주소를 획득할 수 있다.
- 커널은 네트워크 카드를 통해 외부와 통신할 수도 있고, 사용자 공간과 정보를 교환하기도 한다.
- 공격자는 외부 네트워크에 있을 수도 있고, 같은 커널 아래에서 권한이 분리된 다른 사용자일 수도 있다.
- 따라서 커널 개발자는 버퍼 초기화에 있어 양쪽 모두 주의를 기울여야 한다.
- 특히 커널 데이터를 사용자 공간으로 복사하는 put_user, copy_to_user함수 등을 사용할 때 전달할 버퍼를 제대로 초기화하는지 확인할 필요가 있다.
C 구조체 초기화 구문 사용
- C언어에서는 구조체 자료형의 변수를 선언하는 동시에 해당 변수의 값을 초기화하는 struct STRUCT VARNAME = { ... }; 구문을 사용할 수 있다.
- 그러나, 이러한 구문의 문제점 중 하나는 구조체가 차지하는 메모리 전체를 초기화할 수 있다는 보장이 없다는 것이다.
- 일반적으로 CPU는 접근하는 주소가 정렬된 주소일 때, 즉 변수 필드의 자료형 크기로 나누어 떨어질 때 최적의 성능을 발휘한다.
- 이러한 이유로 컴파일러에서는 각 필드의 주소가 정렬되도록 필드 간 또는 구조체 끝에 여백(padding)을 삽입할 수 있다.
- 따라서 초기화 구문을 사용하면 각 필드의 값을 설정하게 되지만 컴파일러가 중간에 삽입한 여백은 초기화되지 않을 수 있다.
- 여백에 위치한 초기화되지 않은 데이터를 통해 커널 주소가 릭될 수 있다.
- 이는 대입 연산자를 사용해서 구조체를 복사하는 경우도 해당된다.
- 개별 필드는 복사되지만, 컴파일러 최적화에 따라 구조체의 여백 부분은 복사되지 않을 수 있기 때문이다.
- 이를 해결하기 위해 외부에 출력되는 구조체 및 버퍼는 무조건 memset같은 함수로 초기화하는 것이 좋다.
C 구조체 초기화 구문 사용 - 방어
- 초기화 누락으로 인한 커널 주소 릭 취약점을 방지하기 위해, 리눅스 커널 개발진은 gcc컴파일러에 부착하여 사용하는 Structleak플러그인을 개발했다.
- 커널 컴파일 중 Structleak플러그인은 커널 주소 릭 가능성을 탐지하면 경고 메시지를 출력한다.
- 또한 추가적인 옵션을 지정하면 C코드에서 초기화하지 않은 구조체를 자동으로 초기화 해준다.
- 단, 약간의 성능 하락이 나타날 수는 있다.
CONFIG_GCC_PLUGIN_STRUCTLEAK=y
CONFIG_INIT_STACK_ALL=y
CONFIG_GCC_PLUGIN_STRUCTLEAK_VERBOSE=y
- 커널 빌드 시 .config파일을 통해 Structleak플러그인을 활성화할 수 있다.
- 위는 검사를 최대로 두는 설정값
OOB Read
- 사용자 입력을 검증하지 않고 배열 인덱스나 크기값으로 사용하면 OOB접근이 일어나게 된다.
- 만약 OOB가 발생하는 버퍼의 뒤에 커널 주소가 저장되어 있다면 공격자는 OOB read를 통해 커널 주소를 릭할 수 있다.
- Stack out-of-bounds read
- 리턴 주소, 함수 포인터 인자 등을 이용해 커널 베이스 주소 릭
- 인접한 스택 변수의 커널 주소를 이용해 커널 베이스 주소 릭
- Heap / slab out-of-bounds read
- 인접한 메모리의 커널 주소를 이용해 커널 베이스 주소 릭
- Global out-of-bounds read
- 인접한 전역 변수에 저장된 커널 주소를 이용해 커널 베이스 주소 릭
예시: VT Infoleak 취약점
- 현재 실습중인 커널에는 가상 터미널(VT)구현이 소프트웨어 프레임버퍼와 함께 사용될 때 infoleak 취약점이 존재한다.
- 가상 터미널은 Ctrl-Alt-F1~F12키를 눌렀을 때 보이는 터미널이다.
- VT는 ioctl 시스템 콜을 이용해서 제어할 수 있으며, 이중 VT_RESIZE 및 VT_RESIZEX 명령을 사용하면 터미널 크기를 조정할 수 있다.
- VT_RESIZEX 명령어는 특이하게도 텍스트 라인의 높이를 설정할 수 있는 v_clin필드가 존재한다.
- 일반적으로 가상 터미널은 커널에 내장된 8*16 글자 크기의 폰트를 기본 값으로 사용하며, 따라서 라인 높이도 16으로 설정되어 있다.
- 그러나 소프트웨어 프레임 버퍼를 사용한다고 가정하였을 때 이를 16보다 높은 값으로 설정하면 폰트 데이터의 크기를 잘못 인식하게 되면서 폰트가 깨지게 된다.
- 여기서 GIO_FONT명령을 사용하면 OOB read가 발생하면서 폰트 데이터 뒤의 메모리를 읽어올 수 있다.
- 최신 버전에는 v_clin필드를 무시하는 방향으로 패치되었다.
- 프레임버퍼 VT 콘솔을 초기화하는 로직은 drivers/video/fbdev/core/fbcon.c에 존재한다.
/* Setup default font */
if (!p->fontdata && !vc->vc_font.data) {
if (!fontname[0] || !(font = find_font(fontname)))
font = get_default_font(info->var.xres,
info->var.yres,
info->pixmap.blit_x,
info->pixmap.blit_y);
vc->vc_font.width = font->width;
vc->vc_font.height = font->height;
vc->vc_font.data = (void *)(p->fontdata = font->data);
vc->vc_font.charcount = 256; /* FIXME Need to support more fonts */
} else {
p->fontdata = vc->vc_font.data;
}
- 이중 폰트를 초기화하는 부분의 코드이다.
- fontname은 기본적으로 빈 문자열로 설정되어 있으므로 get_default_font를 호출하게 된다.
- get_default_font 함수는 모니터 해상도와 폰트 크기 제한에 따라 적절한 폰트를 선택하며, 일반적으로는 VGA8*16 폰트가 선택된다.
- vc_font.data에는 폰트 비트맵 데이터가 저장되며, 데이터 크기는 (vc_font.width + 7) / 8 * vc_font.height * vc_font.charcount바이트로 계산된다.
- 따라서 width, height, charcount필드 중 하나라도 조작할 수 있다면 OOB read가 발생할 수 있다.
- 소프트웨어 프레임 버퍼를 사용하는 경우 커널은 프로그램이 콘솔에 메시지를 출력할 때마다 미리 지정된 폰트 데이터를 화면에 복사(blit)하는 방식으로 텍스트를 화면에 표시한다.
- 응용 프로그램이 터미널에 write()시스템 콜을 호출하면 tty_fops구조체에 지정된 tty_write함수가 호출되게 된다.
- 해당 함수에서 ld->ops는 VT를 사용하는 경우 con_ops구조체 변수를 가리킨다.
- 따라서 do_tty_write가 호출될 때 첫번째 인자로 con_write함수 포인터를 넘기게 되며, do_tty_write는 인자로 넘겨진 함수 포인터를 호출하면서 con_write함수가 호출된다.
- con_write함수는 do_con_write함수를 호출하고 이 함수는 또다시 con_flush함수를 호출한다.
- 프레임버퍼 콘솔을 사용한다고 가정하였을 때 con_putcs함수 포인터는 fbcon_putcs함수를 가리키며, 프레임 버퍼 기본 설정(화면을 회전하지 않음, 타일링 사용 안함)에서 이 함수는 ops->putcs를 통해 bit_putcs함수를 호출하게 된다.
static void bit_putcs(struct vc_data *vc, struct fb_info *info,
const unsigned short *s, int count, int yy, int xx,
int fg, int bg)
{
...
struct fb_image image;
u32 width = DIV_ROUND_UP(vc->vc_font.width, 8);
u32 cellsize = width * vc->vc_font.height;
...
u32 mod = vc->vc_font.width % 8, cnt, pitch, size;
...
image.height = vc->vc_font.height;
...
image.width = vc->vc_font.width * cnt;
...
if (!mod)
bit_putcs_aligned(vc, info, s, attribute, cnt, pitch,
width, cellsize, &image, buf, dst);
...
}
- vc_font.width는 8로 설정되어 있으므로 mod가 0으로 설정되고, !mod 조건이 참이 되어 bit_putcs_aligned함수가 호출된다.
static inline void bit_putcs_aligned(struct vc_data *vc, struct fb_info *info,
const u16 *s, u32 attr, u32 cnt,
u32 d_pitch, u32 s_pitch, u32 cellsize,
struct fb_image *image, u8 *buf, u8 *dst)
{
u16 charmask = vc->vc_hi_font_mask ? 0x1ff : 0xff;
u32 idx = vc->vc_font.width >> 3;
u8 *src;
while (cnt--) {
src = vc->vc_font.data + (scr_readw(s++)&
charmask)*cellsize;
...
}
info->fbops->fb_imageblit(info, image);
}
- bit_putcs_aligned는 문자를 표시하기 위해 vc_font.data에 접근하는 것을 볼 수 있다.
- scr_readw(s++)는 프로그램이 출력한 문자 코드에 대응되며 charmask에 의해 255또는 511로 최댓값이 제한된다.
- 만약 (scr_readw(s++)&charmask)*cellsize값이 폰트 데이터 크기를 초과하게 된다면 src는 폰트 비트맵 데이터의 범위를 넘게 되고, 최종적으로 커널 데이터가 화면에 출력될 수 있다.
u32 width = DIV_ROUND_UP(vc->vc_font.width, 8);
u32 cellsize = width * vc->vc_font.height;
- 위는 bit_putcs함수의 cellsize계산식이다.
- 따라서 vc->vc_font.width를 조작하면 OOB read취약점을 트리거할 수 있다.
어질어질하다... 어케 찾았누...
- 이제 vc_font.height를 조작할 수 있는 경로를 알아보자.
- VT를 대상으로 한 ioctl명령어는 drivers/tty/vt/vt_ioctl.c의 vt_ioctl함수에서 처리된다.
/*
* We handle the console-specific ioctl's here. We allow the
* capability to modify any console, not just the fg_console.
*/
int vt_ioctl(struct tty_struct *tty,
unsigned int cmd, unsigned long arg)
{
...
switch (cmd) {
...
case VT_RESIZEX:
...
for (i = 0; i < MAX_NR_CONSOLES; i++) {
...
console_lock();
vcp = vc_cons[i].d;
if (vcp) {
...
if (v.v_clin)
vcp->vc_font.height = v.v_clin;
...
}
console_unlock();
}
...
}
out:
return ret;
}
- 명령 번호는 cmd인자로 넘겨지며, vt_ioctl은 switch문을 통해 주어진 명령에 따라 처리를 적절히 수행한다.
- 만약 VT_RESIZEX명령을 호출하면서 v_clin값을 설정하면, 모든 VT의 폰트 높이를 v_clin값으로 설정하게 된다.
- 따라서 가상 터미널에 VT_RESIZEX명령을 내릴때 v_clin값을 vcp->vc_font.height보다 높은 값으로 설정하면 OOB read를 발생시킬 수 있다.
- v_clin을 통해 height값을 본래 값보다 높게 설정하면 위와 같이 콘솔 출력에 문제가 발생한다.
- 우측과 같이 height가 32로 설정된 경우 텍스트 높이 또한 2배가 된 것을 볼 수 있다.
- 우측 사진에서 비정상적인 부분은 OOB read가 발생한 결과로, 커널 데이터가 릭되어 출력된 모습이다.
- 만약 현재 사용자가 video그룹에 속해 있다면, /dev/fb0가상 파일을 통해 화면 이미지를 적재하고 릭 부분을 추출하여 커널 베이스 주소를 구할 수 있다.
- /dev/fb0 장치에 접근할 수 없다 해도 OOB read를 수행할 방법이 존재한다.
- 바로 폰트 데이터를 가지고오는 GIO_FONT명령을 사용하는 것이다.
- vt_ioctl함수에서 GIO_FONT를 처리하는 부분은 다음과 같다.
case GIO_FONT: {
op.op = KD_FONT_OP_GET;
op.flags = KD_FONT_FLAG_OLD;
op.width = 8;
op.height = 32;
op.charcount = 256;
op.data = up;
ret = con_font_op(vc_cons[fg_console].d, &op);
break;
}
- con_font_op함수는 KD_FONT_OP_GET가 지정되었으므로 아래와 같이 con_font_get함수를 호출하게 된다.
int con_font_op(struct vc_data *vc, struct console_font_op *op)
{
switch (op->op) {
...
case KD_FONT_OP_GET:
return con_font_get(vc, op);
...
}
return -ENOSYS;
}
- con_font_get함수는 폰트 데이터를 임시로 저장할 버퍼를 할당한 후 vc->vc_sw->con_font_get을 호출한다.
- 프레임버퍼의 경우 이는 fbcon_get_font함수가 된다.
static int fbcon_get_font(struct vc_data *vc, struct console_font *font)
{
u8 *fontdata = vc->vc_font.data;
u8 *data = font->data;
int i, j;
font->width = vc->vc_font.width;
font->height = vc->vc_font.height;
font->charcount = vc->vc_hi_font_mask ? 512 : 256;
if (!font->data)
return 0;
if (font->width <= 8) {
j = vc->vc_font.height;
for (i = 0; i < font->charcount; i++) {
memcpy(data, fontdata, j);
memset(data + j, 0, 32 - j);
data += 32;
fontdata += j;
}
} else if (font->width <= 16) {
...
}
return 0;
}
- 텍스트 출력과 마찬가지로 vc->vc_font.data에 접근하며, 동일한 종류의 OOB 취약점이 발생하는 것을 볼 수 있다.
- 따라서 GIO_FONT로 폰트 데이터를 추출하면 앞에서 출력한 화면 결과에 사용된 폰트와 동일한 데이터를 획득할 수 있다.
- memset으로 각 글자(glyph)간 간격을 0으로 채워넣는 코드를 채워 넣기는 하지만 높이가 32인 경우 간격이 사라지므로 문제가 발생하지 않는다.
타입 혼동
- 한편 오프셋이나 크기가 고정적이더라고 타입 혼동에 의해 OOB접근이 발생하는 경우도 존재한다.
- 만약 인지된 구조체 타입의 크기가 실제 크기보다 크다면 범위 밖의 메모리에 접근할 수 있게 된다.
- 예시로 특정 주소에 위치한 구조체의 크기가 16바이트라고 가정했을 때, 만약 이 주소가 본래 타입과 다르게 64바이트의 구조체의 주소로 해석된다면 타입 혼동 취약점이 발생한다.
- 이때 나머지 48바이트에 위치한 필드에 접근하면 관련없는 메모리에 접근하는 결과를 낳게 된다.
실습
준비
- KASLR이 활성화된 상태에서 lke-bof모듈을 재사용하여 실습해보자.
- 커널 주소를 알아내기 위해 앞서 분석한 가상 터미널의 OOB 취약점을 활용할 수 있다.
- 가상 터미널을 사용하기 위해 다음과 같이 run.sh파일을 수정하고 실행하여 VM의 가상 모니터를 활성화해야 한다.
-display none --> -display gtk 또는 -display sdl
-vga none --> -vga std 또는 -vga virtio
- 근데 현재 클라우드 ssh 서버에 연결하여 진행중이라 gui를 못띄우겠다. (호스트가 m2 맥북..)
- 인터넷에 나오는 x11을 설치해라 어쩌고 저쩌고 다 따라해봤는데 잘 안된다.
- 데스크탑 사면 그때 이어서 하는 걸로...
- gui는 해결했다..(2024.01.09)
- 이게 되는 원리가 x11 forwarding인데 맥북의 경우 xterm역할을 하는 xquartz를 추가로 설치해야 잘 동작한다고 한다.
- ssh연결을 할 때 -X -Y옵션을 넣으면 동작한다.
- 근데 반응속도가 매우 느려서 못 써먹을 정도이다...
- 그리고 왜인지 모르겠는데 shared디렉토리의 커널 모듈이 하나도 보이지 않는다.
- 걍 담에 하는걸로~
'Pwn > Kernel Security' 카테고리의 다른 글
[Pwn] Dreamhack - 8.Exploit Tech: ret2usr (0) | 2024.01.09 |
---|---|
[Pwn] Dreamhack - 6.Mitigation: KASLR (0) | 2024.01.07 |
[Pwn] Dreamhack - 5.Exploit Tech: prepare & commit (1) | 2024.01.07 |
[Pwn] Dreamhack - 4.Background: Tasks (0) | 2024.01.06 |
[Pwn] Dreamhack - 3.Background: Kernel Debugging (0) | 2024.01.06 |