반응형
Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Archives
Today
Total
관리 메뉴

ZZoMb1E

[Fuzzing 101] libtiff (CVE-2016-9297) 본문

STUDY/CVE && Fuzzing

[Fuzzing 101] libtiff (CVE-2016-9297)

ZZoMb1E 2024. 6. 1. 19:22
728x90
반응형

 자료들을 참고하여 분석을 진행하였기에 잘못된 부분이 있을지도 모릅니다. 

※ 보완 혹은 수정해야 되는 부분이 있다면 알려주시면 확인 후 조치하도록 하겠습니다.
 

1. 분석대상


libtiff란?

  TIFF 태그 이미지 파일 형식을 읽고 쓰기 위한 라이브러리로, TIFF 형식을 처리하기 위한 명령을 포함하고 있다.
 
  TIFF 파일은 3가지 섹션으로 구성되어 있다.
이미지 파일 헤더, 파일 디렉토리, 비트맵 데이터로 구성되어 있으며 TIFF 파일은 이미지 하나당 IFD와 하나의 비트맵이 존재한다.

TIFF 파일의 논리적 구성

https://www.fileformat.info/format/tiff/egff.htm

 

TIFF: Summary from the Encyclopedia of Graphics File Formats

Usage Used for data storage and interchange. The general nature of TIFF allows it to be used in any operating environment, and it is found on most platforms requiring image data storage. Comments The TIFF format is perhaps the most versatile and diverse bi

www.fileformat.info

 
 

2. CVE Code


CVE-2016-9297

LibTiff 4.0.6의 TIFFFetchNormalTag 기능을 사용하면 공격자가 제작된 TIFF_SETGET_C16ASCII 또는 TIFF_SETGET_C32_ASCII 태그 값을 통해 서비스 거부를 일으킬 수 있다.

CWE-125

Out-of-bounds Read
민감한 정보를 읽거나 충돌을 유발시키는 것으로 문자열 등의 데이터를 읽는 과정에서 NULL과 같이 문자열의 끝을 가리키는 값이 존재한다는 가정하에 발생한다. 문자열 범위 밖에 해당 값이 존재할 경우 메모리 유출 및 BOF로 이어질 수 있다.
 

3. CVE 관련 정보


https://nvd.nist.gov/vuln/detail/CVE-2016-9297

 

NVD - CVE-2016-9297

CVE-2016-9297 Detail Modified This vulnerability has been modified since it was last analyzed by the NVD. It is awaiting reanalysis which may result in further changes to the information provided. Description The TIFFFetchNormalTag function in LibTiff 4.0.

nvd.nist.gov

https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-9297

 

CVE - CVE-2016-9297

20161114 Disclaimer: The record creation date may reflect when the CVE ID was allocated or reserved, and does not necessarily indicate when this vulnerability was discovered, shared with the affected vendor, publicly disclosed, or updated in CVE.

cve.mitre.org

 

 

4. 분석  환경 및 구현


분석 환경 : Ubuntu 20.04
 
 

실습을 위한 설치 과정


 
실습할 디렉터리 경로 생성

cd $HOME
mkdir fuzzing_tiff && cd fuzzing_tiff/

  
Target 대상 파일 libtiff 4.0.4 버젼 설치 과정

wget https://download.osgeo.org/libtiff/tiff-4.0.4.tar.gz
tar -xzvf tiff-4.0.4.tar.gz

cd tiff-4.0.4/
./configure --prefix="$HOME/fuzzing_tiff/install/" --disable-shared
make
make install

작동 확인

$HOME/fuzzing_tiff/install/bin/tiffinfo -D -j -c -r -s -w $HOME/fuzzing_tiff/tiff-4.0.4/test/images/palette-1c-1b.tiff

 


퍼징 결과를 웹페이지를 통해 깔끔하게 확인하기 위해서 lcov를 설치한다.
이는 코드 커버리지를 시각적으로 볼 수 있게 해준다.

sudo apt install lcov

 
빌드 과정

rm -r $HOME/fuzzing_tiff/install
cd $HOME/fuzzing_tiff/tiff-4.0.4/
make clean
  
CFLAGS="--coverage" LDFLAGS="--coverage" ./configure --prefix="$HOME/fuzzing_tiff/install/" --disable-shared
make
make install

 
코드 커버리지 데이터 수집

cd $HOME/fuzzing_tiff/tiff-4.0.4/
lcov --zerocounters --directory ./
lcov --capture --initial --directory ./ --output-file app.info
$HOME/fuzzing_tiff/install/bin/tiffinfo -D -j -c -r -s -w $HOME/fuzzing_tiff/tiff-4.0.4/test/images/palette-1c-1b.tiff
lcov --no-checksum --directory ./ --capture --output-file app2.info

