-
Notifications
You must be signed in to change notification settings - Fork 94
Description
Summary
There is a heap-based out-of-bounds read in the RLE encoder function RLE_ANALISE in src/rle.cpp. The bug is triggered by crafted input where the buffer length is small (for example, 1 byte). Under AddressSanitizer this manifests as a reproducible heap-buffer-overflow in RLE_ANALISE.
Affected version / commit
- Repository:
KranX/Vangers - Commit:
72145feed605856c6711bbbcb4f9db99db3434fd
Environment
- OS: Ubuntu 22.04.5 LTS
- Kernel: 5.15.0-160-generic
- Compiler: clang++ with AddressSanitizer enabled
Actual behavior
Running the RLE encoder on attacker-controlled input can cause a heap-buffer-overflow read in RLE_ANALISE. AddressSanitizer reports:
=================================================================
==14==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7c03af6e00b2 at pc 0x55cda1781938 bp 0x7ffc54b79cd0 sp 0x7ffc54b79cc8
READ of size 1 at 0x7c03af6e00b2 thread T0
SCARINESS: 12 (1-byte-read-heap-buffer-overflow)
#0 0x55cda1781937 in RLE_ANALISE(unsigned char*, int, unsigned char*&) /src/vangers_rle_fuzzer.cc:23:37
#1 0x55cda1781b7d in LLVMFuzzerTestOneInput /src/vangers_rle_fuzzer.cc:123:27
#2 0x55cda161eded in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:619:13
#3 0x55cda1609b62 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:329:6
#4 0x55cda160fa30 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:865:9
#5 0x55cda163b562 in main /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerMain.cpp:20:10
#6 0x7fe3b01a1082 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x24082)
#7 0x55cda1602c4d in _start (/out/vangers_rle_fuzzer+0x3cc4d)
0x7c03af6e00b2 is located 0 bytes after 2-byte region [0x7c03af6e00b0,0x7c03af6e00b2)
allocated by thread T0 here:
#0 0x55cda173d744 in malloc /src/llvm-project/compiler-rt/lib/asan/asan_malloc_linux.cpp:67:3
#1 0x55cda1782083 in operator new(unsigned long) cxa_noexception.cpp
#2 0x55cda1609b62 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:329:6
#3 0x55cda160fa30 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:865:9
#4 0x55cda163b562 in main /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerMain.cpp:20:10
#5 0x7fe3b01a1082 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x24082)
SUMMARY: AddressSanitizer: heap-buffer-overflow in RLE_ANALISE(unsigned char*, int, unsigned char*&)
=================================================================
Minimal reproducer
Below is a standalone reproducer based on the current src/rle.cpp. It calls RLE_ANALISE with a 1-byte input buffer, compiled with ASan:
#include <cstdint>
#include <cstring>
using uchar = unsigned char;
// Copy of Vangers/src/rle.cpp::RLE_ANALISE (unchanged)
int RLE_ANALISE(uchar* _buf, int len, uchar*& out) {
int i = 0;
int pack_len = 0;
uchar c_len = 0;
uchar* buf = _buf;
uchar* _out = new uchar[len*2];
uchar* p = _out;
uchar _ch = *buf++;
while (i < len) {
while ((i < len) && (_ch == *buf) && (c_len < 127)) {
c_len++;
buf++;
i++;
}
if (c_len) {
*p++ = c_len;
*p++ = _ch;
_ch = *buf++;
pack_len += 2;
c_len = 0;
i++;
}
while ((i < len) && (_ch != *buf) && (c_len < 127)) {
c_len++;
_ch = *buf++;
i++;
}
if (c_len) {
*p++ = 128 + (c_len - 1);
std::memcpy(p, buf - c_len - 1, c_len);
p += c_len;
pack_len += c_len + 1;
c_len = 0;
}
}
out = new uchar[pack_len];
std::memcpy(out, _out, pack_len);
delete[] _out;
return pack_len;
}
int main() {
uchar in[1] = {0x40}; // any value is sufficient
uchar* out = nullptr;
RLE_ANALISE(in, 1, out);
delete[] out;
return 0;
}
Build & run (for example):
clang++ -O1 -g -fsanitize=address -fno-omit-frame-pointer poc.cpp -o poc
./poc
On my environment this reliably reports a heap-buffer-overflow in RLE_ANALISE.
Root cause analysis
The core issue is that RLE_ANALISE consumes one byte from _buf and advances buf before the main loop, but does not update the loop counter i accordingly:
int i = 0;
uchar* buf = _buf;
uchar _ch = *buf++; // consumes _buf[0], buf now points to _buf + 1
At this point:
buf == _buf + 1i == 0len >= 1(for the reproducer,len == 1)
Then the outer loop starts:
while (i < len) {
while ((i < len) && (_ch == *buf) && (c_len < 127)) {
c_len++;
buf++;
i++;
}
...
}
For len == 1, the condition i < len is true (0 < 1), so the inner while evaluates (_ch == *buf). But buf already points one past the end of the input buffer (_buf + 1), so *buf dereferences memory just beyond the valid region. This causes a 1-byte heap out-of-bounds read on the input buffer.
In other words, the logical counter i and the pointer buf are out of sync after the initial *buf++:
- The function has already consumed 1 byte from
_buf, - but
istill indicates “0 bytes processed”, so thei < lencheck does not prevent the out-of-bounds access.
Additionally, the function unconditionally does uchar _ch = *buf++; even when len might be 0, so calling it with len == 0 and a null/invalid _buf would also be undefined behavior. There is no defensive check on len at function entry.
Suggested fix
One straightforward fix is to keep i consistent with the number of bytes consumed from _buf. For example, after the initial read:
int i = 0;
uchar* buf = _buf;
if (len <= 0) {
out = nullptr;
return 0;
}
uchar _ch = *buf++;
i = 1; // keep i in sync with buf
Alternatively, the loops can be rewritten to use pointer comparisons against _buf + len (e.g. buf < _buf + len) instead of relying on i.
Impact
From a security perspective this is a heap-based out-of-bounds read that can cause a crash (denial of service) when processing untrusted input that reaches RLE_ANALISE. In practice, the observed impact is a process crash under ASan. Since the read is only one byte beyond the input buffer, information disclosure potential is limited, but the behavior is still undefined in normal builds.