Загружаем каталог…
Загружаем каталог…
안녕하세요, 이전에 잠깐 언급드렸던 아이디어 중 Shallow Copy UAF에 대해서 좀 더 자세히 다뤄보려 합니다. 아직 취약으로 정식 보고가 올라온 사례는 아닙니다. 단순 개인 아이디어로만 생각하고 편히 읽으심 될 것 같습니다. 리눅스 커널의 BPF 서브시스템은 사용자 공간에서 제출한 BPF 프로그램을 그냥 받아서 바로 실행하지 않습니다. BPF 서브시스템은, constant blinding 이라는 보안 기법을 통해 여러 단계의 검증을 거치게 됩니다. * Constant blinding이 필요한 이유는 JIT 스프레이 공격 때문입니다. * 공격자가 BPF 명령어 안에 특정 상수값을 심어두면 JIT 컴파일러가 그 상수를 기계어로 변환하는 과정에서 공격자가 원하는 바이트 패턴이 커널 메모리 안에 자연스럽게 배치됩니다. 이 패턴이 나중에 실행 흐름을 가로채는 데 쓰일 수 있습니다. 이를 막기 위해 커널은 BPF 프로그램 안의 상수값을 XOR로 랜덤화합니다. 예를 들어 상수 0x1234가 있으면, 랜덤 키 0xABCD로 XOR해서 0xB8F9로 바꾼 뒤 저장하고, 실제 사용할 때는 다시 XOR로 원래 값을 복원하는 방식입니다. 이렇게 하면 공격자가 예측 가능한 바이트 패턴을 심을 수 없게 됩니다. 그런데 이 상수 변환 작업을 원본 BPF 프로그램 위에 직접 수행하면 문제가 생깁니다. 나중에 원본이 필요한 경우를 대비해야 하기도 하고 중간에 실패했을 때 롤백이 어렵습니다. 그래서 커널은 원본을 건드리지 않고 복사본을 만들어서 거기에 변환 작업을 합니다. 그 복사본을 만드는 함수가 바로 bpf_prog_clone_create() 입니다. bpf_prog_clone_create() 들여다보기 static struct bpf_prog *bpf_prog_clone_create(struct bpf_prog *fp_other, gfp_t gfp_extra_flags) { gfp_t gfp_flags = GFP_KERNEL | __GFP_ZERO | gfp_extra_flags; struct bpf_prog *fp; fp = __vmalloc(fp_other->pages * PAGE_SIZE, gfp_flags); if (fp != NULL) { memcpy(fp, fp_other, fp_other->pages * PAGE_SIZE); } return fp; } 함수 자체는 짧습니다. 원본 프로그램이 차지하는 크기만큼 메모리를 새로 잡고 memcpy로 통째로 복사한 뒤 반환합니다. 코드 줄 수만 보면 문제가 없어 보입니다. bpf_prog 구조체 내부 struct bpf_prog { u16 pages; u16 jited:1; u16 locked:1; struct bpf_prog_aux *aux; // 문제의 필드 struct sock *sk; union { struct bpf_insn insnsi[0]; struct bpf_prog *fp; }; }; struct bpf_prog_aux { atomic64_t refcnt; u32 used_map_cnt; u32 func_cnt; const struct bpf_prog_ops *ops; struct bpf_prog *prog; struct bpf_map **used_maps; void *jit_data; struct bpf_ksym ksym; }; * bpf_prog_aux는 BPF 프로그램의 실질적인 두뇌입니다. * JIT 컴파일 관련 데이터, 이 프로그램이 참조하는 맵 목록, 함수 포인터 테이블, 심볼 정보까지 여기에 있습니다. bpf_prog 구조체 자체가 BPF 프로그램의 뼈대라면, bpf_prog_aux는 그 뼈대에 붙어있는 근육 같은 존재입니다. memcpy(fp, fp_other, size) 고찰 memcpy(fp, fp_other, size)가 실행되면 fp_other가 차지하는 메모리 블록의 바이트들을 처음부터 끝까지 순서대로 fp에 복사합니다. pages 필드, jited 플래그, 문제의 aux 필드까지 전부 그대로 복사됩니다. 여기서 핵심은, aux 필드에 실제로 저장되어 있는 값은 무엇인가 하는 겁니다. 그 값은 숫자입니다. 예를 들면 0xFFFF888012345678 같은 64비트 주소값입니다. 이 숫자가 의미하는 건 "이 주소의 메모리에 bpf_prog_aux 데이터가 들어있다"는 것입니다. aux 필드 자체가 데이터를 품고 있는 게 아니라, 데이터가 어디에 있는지 가르쳐주는 지도 역할을 하는 숫자일 뿐입니다. memcpy는 이 숫자를 그대로 복사합니다. 당연히 fp->aux에도 0xFFFF888012345678이라는 똑같은 숫자가 들어갑니다. 그런데 이 복사가 bpf_prog_aux 데이터 블록 자체를 새로 만들어주지는 않습니다. 새 메모리를 잡아서 내용을 옮기는 것이 아니라 그냥 "저 주소에 가면 데이터가 있어"라는 쪽지를 한 장 더 베껴 쓴 것뿐입니다. 결국 fp_other->aux와 fp->aux는 둘 다 0xFFFF888012345678이라는 동일한 주소를 가리키고 있습니다. 하나의 bpf_prog_aux 데이터 블록을 두 개의 bpf_prog가 동시에 소유하고 있는 상황입니다. 커널은 이 사실을 전혀 모릅니다. 참조 카운터는 여전히 1입니다. 원본의 카운터 값 1이 그대로 복사됐기 때문입니다. 실제 참조자는 두 명인데 카운터는 한 명이라고 알고 있습니다. UAF Timeline [P1] 클론 생성 직후 fp_other->aux와 fp->aux가 동일한 메모리 주소를 가리키고 있습니다. 커널은 이 상황을 인지하지 못하고 있으며, 두 bpf_prog 각자가 독립적인 aux를 가진 것으로 착각하고 있습니다. [P2] Constant blinding 완료 void bpf_prog_free(struct bpf_prog *fp) { struct bpf_prog_aux *aux = fp->aux; bpf_prog_free_deferred(aux); // aux 해제 vfree(fp); // fp 본체 해제 } Constant blinding 작업이 클론위에서 완료됩니다. 원본은 할 일을 다 했으므로 커널은 bpf_prog_free를 호출합니다. 이 함수는 먼저 fp_other->aux, 즉 0xFFFF888012345678 주소의 bpf_prog_aux 블록을 커널 메모리 할당자에게 반환합니다. "이 메모리 더 이상 쓰지 않으니 회수해가세요" 라는 신호입니다. 그 다음 fp_other 본체도 해제합니다. 이 순간부터 fp->aux가 가리키는 주소 0xFFFF888012345678은 이미 반납된 메모리가 됩니다. 하지만 fp->aux 필드 안에 저장된 숫자는 여전히 0xFFFF888012345678입니다.이제 이 포인터는 존재하지 않는 것을 가리키는 Dangling Pointer가 되었습니다. [P3] 커널이 해제된 메모리를 사용 * 리눅스 커널의 SLUB 할당자는 해제된 메모리를 즉시 재활용합니다. * 다른 커널 스레드가 소켓을 열거나, 파일을 열거나, 다른 BPF 프로그램을 로드하는 등 어떤 방식으로든 메모리 할당을 요청하면 방금 반납된 0xFFFF888012345678 자리를 그 새로운 객체에게 내어줄 수 있습니다. 이 타이밍은 예측이 불가능합니다. 이 시점 이후로는 그 주소에 완전히 다른 종류의 데이터가 들어있습니다. [P4] 클론 fp에 대한 접근 u32 func_cnt = fp->aux->func_cnt; fp->aux->ops->test_run(fp, attr, uattr); * 클론 fp는 아직 살아있습니다. 커널이 여전히 유효하다고 믿으며 fp->aux에 접근합니다. * func_cnt를 읽거나 ops->test_run() 같은 함수 포인터를 호출하는 순간, 커널은 bpf_prog_aux 구조체를 보고 있다고 믿지만 실제로는 그 자리에 들어온 전혀 다른 객체의 데이터를 읽거나, 심각한 경우에는 그 안에 있는 값을 함수 포인터로 착각해서 엉뚱한 주소로 점프합니다. 이것이 Use-After-Free입니다. Exploit 관점 공격자가 이 취약점을 무기화하려면 P2 와 P4 사이에 0xFFFF888012345678 위치를 공격자가 원하는 데이터로 채워야 합니다. 이를 힙 스프레이라 합니다. for (int i = 0; i < 1000; i++) { spray_object[i] = create_controlled_object( .ops = &fake_ops ); } 힙 스프레이는 확률 게임입니다. 공격자는 bpf_prog_aux와 같은 크기의 객체를 수백에서 수천 개씩 대량으로 생성합니다. SLUB 할당자는 같은 크기의 객체를 같은 슬랩 풀에서 관리하기 때문에 해제된 aux 자리를 그 중 하나가 차지할 가능성이 매우 높아집니다. 스프레이되는 각 객체 안에는 공격자가 제어하는 가짜 함수 포인터 테이블 주소가 심겨 있습니다. 스프레이가 성공하면 fp->aux->ops는 공격자가 만든 가짜 테이블을 가리킵니다. P4에서 커널이 fp->aux->ops->test_run()을 호출하는 순간 공격자가 지정한 주소로 점프합니다. 이건 커널 모드에서의 실행이기 때문에 SMEP이나 SMAP 같은 사용자 공간 격리 보호도 의미가 없습니다. 이미 커널 공간 안에서 일어나는 일입니다. 완전한 권한 탈취로 이어집니다. 패치 static struct bpf_prog *bpf_prog_clone_create(struct bpf_prog *fp_other, gfp_t gfp_extra_flags) { gfp_t gfp_flags = GFP_KERNEL | __GFP_ZERO | gfp_extra_flags; struct bpf_prog *fp; fp = __vmalloc(fp_other->pages * PAGE_SIZE, gfp_flags); if (fp != NULL) { memcpy(fp, fp_other, fp_other->pages * PAGE_SIZE); fp->aux = kmalloc(sizeof(*fp->aux), GFP_KERNEL); if (!fp->aux) { vfree(fp); return NULL; } memcpy(fp->aux, fp_other->aux, sizeof(*fp->aux)); fp->aux->prog = fp; atomic64_set(&fp->aux->refcnt, 1); } return fp; } * 수정의 핵심은 두 bpf_prog가 서로 다른 bpf_prog_aux를 소유하도록 만드는 것입니다. * kmalloc으로 새 메모리를 잡아서 fp->aux가 완전히 새로운 주소를 가리키게 하고, 그 새 공간에 원본의 내용을 복사합니다. 이제 원본이 해제되어 fp_other->aux의 메모리가 반납되더라도 fp->aux는 전혀 다른 주소를 가리키고 있으므로 아무런 영향을 받지 않습니다. 역참조 포인터인 aux->prog도 갱신해야 합니다. 원본의 aux->prog는 fp_other를 가리키고 있었는데, 내용을 그대로 복사하면 클론의 aux->prog도 원본을 가리키는 잘못된 상태가 됩니다. fp->aux->prog = fp로 명시적으로 바로잡아야 합니다. 참조 카운터도 원본에서 복사된 값을 그대로 쓰면 안 되고, 새 aux는 참조자가 클론 하나뿐이므로 1로 초기화해야 합니다.
То, что RADAR обнаружил и классифицировал для этой возможности. Это опубликованный источником текст, а не подтверждение, что предложение ещё действует.
[Linux] Shallow Copy UAF 고찰. 안녕하세요, 이전에 잠깐 언급드렸던 아이디어 중 Shallow Copy UAF에 대해서 좀 더 자세히 다뤄보려 합니다. 아직 취약으로 정식 보고가 올라온 사례는 아닙니다. 단순 개인 아이디어로만 생각하고 편히 읽으심 될 것 같습니다. 리눅스 커널의 BPF 서브시스템은 사용자 공간에서 제출한 BPF 프로그램을 그냥 받아서 바로 실행하지 않습니다. BPF 서브시스템은, constant blinding 이라는 보안 기법을 통해 여러 단계의 검증을 거치게 됩니다. * Constant blinding이 필요한 이유는 JIT 스프레이 공격 때문입니다. * 공격자가 BPF 명령어 안에 특정 상수값을 심어두면 JIT 컴파일러가 그 상수를 기계어로 변환하는 과정에서…
Открыть источник