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돼야 할 문자의 수를 센다.
escape되야 할 문자 수가 0이라면 apr_pmemdup함수로 문자열을 복사한 뒤 return한다.
이때 ""이 반환되어야 한다.
싱글 쓰레드라면 문제가 없어 보이지만 멀티 쓰레드 환경에서 다음 상황이라면 문제가 될 부분이 있다.
위 시나리오대로라면 ap_escape_logitem이 문자열을 리턴하는데 그 문자열이 ""이 아니다.
modules/generators/mod_status.c 코드는 833 line에서
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를 보낼 때를 생각해보자.
위에서 얻어진 값이 server/util.c에 있는 ap_escape_html2 함수에 넣어진다.
그런데 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
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)그리고 str과 s와의 주소값 차이 + 1 로 문자열의 길이를 계산한다.
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 }
escape되야 할 문자 수가 0이라면 apr_pmemdup함수로 문자열을 복사한 뒤 return한다.
이때 ""이 반환되어야 한다.
싱글 쓰레드라면 문제가 없어 보이지만 멀티 쓰레드 환경에서 다음 상황이라면 문제가 될 부분이 있다.
- ap_escape_logitem(pool, "") // 빈 문자열을 받는다.
- escape 수를 셀 때 루프를 바로 빠져나오고 escapes는 0인 상태이다.
- 다른 쓰레드에서 s 포인터가 가리키는 곳의 값을 바꾼다.
- 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를 보낼 때를 생각해보자.
- mod_status가 worker들에서 돌면서 server/scoreboard.c의 update_child_status_internal()를 실행시키다.
- 한 worker의 mod_status가 ap_escape_logitem(pool, ws_record->request) 함수를 호출한다.
- ws_record->request가 빈 문자열이어서 첫 문자가 '\0' 이라고 가정하자.
- ap_escape_logitem 함수에서 ws_record->request에 대한 length가 1로 계산된다.(빈 문자열이니까)
- 다른 쓰레드가 update_child_status_internal 함수에서 ws->request를 바꾸는데 그 주소가 ws_record->request와 같은 주소이므로 ws_record->request의 값이 바뀐다. 예를 들어 "GET /HTTP/1.0"으로 바뀐 상황을 보자.
- apr_pmemdup(pool, str, 1)이 첫 바이트를 복사하는데 원래는 '\0' 이 리턴돼야하지만 str이 바뀌었으므로 바뀐 문자열의 첫 1바이트인 "G"가 복사되고 그 문자를 리턴한다.
- 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를 포함한다고 가정하자.
- 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], "<", 4);
1882 j += 3;
1883 }
1884 else if (s[i] == '>') {
1885 memcpy(&x[j], ">", 4);
1886 j += 3;
1887 }
1888 else if (s[i] == '&') {
1889 memcpy(&x[j], "&", 5);
1890 j += 4;
1891 }
1892 else if (s[i] == '"') {
1893 memcpy(&x[j], """, 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 }
- line 1865에서 시작된 for loop에서 escaped string의 길이를 계산한다. 이때 루프가 끝나는 지점은 '\0'를 만날 때 까지이다.
- s 문자열이 junk를 갖고 있기 때문에 '>' 문자를 갖고 있을 수 있다. 이 상황에 대해 생각해보자.
- line 1866의 loop가 끝나고 적어도 하나의 s[i]가 '>'라고 가정했으므로 j는 0보다 크다.
- line 1879에서 escaped 'p'에 대한 메모리가 할당된다.
- 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
댓글
댓글 쓰기