Talos Vulnerability Report

TALOS-2017-0456

Blender draw_new_particle_system PART_DRAW_AXIS Integer Overflow Code Execution Vulnerability

January 11, 2018
CVE Number

CVE-2017-12104

Summary

An exploitable integer overflow exists in the way that the Blender open-source 3d creation suite v2.78c draws a Particle object. A specially crafted .blend file can cause an integer overflow resulting in a buffer overflow which can allow for code execution under the context of the application. An attacker can convince a user to open the file or use the file as a library in order to trigger this vulnerability.

Tested Versions

Blender v2.78c (32-bit)

Product URLs

http://www.blender.org git://git.blender.org/blender.git

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

Blender is a professional, open-source 3d computer graphics application. It is used for creating animated films, visual effects, art, 3d printed applications, and video games. It is also capable of doing minimalistic video editing and sequencing as needed by the user. There are various features that it provides which allow for a user to perform a multitude of actions as required by a particular project.

This vulnerability exists with how the Blender application draws a ParticleSystem object as a PART_DRAW_AXIS(4) type. When allocating space for the number of particles, the application will perform some arithmetic which can overflow. This result will then be used to perform an allocation which can allow for the resulting heap-buffer to be undersized. When the application tries to write values into this undersized buffer, a heap-based buffer overflow will occur.

When drawing an object, the application will call the following function. The draw_object function will figure out whether or not to draw particles or the object that the particles are emitted from. At [1], the application will first check to see if the particlesystem field contains any elements in its linked list. This is used to set the has_particles flag. The provided proof-of-concept attempts to skip unnecessary code when triggering the vulnerability and so various flags were set in order to achieve the desired result. The flags specified for the ‘mode’ field are set to zero which results in ob->mode being equivalent to OB_MODE_OBJECT(0). This causes the skip_object field to be set to true which skips rendering some objects. Eventually at [3], the application will iterate through all the ParticleSystem objects within the Object and hand it off to the draw_new_particle_system function.

