Apache Race Bug Review (CVE-2014-0226)

아파치 2.4.7 버전에서 발견된 race 버그이다.
scoreboard: 부모와 자식 간에 소통하는 파일

scoreboard와 mod_status 를 업데이트 할 때 생기는 race condition이다.
htaccess credential, ssl certificate private key 등을 노출시킬 수 있는 힙 버퍼 오버플로우를 유발할 수 있는 심각한 버그이다.


세팅
아파치는 MPM event 혹은 MPM worker로 컴파일 돼야 한다.
./configure --enable-mods-shared=reallyall --with-included-apr
httpd.conf에서 mod_status의 configuration에
SetHandler server-status
ExtendedStatus On
설정을 해준다.
이 설정은 서버상태의 모니터링을 할 때 자세한 상태 정보 제공을 가능하게 한다.


버그 동작 분석
server/util.c파일의 1908 line ap_escape_logitem 함수에서 문자열의 포인터str을 받는다.
그 포인터를 다른 문자열 포인터s로 복사한 뒤 문자열 끝까지 가면서 escape돼야 할 문자의 수를 센다.
1907 AP_DECLARE(char *) ap_escape_logitem(apr_pool_t *p, const char *str)
1908 {
1909     char *ret;
1910     unsigned char *d;
1911     const unsigned char *s;
1912     apr_size_t length, escapes = 0;
1913
1914     if (!str) {
1915         return NULL;
1916     }
1917
1918     /* Compute how many characters need to be escaped */
1919     s = (const unsigned char *)str;
1920     for (; *s; ++s) {
1921         if (TEST_CHAR(*s, T_ESCAPE_LOGITEM)) {
1922             escapes++;
1923         }
1924     }
1925
1926     /* Compute the length of the input string, including NULL */
1927     length = s - (const unsigned char *)str + 1;
1928
1929     /* Fast path: nothing to escape */
1930     if (escapes == 0) {
1931         return apr_pmemdup(p, str, length);
1932     }
그리고 str과 s와의 주소값 차이 + 1 로 문자열의 길이를 계산한다.
escape되야 할 문자 수가 0이라면 apr_pmemdup함수로 문자열을 복사한 뒤 return한다.
이때 ""이 반환되어야 한다.

싱글 쓰레드라면 문제가 없어 보이지만 멀티 쓰레드 환경에서 다음 상황이라면 문제가 될 부분이 있다.

  1. ap_escape_logitem(pool, "") // 빈 문자열을 받는다.
  2. escape 수를 셀 때 루프를 바로 빠져나오고 escapes는 0인 상태이다.
  3. 다른 쓰레드에서 s 포인터가 가리키는 곳의 값을 바꾼다.
  4. apr_pmemdup가 그 바뀐 문자열을 복사한 뒤 return한다.

위 시나리오대로라면 ap_escape_logitem이 문자열을 리턴하는데 그 문자열이 ""이 아니다.

modules/generators/mod_status.c 코드는 833 line에서
ap_escape_html(r->pool, ap_escape_logitem(r->pool, ws_record->request))
를 부르는 게 있다.
include/scoreboard.h에 정의된 worker_score라는 struct에는 char request[64]가 있는데 이 필드는 update_child_status_internal에 의해 불리는 copy_request 함수로 바뀔 수 있다.
이건 mod_status가 worker에서 동시에 돌면서 ap_escape_logitem이 다른 쓰레드에서 불릴 때 request 필드가 바뀔 수 있고 race condition을 유발한다.

두 개의 클라이언트가 실행되고, 한 클라이언트가 mod_status handler를 발생시키는데 다른 클라이언트가 웹 서버에 어떤 request를 보낼 때를 생각해보자.

  1. mod_status가 worker들에서 돌면서 server/scoreboard.c의 update_child_status_internal()를 실행시키다.
  2. 한 worker의 mod_status가 ap_escape_logitem(pool, ws_record->request) 함수를 호출한다.
  3. ws_record->request가 빈 문자열이어서 첫 문자가 '\0' 이라고 가정하자.
  4. ap_escape_logitem 함수에서 ws_record->request에 대한 length가 1로 계산된다.(빈 문자열이니까)
  5. 다른 쓰레드가 update_child_status_internal 함수에서 ws->request를 바꾸는데 그 주소가 ws_record->request와 같은 주소이므로 ws_record->request의 값이 바뀐다. 예를 들어 "GET /HTTP/1.0"으로 바뀐 상황을 보자.
  6. apr_pmemdup(pool, str, 1)이 첫 바이트를 복사하는데 원래는 '\0' 이 리턴돼야하지만 str이 바뀌었으므로 바뀐 문자열의 첫 1바이트인 "G"가 복사되고 그 문자를 리턴한다.
  7. apr_pmemdup(apr_pool_t *a, const void *m, apr_size_t n)함수에서 메모리 할당을 apr_palloc(apr_pool_t* , apr_size_t )로 하고 그 메모리를 NUL로 덮어쓴다. 복사된 "G" 바이트 뒤에 있는 값들에 대해서 체크하는 게 없다. apr_palloc으로 할당된 메모리가 dirty해서 random byte를 포함한다고 가정하자.
  8. ap_escape_logitem이 "G"+junk를 갖고 있는 문자열의 포인터를 return한다. 정상적인 작동이라면 뒤에 junk가 있더라도 '\0'+junk가 return돼야 한다.

위에서 얻어진 값이 server/util.c에 있는 ap_escape_html2 함수에 넣어진다.
1859 AP_DECLARE(char *) ap_escape_html2(apr_pool_t *p, const char *s, int toasc)
1860 { 
1861     int i, j;
1862     char *x;
1863   
1864     /* first, count the number of extra characters */
1865     for (i = 0, j = 0; s[i] != '\0'; i++)
1866         if (s[i] == '<' || s[i] == '>')
1867             j += 3;
1868         else if (s[i] == '&')
1869             j += 4;
1870         else if (s[i] == '"')
1871             j += 5;
1872         else if (toasc && !apr_isascii(s[i]))
1873             j += 5;
1874
1875     if (j == 0)
1876         return apr_pstrmemdup(p, s, i);
1877   
1878     x = apr_palloc(p, i + j + 1);
1879     for (i = 0, j = 0; s[i] != '\0'; i++, j++)
1880         if (s[i] == '<') {
1881             memcpy(&x[j], "&lt;", 4);
1882             j += 3;
1883         }
1884         else if (s[i] == '>') {
1885             memcpy(&x[j], "&gt;", 4);
1886             j += 3;
1887         }
1888         else if (s[i] == '&') {
1889             memcpy(&x[j], "&amp;", 5);
1890             j += 4;
1891         }
1892         else if (s[i] == '"') {
1893             memcpy(&x[j], "&quot;", 6);
1894             j += 5;
1895         }
1896         else if (toasc && !apr_isascii(s[i])) {
1897             char *esc = apr_psprintf(p, "&#%3.3d;", (unsigned char)s[i]);
1898             memcpy(&x[j], esc, 6);
1899             j += 5;
1900         }
1901         else
1902             x[j] = s[i];
1903   
1904     x[j] = '\0';
1905     return x;
1906 }
그러면 다음과 같이 동작한다.

  1. line 1865에서 시작된 for loop에서 escaped string의 길이를 계산한다. 이때 루프가 끝나는 지점은 '\0'를 만날 때 까지이다.
  2. s 문자열이 junk를 갖고 있기 때문에 '>' 문자를 갖고 있을 수 있다. 이 상황에 대해 생각해보자. 
  3. line 1866의 loop가 끝나고 적어도 하나의 s[i]가 '>'라고 가정했으므로 j는 0보다 크다.
  4. line 1879에서 escaped 'p'에 대한 메모리가 할당된다.
  5. line 1880의 루프에서 s 문자열을 escaped 'p' 문자열로 복사를 하지만 apr_palloc이 s에 대해 1 바이트만 할당을 했다. 그러므로 i>0일 때마다 루프는 random memory를 읽고 그 값을 x 문자열에 복사한다. 이는 information leak을 유발할 수 있다.


그런데 s 문자열이 x문자열과 겹칠 수 있다.
예를 들어 s가 0의 주소부터 "AAAAAAAA>"로 있고 x가 8의 주소라면 s[8] = d[0]이다.
이런 경우는 s[1]이 x[1]으로 복사될 때 그곳은 s[9]의 위치이기 때문에 원래 s[9]의 값이 A로 바뀌고, 이런식으로 무한히 루프를 돌게 된다.
unmapped memory나 read only area에 닿기 전까지 계속 돌게 된다.





결과
Tsan으로 충돌한 로그를 보면 mod_status.c의 ap_escape_logitem 함수와 scoreboard.c의 update_child_status_internal 함수가 찍혀있다.
이 race condition은
 ap_escape_logitem이 반환하는 문자열의 맨 마지막이 '\0'이 아닐 때 정보 유출이 일어난다.
 복사된 바이트 뒤에 오는 junk가 중요한 정보일 수 있다.
 유저가 힙에 덮어쓸 수 있다.


공격
힙 오버플로우 버그를 공격하기 위해서는
1. race condition 버그를 발생기킨다.
2. ap_escape_html2에서 s와 d에 대한 메모리를 할당한다.
3. s중에서 d와 겹치지 않는 부분. 이 문자열은 계속 복사가 된다.
4. 힙에 덮어써서 cpu 컨트롤을 얻는다던가 혹은 아파치의 핸들러 코드 flow를 바꾼다.








--------------------------------------
Reference
https://www.exploit-db.com/exploits/34133






댓글

이 블로그의 인기 게시물

논문 정리 - MapReduce: Simplified Data Processing on Large Clusters

논문 정리 - The Google File System

kazoo: Using zookeeper api with python