프로젝트 주제로 ALZip 프로그램을 Fuzzing을 시도했습니다.
결과부터 말하면 ALZip Fuzzing은 실패했습니다. 구현과정에서 얻은 점이 많이 내용을 남깁니다.
프로젝트 진행 사항입니다.
- Fuzzing 대상 프로그램 : ALZip (v10.72)
- 입력 파일 포맷 : zip
입력 파일 포맷의 경우 전세계에서 가장 많이 사용되는 zip 포맷을 사용하기로 하였습니다.
아직까지 zip 포맷의 확장 정의가 관리되고 있고, 공개 된 파일 포맷이기에 포맷 분석 및 프로그램 테스트가 용이하리라 판단되었습니다.
- zip 파일 구조
Zip 파일의 구조는 위 그림과 같이 나타 납니다.
앞에 여러개의 파일 Entry 부분은 Zip 형식으로 압축 된(deflate 알고리즘) 각각의 데이터를 의미하고 Local header는 각각의 데이터에 대한 설명을 기술합니다. Zip 파일 자체의 Header는 뒤에 있는 Central Directory 부분이며 그 중에서도 가장 끝 부분에 위치하는 end of Central Directory에서 Central Directory의 offset을 알아내고 Central Directory의 구조를 설명합니다. 이를 통해 Zip 파일의 경우 전체 Zip 아카이브를 읽지 않고 Central Directory 부분만을 읽어 파일 목록을 로드 할 수 있습니다.
- zip 파일의 각 구조에 대한 설명
# Local File Header
Local file header는 파일의 0번부터 시작하며, 순차적으로 N번째 파일까지 이어집니다.
Local file header에는 Zip파일 내부에 존재하는 각 파일에 대한 정보들(파일명, 압축 정보, CRC-32, ...)을 가지고 있습니다.
Local file header뒤에는 압축에 암호화가 되어있는 경우와 암호화가 되어있지 않은 경우로 뒤에 위치하는 데이터가 다릅니다.
암호화가 되어 있는 경우에는 encryption header가 뒤에 온 후 file data가 오게 되며, 암호화가 되어 있지 않은 경우에는 바로 file data가 오게 됩니다.
# Central Directory
Central Directory는 zip파일에 포함된 파일들의 정보를 가지고 있는 Local header의 정보(Local header의 offset, 파일명, 압축사이즈 등)를 가지고 있습니다.
End of Central Directory에서 얻은 offset을 통해 Central Directory들의 첫 시작 주소를 찾았다면, 이 또한 역시 4바이트를 읽어 signature(0x02014B50)와 비교하여 검증을 해볼 수 있습니다. 일치 하지 않는다면, 위에서 offset을 잘 못 찾았다고 볼 수 있습니다.
Central Directory들은 Local file header와 달리 연속되어 존재합니다.
첫 번째 파일의 Central Directory -> 두 번째 파일의 Central Directory -> ....... -> N 번째 파일의 Central Directory
Central Directory 역시 파일명, extra field, file comment등의 가변 크기의 변수를 갖지만 사이즈를 가지고 있으므로, 읽는데 어려움은 없습니다. file comment의 끝에 바로 다음 Central Directory가 시작되기 때문에 다음 Central Directory의 offset이 따로 존재 하지 않는 것으로 추측 됩니다.
Central Directory에서 local header offset을 얻었으면 비로소 실제 파일에 접근 할 수 있는 local header에 도달할 수 있습니다.
# End of central directory record
End of Central Directory는 파일의 맨 마지막에 위치하고 있으며, 위에서 언급했듯 Central Directory의 정보(시작 offset, 전체 Central Directory 사이즈, ...)를 가지고 있습니다. 즉, Zip파일 구조를 뜯어보려면 제일 먼저 End of Central Directory를 찾으면서 시작해야 한다. 하지만 End of Central Directory의 크기는 file comment 때문에 가변적이고, 시작점을 단번에 찾기는 쉽지 않습니다.
file comment는 End of Central Directory의 맨 마지막에 위치하고, file comment를 제외한 상위 바이트들의 사이즈는 22바이트 입니다. 따라서 file comment가 존재하지 않는 경우 파일의 끝에서부터 22바이트 떨어진 지점부터 End of Central Directory가 시작된다. End of Central Directory가 시작되는지 확인하기 위해서는 4바이트를 읽어 signature(0x06054B50)와 일치하는지 체크하여 확인 할 수 있습니다.
- zip 파일 헤더
Local file header
Offset | Bytes | Description |
0 | 4 | 파일의 header 서명 |
4 | 2 | 압축 해제 시 필요한 버전 |
6 | 2 | 범용 비트 플래그 |
8 | 2 | 압축 방법 |
10 | 2 | 최종 수정 시간 |
12 | 2 | 최종 수정 날짜 |
14 | 4 | CRC-32 (순환 중복 검사 – 에러 검출 시 사용) |
18 | 4 | 압축 크기 |
22 | 4 | 원본 크기 |
26 | 2 | 파일 이름 길이 (n) |
28 | 2 | 추가 필드 길이 (m) |
30 | n | 파일 이름 |
30+n | m | 추가 필드 |
Data descriptor
Offset | Bytes | Description |
0 | 0/4 | 선택적 데이터 설명자 서명(생략 가능) = 0x08074b50 |
0/4 | 4 | CRC-32 |
4/8 | 4 | 압축 크기 |
8/12 | 4 | 원본 크기 |
Central directory file header
Offset | Bytes | Description |
0 | 4 | 중앙 디렉토리 파일 헤더 서명= 0x02014b50 |
4 | 2 | 만들어진 버전 |
6 | 2 | 압축 해제에 필요한 최소 버전 |
8 | 2 | 범용 비트 플래그 |
10 | 2 | 압축 방법 |
12 | 2 | 최종 수정 시간 |
14 | 2 | 최종 수정 날짜 |
16 | 4 | CRC-32 (순환 중복 검사 – 에러 검출 시 사용) |
20 | 4 | 압축 크기 |
24 | 4 | 원본 크기 |
28 | 2 | 파일 이름 길이 (n) |
30 | 2 | 추가 필드 길이 (m) |
32 | 2 | 파일 주석 길이 (k) |
34 | 2 | 파일이 시작되는 디스크 번호 |
36 | 2 | 내부 파일 속성 |
38 | 4 | 외부 파일 속성 |
42 | 4 | 로컬 파일 헤더의 상대 오프셋. |
46 | n | 파일 이름 |
46+n | m | 추가 필드 |
46+n+m | k | 파일 주석 |
End of central directory record (EOCD)
Offset | Bytes | Description |
0 | 4 | 중앙 디렉토리 서명의 끝 = 0x06054b50 |
4 | 2 | 디스크 번호 |
6 | 2 | 중앙 디렉토리의 시작 디스크 |
8 | 2 | 디스크의 중앙 디렉토리 레코드 수 |
10 | 2 | 총 중앙 디렉터리 레코드 수 |
12 | 4 | 중앙 디렉토리 크기 (Byte) |
16 | 4 | 중앙 디렉토리의 시작 오프셋, 아카이브 시작에 대한 상태 값 |
20 | 2 | 코멘트 길이 (n) |
22 | n | 코멘트 |
- 접근 방법
접근 방법은 Mutation 방식과 Generation 방식을 모두 사용해 보기로 하였습니다.
처음에는 zip 파일의 포맷을 분석한 것을 바탕으로 Generation 방식을 이용하였습니다.
zip 파일의 헤더는 zip 파일 자체의 헤더인 Central Directory 부분과 압축된 데이터에 해당하는 헤더인 Local 부분 헤더가 존재하고, 또 파일의 데이터 부분이 존재하여 세 부분을 모두 적절히 변환해 보았습니다.
일단 zip 파일의 Local 헤더 부분을 변경하여 fuzzing을 시도하였습니다. 이때 Local헤더 부분 중 독립적인 부분(파일의 생성 날짜, 파일 이름, ...)을 수정할 때에는 zip파일을 열었을 때 그 값만 바뀌고 아무 오류없이 정상적으로 실행되었습니다. 특정 헤더를 수정하면(압축크기, 압축 원본 등) 파일이 손상되었다고 뜨면서 복구 할 지 여부를 물어 보았습니다. 이를 통해 압축된 데이터의 헤더를 수정하여도 zip 자체의 헤더가 손상되지는 않아 zip파일을 open하는데 영향을 주지 않는다는것을 확인하였습니다.
그 후 zip 파일의 Central Directory 부분을 수정하여 fuzzing을 시도하였습니다. 역시나 몇몇 부분을 수정하면(수정 날짜, 코멘트, ...) 정상적으로 수행되었고, 다른 몇몇 부분(CRC-32, 헤더 서명, ...) 을 수행하였더니 open 에러로 실행이 불가함을 확인 할 수 있었습니다.
마지막으로 zip 파일 내에 저장된 데이터의 데이터 부분을 일부 수정해 보았습니다. 이때는 zip 파일을 열었을 때 데이터가 손상 되었으니 복구 여부를 물어보았고, 복구 여부를 물어보지 않거나 복구하지 않은 경우에도 압축 해제 시 데이터가 손상되었다는 오류를 표시할 뿐 디버깅 화면으로 넘어가지는 않았습니다.
위에서 Generation 방식을 통해 취약점을 찾지 못해 차선책으로 Mutation 방식을 구현하였고, 이를 위해 파일의 랜덤한 위치에 “ff ff ff ff” 값을 삽입하는 과정을 계속해서 반복하였습니다. 보통 10번 내외로 수정되면 파일의 헤더가 손상되었다는 오류 메시지가 뜨지만 수많은 반복에서도 디버깅 화면으로 넘어가는 경우는 발생하지 않았습니다.
- 프로그램 소스 코드
(실행 방법은 주석으로 Comment 했습니다. 알집 파일의 경우 이스트소프트 사이트에서 다운 받으시길 바랍니다.)
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | /* # param (알집 프로그램 cmd input): argv[1] - ALZip_Link "C:\Program Files (x86)\ESTsoft\ALZip\ALzip.exe" argv[2] - Test_option "/context" (생략 가능) argv[2 or 3] - Test_file "Test_file.zip"
# see (참고사항) Windows.h 를 사용하므로 Windows 환경에서만 정상적으로 동작하며 다른 운영체제에서 실행되지 않을 수 있다. # todo (추가적으로 처리해야 할 사항) 실행시 C드라이브에 직접 접근하므로 관리자 권한으로 실행하여야 한다. C드라이브에 프로그램 설치 및 테스트파일을 해당 경로에 저장해야 한다. */ #include <Windows.h> #include <iostream> #include <fstream> #include <tchar.h> #include <cstdlib> using namespace std; //cmd 문자열은 프로그램 경로와 전달할 인자(Test_file.zip)을 담고 있음 wchar_t cmd[] = L"C:\\Program Files (x86)\\ESTsoft\\ALZip\\ALzip.exe Test_file.zip"; int main(int argc, char* argv[]) { char buffer[4]; //zip 파일의 헤더를 변형할 값을 저장 //파일의 길이 측정 (for문 안에서 중복 측정 방지) fstream testfile("C:\\Program Files (x86)\\ESTsoft\\ALZip\\Test_file.zip", ios::binary | ios::out | ios::in); if (!testfile.is_open()) { perror("error name : "); testfile.close(); return -1; } testfile.seekg(0, ios::end); long long int length = testfile.tellg(); //파일의 길이를 저장 testfile.close(); //랜덤으로 zip파일의 값을 변경 / 삽입 - 1000회 반복 for (int i = 0; i < 1000; i++) { //프로그램이 저장된 파일을 open (바이너리 형태, 읽기쓰기 전용) fstream testfile("C:\\Program Files (x86)\\ESTsoft\\ALZip\\Test_file.zip", ios::binary | ios::out | ios::in); if (!testfile.is_open()) { perror("error name : "); testfile.close(); return -1; } //버퍼의 값을 "ff ff ff ff" 로 설정 memset(buffer, 255, sizeof(buffer)); //변경할 구간을 입력받음 int change = rand() % length; /* 이 값을 설정하면 파일이 덮어쓰기 범위 안에만 있어 파일이 커지지 않음. if (change - 8 >= length) change -= 8; */ //랜덤한 파일 위치에 접근하여 파일을 쓰고 close testfile.seekg(change, ios::beg); testfile.write(buffer, sizeof(buffer)); testfile.flush(); testfile.close(); cout << "[" << i + 1 << "]" << "CreateProcess!" << endl; STARTUPINFO si; PROCESS_INFORMATION pi; ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); ZeroMemory(&pi, sizeof(pi)); //ALZip 프로그램 open (인자로 zip 파일 전달) if (!CreateProcess(NULL, cmd, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) { cout << "CreateProcess fail!" << endl; cout << "Error message: " << GetLastError() << endl; exit(0); } //cout << "\tSleep 1sec" << endl; Sleep(1 * 1000); //cout << "\tterminate process" << endl; TerminateProcess(pi.hProcess, 100); //cout << "\twait child process" << endl; WaitForSingleObject(pi.hProcess, INFINITE); //cout << "\tclose handles" << endl; CloseHandle(pi.hProcess); CloseHandle(pi.hThread); cout << "\t" << i + 1 << "th test fin." << endl << endl; } return 0; }
|
|
- 실행 결과
실행결과는 퍼징 과정에서 오류가 발생하지만 프로그램 자체에서 오류가 나타났음을 알리는 것을 확인 할 수 있었습니다.
(fuzzing 영상의 경우 Mutation 방식으로 fuzzing을 구현하였고, 랜덤으로 파일의 값을 변경하면서 처음에는 오류가 발생하지 않다가 이후 오류가 발생하여 프로그램 내에서 오류가 나타났음을 알리는 알림창이 뜨는 것을 확인할 수 있습니다.)
- 미발생 원인 추측
zip 파일의 Local 헤더, Central Directory 헤더 부분을 각각 하나씩 수정해 보았고, 랜덤으로 데이터와 헤더를 바꾸면서 체크도 해 보았지만 프로그램 내에서 오류 창이 나타날 뿐 Debug 오류는 발생하지 않았습니다.
이와 같이 오류가 발생하지 않은 이유로는 첫 번째로 zip 파일이 압축 과정을 수행하여 만들어지는 파일이라 압축 과정에서 오류가 발생할 확률이 높고, 오류가 발생하게 되면 압축 된 파일 모두를 못 쓰게 되는 치명적인 결과를 초래할 수 있어 이를 방지하기 위해 오류의 처리와 복구를 위한 많은 기능을 삽입하였다고 추측하였습니다. 이와 같은 이유 때문인지 알집에서는 조금만 파일이 손상되어도 오류 메시지를 출력하고 복구 여부를 확인하는 과정을 수행하였습니다.
또 다른 이유로는 zip 파일을 단순히 open하는 과정은 open과 동시에 압축해제 하는 과정에 비해 오류가 나타나기 힘들고 탐지도 쉬울 것이라고 생각 되었습니다. 이 때문에 open과 동시에 압축해제를 수행하도록 실행할 때 open과 unzip 인자를 동시에 전달 하려고 하였지만 압축해제 인자를 전달 할 때는 ALZip 프로그램에서 파일의 이름을 암호화하여 전달 받는 것을 확인 할 수 있었습니다. ALZip 프로그램에서 퍼징이나 기타 취약점 공격을 방지하기 위해 이와 같은 조치를 취한 것으로 생각 되며, 암호화 규칙이 알려지지 않아 파일을 code 상에서 암호화하여 전달할 수 없었습니다.
- 비고
Program Language : C++ Languege
Test environment :
ㆍ OS : Windows 10 1703 (15063.674)
ㆍ IDE : Visual Studio Community 2017 v15.1 (26403.7) Release
Software Requirements :
ㆍ “Windows.h” 헤더를 사용, Windows 이외의 환경에서 정상적인 동작을 보장할 수 없음.
Reference :
ㆍ zip file format
https://www.mql5.com/en/articles/1971
https://users.cs.jmu.edu/buchhofp/forensics/formats/pkzip.html
https://en.wikipedia.org/wiki/Zip_(file_format)
http://www.pkware.com/documents/casestudies/APPNOTE.TXT
https://www.iana.org/assignments/media-types/application/zip