lcov 옵션들에 대한 설명은 다음과 같다.

  • lcov --zerocounters --directory ./: 이전 카운터 재설정한다.
  • lcov --capture --initial --directory ./ --output-file app.info: 모든 계측 라인에 대해 제로 커버리지를 포함하는 "기준선" 커버리지 데이터 파일을 반환한다.
  • $HOME/fuzzing_tiff/install/bin/tiffinfo -D -j -c -r -s -w $HOME/fuzzing_tiff/tiff-4.0.4/test/images/palette-1c-1b.tiff: 분석하고 싶은 응용 프로그램을 실행한다. 다른 입력으로 여러 번 실행할 수 있다.
  • lcov --no-checksum --directory ./ --capture --output-file app2.info: 현재 커버리지 상태를 app2.info 파일에 저장한다.

 
출력 html 파일 생성

genhtml --highlight --legend -output-directory ./html-coverage/ ./app2.info

 
Fuzzing 수행을 위한 계측 코드 삽입

rm -r $HOME/fuzzing_tiff/install
cd $HOME/fuzzing_tiff/tiff-4.0.4/
make clean

export LLVM_CONFIG="llvm-config-11"
CC=afl-clang-lto ./configure --prefix="$HOME/fuzzing_tiff/install/" --disable-shared
AFL_USE_ASAN=1 make -j4
AFL_USE_ASAN=1 make install

 


fuzzer 수행
 

afl-fuzz -m none -i $HOME/fuzzing_tiff/tiff-4.0.4/test/images/ -o $HOME/fuzzing_tiff/out/ -s 123 -- $HOME/fuzzing_tiff/install/bin/tiffinfo -D -j -c -r -s -w @@

-i : 입력에 사용할 예제의 디렉터리 경로를 설정

-o : AFL++의 결과를 저장할 디렉터리 경로 설정 옵션
-s : 무작위 정적 시드를 사용
-m : 메모리 제한을 설정
@@ : 입력 파일 이름으로 대체할 대상 표시
 
libtiff 옵션
-D : 데이터 읽기
-j : JPEG 테이블 표시
-c : 컬러맵에 대한 데이터 표시
-r : 디코딩된 데이터 대신 원시 이미지 데이터를 읽기/표시
-s : 스트립 오프셋과 바이트 수를 표시
-w : 바이트보다 단어로 원시 데이터를 표시


 

 
 
 

5. 루크 커즈 분석


ASAN으로 진행을 했기 때문에 GDB가 아닌 파일의 파라미터로 crash 파일을 전달해주면 분석을 할 수 있다.
편의를 위해 crash 파일의 이름을 변경해주었다.
 
 

$HOME/fuzzing_tiff/install/bin/tiffinfo -vvvXX -ee- nn -r'./crash'

 


 

분석



BackTrace 부분을 살펴보면  _start() → __libc_start_main() → main () → tiffinfo() → TIFFPrintDirectory() → _TIFFPrintField() → fputs()   순으로 호출되는 것을 확인할 수 있다.

 
main () → tiffinfo() → TIFFPrintDirectory() → _TIFFPrintField() 순으로 분석을 수행하면 된다.
 
 

코드 분석


main()에서는 TIFF 파일을 열고 디렉토리 설정 및 필요한 정보를 읽고, 출력하는 등의 작업을 수행하는 함수를 호출한다.

tiffinfo.c:main()

old_error_handler = TIFFSetErrorHandler(PrivateErrorHandler);

	multiplefiles = (argc - optind > 1);
	for (; optind < argc; optind++) {
		if (multiplefiles)
			printf("%s:\n", argv[optind]);
		tif = TIFFOpen(argv[optind], chopstrips ? "rC" : "rc");
		if (tif != NULL) {
			if (dirnum != -1) {
				if (TIFFSetDirectory(tif, (tdir_t) dirnum))
					tiffinfo(tif, order, flags, 1);
			} else if (diroff != 0) {
				if (TIFFSetSubDirectory(tif, diroff))
					tiffinfo(tif, order, flags, 1);
			} else {
				do {
					toff_t offset=0;

					tiffinfo(tif, order, flags, 1);
					if (TIFFGetField(tif, TIFFTAG_EXIFIFD,
							 &offset)) {
						if (TIFFReadEXIFDirectory(tif, offset)) {
							tiffinfo(tif, order, flags, 0);
						}
					}
				} while (TIFFReadDirectory(tif));
			}
			TIFFClose(tif);
		}
	}
	return (status);
}

pcap_loop()를 호출할 때 패킷에 대해 호출될 콜백 함수를 지정해주고 있다.

 

tiffinfo(tif, order, flags, 1);

tiffinfo()의 인자로 4개의 값이 주어진다.
tif : 파일 핸들러
order : 채우기 순서
flags : 출력 옵션
1 : 이미지인지 여부
 
