Introduction
Google의 오픈소스 라이브러리인 glog를 대상으로 Fuzzing을 수행하던 중 demangling process 과정에서 SEGV(segmentation fault) 관련 crash를 발견하였습니다.
해당 crash는 숫자가 포함되어 있는 mangling된 함수 이름을 demangling하는 과정이 주요 원인이였으며, 잘못된 주소 참조 이후 비교문을 수행하는 과정까지 이어집니다.
최종 분석 결과 crash가 발견되는 지점 이후 오류핸들러가 존재하였기 때문에 보안 취약점으로 이어질 수는 없었습니다.
Name Mangling / Deangling
네임 맹글링에 대한 개념을 말씀드리기에 앞서 c++ 언어에는 함수 오버로딩(funcation overloading) 라는 개념이 존재합니다. 이는 동일한 이름의 함수를 다른 매개변수로 여러 번 정의할 수 있도록 하는 기능이며 이는 코드의 가독성과 재사용성 향상에 도움됩니다.
하지만 해당 소스코드를 빌드하는 과정 중 링크 단계는 함수 이름을 이용하여 함수 호출자를 결정하기 때문에 앞선 상황에서 문제가 발생할 수 있습니다. 이러한 부분을 해소하기 위하여 컴파일러는 네임 맹글링(Name mangling)이라는 개념을 이용하여 각 오버로딩 함수를 고유하게 식별할 수 있도록 합니다. 결과적으로 오버로딩된 함수는 컴파일 단계에서 각각 고유한 이름을 가지게 되며 생성되는 이름의 규칙은 컴파일러마다 다르게 수행되어집니다.
아래는 네임 맹글링의 예시이며 gcc 버전 3.X 이상의 경우 네임 맹글링의 접두사로 "_Z" 가 사용되어 집니다.
// Example:
//
// | Mangled Name | The Demangler | abi::__cxa_demangle()
// |---------------|---------------|-----------------------
// | _Z1fv | f() | f()
// | _Z1fi | f() | f(int)
// | _Z3foo3bar | foo() | foo(bar)
// | _Z1fIiEvi | f<>() | void f<int>(int)
// | _ZN1N1fE | N::f | N::f
// | _ZN3Foo3BarEv | Foo::Bar() | Foo::Bar()
// | _Zrm1XS_" | operator%() | operator%(X, X)
// | _ZN3FooC1Ev | Foo::Foo() | Foo::Foo()
// | _Z1fSs | f() | f(std::basic_string<char,
// | | | std::char_traits<char>,
// | | | std::allocator<char> >)
//
Fuzz Information
Summary
Project Name | glog |
Fuzzer | libfuzzer |
Fuzz binary | fuzz_demangle |
Sanitizer | ASAN |
Crash Type | SEGV |
Crash State | /src/glog/src/demangle.cc:162:7 in ParseOneCharToken |
ASAN Log
AddressSanitizer:DEADLYSIGNAL
=================================================================
==405==ERROR: AddressSanitizer: SEGV on unknown address 0x7ffe4c6442c1 (pc 0x0000005738f2 bp 0x7ffea1b995d0 sp 0x7ffea1b99500 T0)
==405==The signal is caused by a READ memory access.
SCARINESS: 20 (wild-addr-read)
#0 0x5738f2 in ParseOneCharToken /src/glog/src/demangle.cc:162:7
#1 0x5738f2 in ParseAbiTag /src/glog/src/demangle.cc:673:10
#2 0x5738f2 in OneOrMore /src/glog/src/demangle.cc:202:7
#3 0x5738f2 in ParseAbiTags /src/glog/src/demangle.cc:663:7
#4 0x5738f2 in ParseUnqualifiedName /src/glog/src/demangle.cc:555:47
#5 0x5738f2 in google::ParseUnscopedName(google::State*) /src/glog/src/demangle.cc:480:7
#6 0x56f790 in ParseUnscopedTemplateName /src/glog/src/demangle.cc:497:10
#7 0x56f790 in google::ParseName(google::State*) /src/glog/src/demangle.cc:464:7
#8 0x56c940 in ParseEncoding /src/glog/src/demangle.cc:443:7
#9 0x56c940 in ParseMangledName /src/glog/src/demangle.cc:435:44
#10 0x56c940 in ParseTopLevelMangledName /src/glog/src/demangle.cc:1252:7
#11 0x56c940 in google::Demangle(char const*, char*, unsigned long) /src/glog/src/demangle.cc:1306:10
#12 0x56c534 in LLVMFuzzerTestOneInput /src/glog/src/fuzz_demangle.cc:30:3
#13 0x43ddb3 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:611:15
#14 0x429512 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:324:6
#15 0x42edbc in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:860:9
#16 0x4582f2 in main /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerMain.cpp:20:10
#17 0x7f3c68a56082 in __libc_start_main /build/glibc-SzIz7B/glibc-2.31/csu/../csu/libc-start.c:308:16
#18 0x41f6dd in _start (/out/glog/fuzz_demangle+0x41f6dd)
DEDUP_TOKEN: ParseOneCharToken--ParseAbiTag--OneOrMore
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /src/glog/src/demangle.cc:162:7 in ParseOneCharToken
==405==ABORTING
Enviroment
Fuzzing Enviroment | gcr.io/oss-fuzz/glog |
Debugging Enviroment | gcr.io/oss-fuzz/base-runner-debug |
#include <cstring>
#include "demangle.h"
extern "C" int LLVMFuzzerTestOneInput(const unsigned char *Data,
unsigned Size) {
if (Size >= 4095) {
return 0;
}
char Buffer[Size + 1];
std::memcpy(Buffer, Data, Size);
Buffer[Size] = 0;
char demangled[4096];
google::Demangle(Buffer, demangled, Size);
return 0;
}
Analyze
stuct State information:
struct State {
const char *mangled_cur; // Cursor of mangled name.
char *out_cur; // Cursor of output string.
const char *out_begin; // Beginning of output string.
const char *out_end; // End of output string.
const char *prev_name; // For constructors/destructors.
ssize_t prev_name_length; // For constructors/destructors.
short nest_level; // For nested names.
bool append; // Append flag.
bool overflowed; // True if output gets overflowed.
};
Crash state tracing:
1. ParseSourceName 함수는 사용자의 입력 데이터(input data)에서 문자와 숫자를 parsing하는 함수를 호출합니다. ParseNumber 함수에는 input data에 포함되어 있는 ASCII 형태의 숫자를 정수로 변환하는 과정이 존재합니다.
이 때 변환되어진 결과값은 length 변수에 저장됩니다.
static bool ParseSourceName(State *state) {
State copy = *state;
int length = -1;
if (ParseNumber(state, &length) && ParseIdentifier(state, length)) {
return true;
}
*state = copy;
return false;
}
static bool ParseNumber(State *state, int *number_out) {
int sign = 1;
if (ParseOneCharToken(state, 'n')) {
sign = -1;
}
const char *p = state->mangled_cur;
int number = 0;
for (;*p != '\0'; ++p) {
if (IsDigit(*p)) {
/* number is the user input data, and there is no limit on the number of digits. */
number = number * 10 + (*p - '0');
} else {
break;
}
}
if (p != state->mangled_cur) { // Conversion succeeded.
state->mangled_cur = p;
if (number_out != nullptr) {
*number_out = number * sign;
}
return true;
}
return false;
}
2. 다음으로 호출되어지는 ParseIdentifier 함수는 length 변수에 저장된 값을 현재 데이터의 위치를 가르키는 char형 포인터(state->mangled_cur)에 더하는 작업을 수행합니다.
static bool ParseIdentifier(State *state, ssize_t length) {
if (length == -1 ||
!AtLeastNumCharsRemaining(state->mangled_cur, length)) {
return false;
}
if (IdentifierIsAnonymousNamespace(state, length)) {
MaybeAppend(state, "(anonymous namespace)");
} else {
MaybeAppendWithLength(state, state->mangled_cur, length);
}
/* The current pointer position is determined by the previously recorded number information. */
state->mangled_cur += length;
return true;
}
3. 이후 crash는 ParseOneCharToken 함수가 호출되어진 후 state-mangled_cur[0] 지점에서 발생하게 됩니다. 사용자의 input data에 따라 state->mangled_cur가 가르키는 주소가 잘못된 주소를 참조시킬 수 있기 때문입니다.
// <abi-tags> ::= <abi-tag> [<abi-tags>]
static bool ParseAbiTags(State *state) {
State copy = *state;
DisableAppend(state);
if (OneOrMore(ParseAbiTag, state)) {
RestoreAppend(state, copy.append);
return true;
}
*state = copy;
return false;
}
static bool ParseAbiTag(State *state) {
return ParseOneCharToken(state, 'B') && ParseSourceName(state);
}
static bool ParseOneCharToken(State *state, const char one_char_token) {
if (state->mangled_cur[0] == one_char_token) {
++state->mangled_cur;
return true;
}
return false;
}
state->mangled_cur 의 값은 현재 구조체의 위치와 length 변수 값의 합이기 때문에 사용자가 제어할 수 있습니다. 앞서 언급했듯 length 는 input data를 통해 조작할 수 있기 때문입니다.
state->mangled_cur += length;
Debug