source/blender/editors/space_view3d/drawobject.c:7455
void draw_object(Scene *scene, ARegion *ar, View3D *v3d, Base *base, const short dflag)
{
...
    Object *ob = base->object;
...
    const bool is_obact = (ob == OBACT);
    const bool render_override = (v3d->flag2 & V3D_RENDER_OVERRIDE) != 0;
    const bool is_picking = (G.f & G_PICKSEL) != 0;
    const bool has_particles = (ob->particlesystem.first != NULL);                  // [1]
    bool skip_object = false;  /* Draw particles but not their emitter object. */
...
    if (has_particles) {
...
        if (ob->mode == OB_MODE_OBJECT) {
            ParticleSystem *psys;

            skip_object = render_override;                                          // [2]
            for (psys = ob->particlesystem.first; psys; psys = psys->next) {
                /* Once we have found a psys which renders its emitter object, we are done. */
                if (psys->part->draw & PART_DRAW_EMITTER) {
                    skip_object = false;
                    break;
                }
            }
        }
    }
...
    if ((dflag & DRAW_PICKING) == 0 && (base->flag & OB_FROMDUPLI) == 0 && (v3d->flag2 & V3D_RENDER_SHADOW) == 0) {
        /* don't do xray in particle mode, need the z-buffer */
        if (!(ob->mode & OB_MODE_PARTICLE_EDIT)) {
...
    }
...
    if ((ob->particlesystem.first) &&
        (ob != scene->obedit))
    {
        ParticleSystem *psys;
...
        for (psys = ob->particlesystem.first; psys; psys = psys->next) {
...
            draw_new_particle_system(scene, v3d, rv3d, base, psys, dt, dflag);      // [3]
        }

At the beginning of the draw_new_particle_system function, the application will assign a few variables and then check some flags of the respective objects. The provided proof-of-concept sets the part->draw_as field to the value PART_DRAW_REND(10). This results in the part->ren_as field being assigned to the draw_as variable [4]. The shortest path discovered by the author to the vulnerability was to use PART_DRAW_AXIS(4) as the value for part->ren_as. A little bit later at [5], the application will use some fields from both the ParticleSystem and ParticleSettings objects to perform a calculation. This uses the psys->totchild field and the part->disp field to calculate the total number of children. After that, the application will assign the psys->totpart field to the local totpart variable [6]. It is these three fields that are used by the proof-of-concept to trigger the vulnerability. After assigning those variables, at [7] the application will assign a true value to the create_cdata field. This variable is used to control the case for the allocation that contains the vulnerability. Finally at [8], the application will perform some arithmetic that is used to make the vulnerable allocation. This code may already result in an overflow, but at both [9] and [10] are more calculations that influence the buffer that will be allocated. If the result of the arithmetic at [8], [9], and [10] is larger than 32-bits then an integer overflow will occur. Due to this integer overflow, the allocations at [11] can all be made to be undersized. This can result in a heap-based buffer overflow when either of them are used.

source/blender/editors/space_view3d/drawobject.c:4995
static void draw_new_particle_system(Scene *scene, View3D *v3d, RegionView3D *rv3d,
                                     Base *base, ParticleSystem *psys,
                                     const char ob_dt, const short dflag)
{
    Object *ob = base->object;
...
    ParticleSettings *part = psys->part;
    ParticleData *pars = psys->particles;
...
    if (part->draw_as == PART_DRAW_REND)                                // [4]
        draw_as = part->ren_as;
    else
        draw_as = part->draw_as;

    if (draw_as == PART_DRAW_NOT)
        return;
...
    if (part->type == PART_HAIR && !psys->childcache)
        totchild = 0;
    else
        totchild = psys->totchild * part->disp / 100;                   // [5]
...
    totpart = psys->totpart;                                            // [6]
...
    switch (draw_as) {
...
        case PART_DRAW_CROSS:
        case PART_DRAW_AXIS:
...
            if (draw_as == PART_DRAW_AXIS)
                create_cdata = 1;                                       // [7]
            break;
...
    }
...
    if (ELEM(draw_as, PART_DRAW_DOT, PART_DRAW_CROSS, PART_DRAW_LINE) &&
        (part->draw_col > PART_DRAW_COL_MAT))
    {
        create_cdata = 1;                                               // [7]
    }
...
    if (draw_as && ELEM(draw_as, PART_DRAW_PATH, PART_DRAW_CIRC) == 0) {
        int tot_vec_size = (totpart + totchild) * 3 * sizeof(float);    // [8]
...
        if (part->draw_as == PART_DRAW_REND && part->trail_count > 1) {
            tot_vec_size *= part->trail_count;                          // [9]
            psys_make_temp_pointcache(ob, psys);
        }

        switch (draw_as) {
            case PART_DRAW_AXIS:
            case PART_DRAW_CROSS:
                tot_vec_size *= 6;                                      // [10]
                if (draw_as != PART_DRAW_CROSS)
                    create_cdata = 1;
                break;
            case PART_DRAW_LINE:
                tot_vec_size *= 2;                                      // [10]
                break;
            case PART_DRAW_BB:
                tot_vec_size *= 4;                                      // [10]
                create_ndata = 1;
                break;
        }
...
        if (!pdd->vdata)
            pdd->vdata = MEM_callocN(tot_vec_size, "particle_vdata");   // [11]
        if (create_cdata && !pdd->cdata)
            pdd->cdata = MEM_callocN(tot_vec_size, "particle_cdata");   // [11]
        if (create_ndata && !pdd->ndata)
            pdd->ndata = MEM_callocN(tot_vec_size, "particle_ndata");   // [11]

        if (part->draw & PART_DRAW_VEL && draw_as != PART_DRAW_LINE) {
            if (!pdd->vedata)                                           // [11]
                pdd->vedata = MEM_callocN(2 * (totpart + totchild) * 3 * sizeof(float), "particle_vedata");

            need_v = 1;
        }

Continuing execution after the vulnerable allocation is made, the application will check to see if the draw_as variable is not equal to PART_DRAW_PATH(6) [12]. After verifying a few more fields and that the part->trail_count of the ParticleSettings field is larger than 1, then the function draw_particle_data at [13] will be called. The draw_particle_data function is a very simple function that will specially handle the PART_DRAW_BB(9) constant, and then wrap the call to the draw_particle function at [14].

source/blender/editors/space_view3d/drawobject.c:5255
    if ((pdd || draw_as == PART_DRAW_CIRC) && draw_as != PART_DRAW_PATH) {                  // [12]
...
        if (pdd && (pdd->flag & PARTICLE_DRAW_DATA_UPDATED) &&
            (pdd->vedata || part->draw & (PART_DRAW_SIZE | PART_DRAW_NUM | PART_DRAW_HEALTH)) == 0)
        {
            totpoint = pdd->totpoint; /* draw data is up to date */
        }
        else {
...
                drawn = 0;
                if (part->draw_as == PART_DRAW_REND && part->trail_count > 1) {
...
                else {
                    state.time = cfra;
                    if (psys_get_particle_state(&sim, a, &state, 0)) {

                        draw_particle_data(psys, rv3d,                                      // [13] \
                                           &state, draw_as, imat, &bb, psys->pdd,
                                           pa_time, pa_size, r_tilt, pixsize_scale);

                        totpoint++;
                        drawn = 1;
                    }
                }
\
source/blender/editors/space_view3d/drawobject.c:4948
static void draw_particle_data(ParticleSystem *psys, RegionView3D *rv3d,
                               ParticleKey *state, int draw_as,
                               float imat[4][4], ParticleBillboardData *bb, ParticleDrawData *pdd,
                               const float ct, const float pa_size, const float r_tilt, const float pixsize_scale)
{
...
    if (draw_as == PART_DRAW_BB) {
...
    ]

    pixsize = ED_view3d_pixel_size(rv3d, state->co) * pixsize_scale;

    draw_particle(state, draw_as, part->draw, pixsize, imat, part->draw_line, bb, pdd);     // [14]
}

Finally inside the draw_particle function, the application will assign a pointer from the pdd->vd field. This is one of the undersized heap-buffers that was allocated earlier. Immediately afterwards, the application will check the draw_as argument [15]. If draw_as argument is set to PART_DRAW_AXIS(4) then the application will write to the pointer that was assigned at [15]. Due to the heap-buffers being undersized as a result of the 32-bit integer overflow, this will result in a heap-based buffer overflow which can lead to code execution under the context of the application.

source/blender/editors/space_view3d/drawobject.c:4799
static void draw_particle(ParticleKey *state, int draw_as, short draw, float pixsize,
                          float imat[4][4], const float draw_line[2], ParticleBillboardData *bb, ParticleDrawData *pdd)
{
...
    if (pdd) {
        vd = pdd->vd;                                           // [15]
        cd = pdd->cd;

        if (pdd->ma_col) {
            copy_v3_v3(ma_col, pdd->ma_col);
        }
    }
...
    switch (draw_as) {
...
        case PART_DRAW_CROSS:
        case PART_DRAW_AXIS:                                    // [16]
        {
            vec[0] = 2.0f * pixsize;
            vec[1] = vec[2] = 0.0;
            mul_qt_v3(state->rot, vec);
            if (draw_as == PART_DRAW_AXIS) {
                if (cd) {
                    cd[1] = cd[2] = cd[4] = cd[5] = 0.0;        // [17]
                    cd[0] = cd[3] = 1.0;
                    cd[6] = cd[8] = cd[9] = cd[11] = 0.0;
                    cd[7] = cd[10] = 1.0;
                    cd[13] = cd[12] = cd[15] = cd[16] = 0.0;
                    cd[14] = cd[17] = 1.0;
                    pdd->cd += 18;
                }

                copy_v3_v3(vec2, state->co);
            }
...

Crash Information

(1dfc.3750): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00a7f6e8 ebx=00a7f6d0 ecx=00a7f510 edx=304f6ffc esi=1dbeaffc edi=2ffdcfcc
eip=01395c56 esp=00a7f4e8 ebp=00a7f550 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010206
blender!osl_floor_ff+0xabb96:
01395c56 c7461400000000  mov     dword ptr [esi+14h],0 ds:002b:1dbeb010=????????

0:000> !heap -p -a @esi
    address 1dbeaffc found in
    _DPH_HEAP_ROOT @ b21000
    in busy allocation (  DPH_HEAP_BLOCK:         UserAddr         UserSize -         VirtAddr         VirtSize)
                                1db116b4:         1dbeaff8                4 -         1dbea000             2000
    6ace8d9c verifier!AVrfDebugPageHeapAllocate+0x0000023c
    77d6fb79 ntdll!RtlDebugAllocateHeap+0x00000032
    77d09a73 ntdll!RtlpAllocateHeap+0x0003914a
    77cd0b43 ntdll!RtlAllocateHeap+0x0000014c
    02d8f5b3 blender!xmlGetThreadId+0x0000c0b3
    02d75fc1 blender!xmlListWalk+0x00223241
    01c2d129 blender!osl_texture_set_swrap_code+0x00171e99
    013920f6 blender!osl_floor_ff+0x000a8036
    013947e5 blender!osl_floor_ff+0x000aa725
    01381a6f blender!osl_floor_ff+0x000979af
    01383101 blender!osl_floor_ff+0x00099041
    01382863 blender!osl_floor_ff+0x000987a3
    0154ac30 blender!osl_texture_set_twidth+0x000e25f0
    012e678c blender!xmlFileMatch+0x000202bc
    012e5d83 blender!xmlFileMatch+0x0001f8b3
    012cc58f blender!xmlFileMatch+0x000060bf
    012c9b1b blender!xmlFileMatch+0x0000364b
    02d6a125 blender!xmlListWalk+0x002173a5
    75f7919f KERNEL32!BaseThreadInitThunk+0x0000000e
    77cda8cb ntdll!__RtlUserThreadStart+0x00000020
    77cda8a1 ntdll!_RtlUserThreadStart+0x0000001b

0:000> u .
blender!osl_floor_ff+0xabb96:
01395c56 c7461400000000  mov     dword ptr [esi+14h],0
01395c5d c7461000000000  mov     dword ptr [esi+10h],0
01395c64 c7460800000000  mov     dword ptr [esi+8],0
01395c6b c7460400000000  mov     dword ptr [esi+4],0
01395c72 c7460c0000803f  mov     dword ptr [esi+0Ch],3F800000h
01395c79 c7060000803f    mov     dword ptr [esi],3F800000h
01395c7f c7462c00000000  mov     dword ptr [esi+2Ch],0
01395c86 c7462400000000  mov     dword ptr [esi+24h],0

0:000> lm a blender.exe
Browse full module list
start    end        module name
01130000 04a98000   blender  C (export symbols)       \path\to\blender.exe

Exploit Proof-of-Concept

Included with this advisory is a generator for the vulnerability. This proof-of-concept requires python and takes a single-argument which is the filename to write the .blend file to.

$ python poc.py.zip $FILENAME.blend

To trigger the vulnerability, one can simply open the file, treat it as a library and import the Curve object, or pass it as an argument to the blender executable.

$ /path/to/blender.exe $FILENAME.blend

Mitigation

In order to mitigate this vulnerability, it is recommended to not use untrusted .blend files within the application.

Timeline

2017-09-27 - Vendor Disclosure
2018-01-11 - Public Release

Credit

Discovered by a member of Cisco Talos.