main.c:tiffinfo()

static void
tiffinfo(TIFF* tif, uint16 order, long flags, int is_image)
{
	TIFFPrintDirectory(tif, stdout, flags);
	if (!readdata || !is_image)
		return;
	if (rawdata) {
		if (order) {
			uint16 o;
			TIFFGetFieldDefaulted(tif,
			    TIFFTAG_FILLORDER, &o);
			TIFFReadRawData(tif, o != order);
		} else
			TIFFReadRawData(tif, 0);
	} else {
		if (order)
			TIFFSetField(tif, TIFFTAG_FILLORDER, order);
		TIFFReadData(tif);
	}
}

 
TIFF 파일 정보를 읽고 출력하는 경우 원시 데이터/일반 데이터를 읽는 경우에 따라 그에 맞는 처리를 수행한다.
원시 데이터를 읽는다면 필요한 경우 채우기 순서를 확인하여 그에 맞게 바꾸지만 일반 데이터의 경우 사용자가 지정한 순서에 따라 데이터를 읽게 된다.
 
tif_print.c:TIFFPrintDirectory()

/*
			 * Catch the tags which needs to be specially handled
			 * and pretty print them. If tag not handled in
			 * _TIFFPrettyPrintField() fall down and print it as
			 * any other tag.
			 */
			if (!_TIFFPrettyPrintField(tif, fip, fd, tag, value_count, raw_data))
				_TIFFPrintField(fd, fip, value_count, raw_data);

			if(mem_alloc)
				_TIFFfree(raw_data);
		}
	}

특정 태그를 찾아서 보기 좋게 출력하는 부분이다.
TIFF 파일의 Strip 정보를 출력하는 옵션이 활성화 및 Strip Offset 필드가 설
정된 경우 _TIFFPrintField()가 실행되게 된다.
 
tif_print.c:_TIFFPrintField()

#else
			fprintf(fd, "0x%llx",
				(unsigned long long)((uint64 *) raw_data)[j]);
#endif
		else if(fip->field_type == TIFF_FLOAT)
			fprintf(fd, "%f", ((float *)raw_data)[j]);
		else if(fip->field_type == TIFF_DOUBLE)
			fprintf(fd, "%f", ((double *) raw_data)[j]);
		else if(fip->field_type == TIFF_ASCII) {
			fprintf(fd, "%s", (char *) raw_data);
			break;
		}
		else {
			fprintf(fd, "<unsupported data type in TIFFPrint>");
			break;
		}

		if(j < value_count - 1)
			fprintf(fd, ",");
	}

	fprintf(fd, "\n");
}

fip→field_type이 TIFF_ASCII인 경우 문자열 형태로 출력 후 종료한다.
_TIFFPrintField()에서 2번째 인자로 fip구조체를 받고 있다.
 

fip = TIFFFieldWithTag(tif, tag);

TIFFPrintDirectory()를 보면 위의 코드가 있다.
TIFFFieldWithTag()의 반환값을 가지기 때문에 해당 함수를 확인해보겠다.
 
tif_dirinfo.c:TIFFFieldWithTag()

const TIFFField*
TIFFFieldWithTag(TIFF* tif, uint32 tag)
{
	const TIFFField* fip = TIFFFindField(tif, tag, TIFF_ANY);
	if (!fip) {
		TIFFErrorExt(tif->tif_clientdata, "TIFFFieldWithTag",
			     "Internal error, unknown tag 0x%x",
			     (unsigned int) tag);
	}
	return (fip);
}

 
특정 태그를 검색하는 함수로, TIFF_ANY는 모든 필드 타입을 대상으로 수행하게 된다.
이제 동적 디버깅을 통해 살펴보도록 하겠다.
 

_TIFFPrintfField()에서 취약점이 발생하기 때문에 해당 지점에 bp를 걸었다.
 

취약점이 발생할 때의 구조체이다.
해당 구조체의 field_type은 TIFF_ASCII이기 때문에 else if(fip0->field_type == TIFF_ASCII)에 해당하는 코드가 실행되게 된다.

 
여기서 raw_data가 출력되게 되는데 이 부분을 살펴보면 취약점이 터지는 이유를 확인할 수 있다.

raw_data는 0x805fa70에 위치하는데, 확인해보면 문자열의 끝을 가르키는 NULL이 존재하지 않는 것을 볼 수 있다. 때문에 CWE-125 Out-of-bounds read가 발생하는 것이다.
 
 

또한 Heap 영역에서 발생하는 것이기 때문에 Asan으로 봤을 때 Heap over flow로 나오게 된다.
 
여기에 이제 lcov를 살펴보도록 하겠다.

LCOV를 통해서 각 커버리지가 Hit인지 아닌지를 볼 수 있다.
위에서 분석한 코드의 부분을 찾아서 확인해보겠다.
 

분석한 부분의 코드가 Hit로 2개의 커버리지가 존재하는 것을 확인할 수 있다.
이를 이용하면 좀 더 쉽게 분석을 할 수 있다.
 


6. 테스트

NULL 바이트가 없어서 발생하는 취약점인 것을 확인했다.
crash 파일을 수정하여 정상적인 경우에는 어떨지 확인해보겠다.
 

우선 실행 결과이다. 출력되는 부분에 있어서 변화가 있는 것을 볼 수 있다.
 

위는 원본 파일이다. 이상한 값이 같이 출력되는 것을 볼 수 있다.
 
해당 파일을 101 Editor로 확인해보겠다.

 
raw_data에 해당하는 부분에 NULL 값을 삽입하여 아래와 같이 만들어 준다.

 
해당 파일을 가지고 돌리면 이전과는 다르게 NULL 바이트 이전까지만 출력되는 것을 볼 수 있다.

 
 
 

7. 패치 파악


패치된 버젼과의 비교를 수행했다.

case TIFF_SETGET_C16_ASCII:
	{
		uint8* data;
		assert(fip->field_readcount==TIFF_VARIABLE);
		assert(fip->field_passcount==1);
		if (dp->tdir_count>0xFFFF)
			err=TIFFReadDirEntryErrCount;
		else
		{
			err=TIFFReadDirEntryByteArray(tif,dp,&data);
			if (err==TIFFReadDirEntryErrOk)
			{
				int m;
				m=TIFFSetField(tif,dp->tdir_tag,(uint16)(dp->tdir_count),data);
				if (data!=0)
					_TIFFfree(data);
				if (!m)
					return(0);
			}
		}
	}
	break;
case TIFF_SETGET_C32_ASCII:
	{
		uint8* data;
		assert(fip->field_readcount==TIFF_VARIABLE2);
		assert(fip->field_passcount==1);
		err=TIFFReadDirEntryByteArray(tif,dp,&data);
		if (err==TIFFReadDirEntryErrOk)
		{
			int m;
			m=TIFFSetField(tif,dp->tdir_tag,(uint32)(dp->tdir_count),data);
			if (data!=0)
				_TIFFfree(data);
			if (!m)
				return(0);
		}
	}
	break;

TIFF_SETGET_C16_ASCII, TIFF_SETGET_C32_ASCII 태그일때 실행되는 코드이다.
각 비트 카운터와 ASCII 오프셋을 가진 필드를 처리한다.
TIFFReadDirEntryArray()를 호출하여 바이트 배열을 읽으며 이에 성공하면 TIFFSetField()를 호출하여 TIFF 파일의 해당 필드를 설정한다.
 

case TIFF_SETGET_C16_ASCII:
	{
		uint8* data;
		assert(fip->field_readcount==TIFF_VARIABLE);
		assert(fip->field_passcount==1);
		if (dp->tdir_count>0xFFFF)
			err=TIFFReadDirEntryErrCount;
		else
		{
			err=TIFFReadDirEntryByteArray(tif,dp,&data);
			if (err==TIFFReadDirEntryErrOk)
			{
				int m;
                    if( dp->tdir_count > 0 && data[dp->tdir_count-1] != '\0' )
                    {
                        TIFFWarningExt(tif->tif_clientdata,module,"ASCII value for tag \"%s\" does not end in null byte. Forcing it to be null",fip->field_name);
                        data[dp->tdir_count-1] = '\0';
                    }
				m=TIFFSetField(tif,dp->tdir_tag,(uint16)(dp->tdir_count),data);
				if (data!=0)
					_TIFFfree(data);
				if (!m)
					return(0);
			}
		}
	}
	break;
case TIFF_SETGET_C32_ASCII:
	{
		uint8* data;
		assert(fip->field_readcount==TIFF_VARIABLE2);
		assert(fip->field_passcount==1);
		err=TIFFReadDirEntryByteArray(tif,dp,&data);
		if (err==TIFFReadDirEntryErrOk)
		{
			int m;
                if( dp->tdir_count > 0 && data[dp->tdir_count-1] != '\0' )
                {
                    TIFFWarningExt(tif->tif_clientdata,module,"ASCII value for tag \"%s\" does not end in null byte. Forcing it to be null",fip->field_name);
                    data[dp->tdir_count-1] = '\0';
                }
			m=TIFFSetField(tif,dp->tdir_tag,(uint32)(dp->tdir_count),data);
			if (data!=0)
				_TIFFfree(data);
			if (!m)
				return(0);
		}
	}
	break;

패치된 부분을 살펴보면 entry의 마지막이 NULL이 아닌 경우 NULL을 삽입하여 문자열의 종료를 가르킬 수 있도록 코드가 추가되었다.


 

728x90
반응형