An exploitable integer overflow vulnerability exists in the PlanMaker document parsing functionality of SoftMaker Office 2021’s PlanMaker application. A specially crafted document can cause the document parser perform arithmetic that may overflow which can result in an undersized heap allocation. Later when copying data from the file into this allocation, a heap-based buffer overflow will occur which can corrupt memory. These types of memory corruptions can allow for code execution under the context of the application. An attacker can entice the victim to open a document to trigger this vulnerability.
SoftMaker Software GmbH SoftMaker Office PlanMaker 2021 (Revision 1014)
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
SoftMaker Software GmbH is a German software company that develops and releases office software. Their flagship product, SoftMaker Office, is supported on a variety of platforms and contains a handful of components which can allow the user to perform a multitude of tasks such as word processing, spreadsheets, presentation design, and even allows for scripting. Thus the SoftMaker Office suite supports a variety of common office file formats, as well as a number of internal formats that the user may choose to use when performing their necessary work.
The PlanMaker component of SoftMaker’s suite is designed as an all-around spreadsheet tool, and supports of a number of features that allow it to remain competitive with similar office suites that are developed by its competitors. Although the application includes a number of parsers that enable the user to interact with these common document types or templates, a native document format is also included. This undocumented format is labeled as a PlanMaker Document, and will typically have the extension “.pmd” when saved as a file. The PlanMaker Document file format is based on Microsoft’s Compound Document file format and contains two streams, one of which is the “PMW” stream and then the “PMW Objects” stream.
Once the application unpacks the “PMW” stream, it will check the first few records of the stream in order to fingerprint the document and verify the stream if of the correct format. After this confirmation, the application will then execute the following function to read all of the records in the stream. At , the function will take an object containing the state and the stream to parse records from in order to store them on the stack. Later, the function will enter a loop at  which is responsible for continuously iterating through all of the records in the stream and then parsing them. The function call at  is responsible for parsing a general record. This function will return a pointer to the record’s contents at .
0x682f8d: push %rbp 0x682f8e: mov %rsp,%rbp 0x682f91: sub $0x300,%rsp 0x682f98: mov %rdi,-0x2e8(%rbp) ;  record object 0x682f9f: mov %rsi,-0x2f0(%rbp) ;  stream object 0x682fa6: mov %edx,-0x2f4(%rbp) 0x682fac: mov %fs:0x28,%rax 0x682fb5: mov %rax,-0x8(%rbp) 0x682fb9: xor %eax,%eax ... 0x6830bc: movl $0x0,-0x2cc(%rbp) ;  beginning of loop 0x6830c6: mov -0x2c8(%rbp),%r9 0x6830cd: lea -0x2dc(%rbp),%r8 0x6830d4: lea -0x2de(%rbp),%rcx 0x6830db: lea -0x2e0(%rbp),%rdx 0x6830e2: mov -0x2f0(%rbp),%rsi ; stream 0x6830e9: mov -0x2e8(%rbp),%rax ; record object 0x6830f0: sub $0x8,%rsp 0x6830f4: lea -0x2d8(%rbp),%rdi 0x6830fb: push %rdi 0x6830fc: mov %rax,%rdi 0x6830ff: callq 0x61e4a8 ;  parse record 0x683104: add $0x10,%rsp 0x683108: mov %rax,-0x2c8(%rbp) ;  save pointer to record ... 0x683313: cmpl $0x0,-0x2cc(%rbp) 0x68331a: jne 0x6830bc
Within the aforementioned loop, there’s a number of sub-loops that are responsible for checking the record’s type and using it to dispatch to the correct handler for the record to parse. Once one of the loops finds a handler for the current record type, code similar to the following is executed. This code will calculate an offset into the current function’s stack frame, and use it to find an index to one of the record handlers. Once the pointer has been calculated, the record’s contents and state are passed to the function call at .
0x68321d: mov -0x2d0(%rbp),%eax 0x683223: cltq 0x683225: shl $0x4,%rax 0x683229: add %rbp,%rax 0x68322c: sub $0x218,%rax ; point to function pointer array on stack. 0x683232: mov (%rax),%rax 0x683235: mov -0x2c8(%rbp),%rcx ; record contents 0x68323c: mov -0x2e8(%rbp),%rdx ; record object 0x683243: mov %rcx,%rsi 0x683246: mov %rdx,%rdi 0x683249: callq *%rax ;  dispatch to record handler 0x68324b: test %eax,%eax 0x68324d: sete %al 0x683250: test %al,%al 0x683252: jne 0x68338d
When either record types 0x8011 or 0x820a are parsed, the following function will be used to process their contents. After storing the parsing state and a pointer to the current record in the frame, at  the application will shift the pointer to the record past the uint16_t record type, and a uint16_t length. Afterwards the application will store 0x1c into the
%eax register and then at  will check if the record type is 0x8011. If it’s not, then the record type is 0x820a and at  the application will subtract 2 from the prior calculated constant. Finally at , the application will read a uint32_t from offset +0x12 of the record’s contents. This uint32_t is explicitly trusted and will later be used in a signed multiply which can result in an integer overflow.
0x67eced: push %rbp 0x67ecee: mov %rsp,%rbp 0x67ecf1: push %rbx 0x67ecf2: sub $0x1f8,%rsp 0x67ecf9: mov %rdi,-0x1f8(%rbp) ; object 0x67ed00: mov %rsi,-0x200(%rbp) ; record data 0x67ed07: mov %fs:0x28,%rax 0x67ed10: mov %rax,-0x18(%rbp) ... 0x67ed27: mov -0x200(%rbp),%rax ; record data 0x67ed2e: add $0x4,%rax ; shift past record length and type 0x67ed32: mov %rax,-0x1c8(%rbp) ;  store it as the record contents 0x67ed39: mov $0x18,%eax 0x67ed3e: add $0x4,%eax 0x67ed41: mov %eax,-0x1d8(%rbp) 0x67ed47: mov -0x200(%rbp),%rax ; record data 0x67ed4e: movzwl (%rax),%eax ; read record type 0x67ed51: mov %ax,-0x1e2(%rbp) 0x67ed58: cmpw $0x8011,-0x1e2(%rbp) ;  check if its 0x8011 0x67ed61: jne 0x67ed76 ... 0x67ed63: mov -0x1d8(%rbp),%eax 0x67ed69: mov $0x2,%edx ;  if type is 0x8201, then subtract 2 from constant 0x67ed6e: sub %edx,%eax 0x67ed70: mov %eax,-0x1d8(%rbp) ... 0x67ed76: mov -0x1c8(%rbp),%rax ; record contents 0x67ed7d: mov 0x12(%rax),%eax ;  read uint32_t from record's contents at +0x12 0x67ed80: test %eax,%eax 0x67ed82: je 0x67f12d
After reading the uint32_t, the following code will be executed. At , the application will again read the uint32_t at offset +0x12 of the record’s contents, and multiply it by 8. Due to the application explicitly trusting the uint32_t, this multiplication can overflow resulting in a smaller value than intended. At , this undersized value is then passed as a size to a function responsible for allocating a buffer. This results in an undersized heap allocation which is then stored into a pointer.
0x67eeaf: mov $0x8,%edx 0x67eeb4: mov -0x1c8(%rbp),%rax ; record contents 0x67eebb: mov 0x12(%rax),%eax ;  read uint32_t from +0x12 of record 0x67eebe: imul %eax,%edx ;  multiply by 8 and save in %edx ... 0x67eec1: mov -0x1d0(%rbp),%rax 0x67eec8: mov 0x8(%rax),%rax 0x67eecc: mov %edx,%esi ; pass multiplication result as size 0x67eece: mov %rax,%rdi 0x67eed1: callq 0xab7a01 ;  allocate buffer 0x67eed6: mov %rax,%rdx 0x67eed9: mov -0x1c0(%rbp),%rax ;  store pointer to allocation
After allocating the pointer which is used for an array, the application will enter the following loop. At , the current index for the loop is tested against the original uint32_t at offset +0x12 of the record. This results in the loop iterating that number of times. Within this loop is a pointer that is calculated that is written to in order to write data from the record into the pointer that was prior allocated. At , this pointer is incremented to point to each element of the array within the allocation. At , the loop will increment its index and continue on to the next pass.
0x67ef02: mov -0x1c8(%rbp),%rax ; record contents 0x67ef09: mov 0x12(%rax),%edx ;  loop sentinel from +0x12 of record 0x67ef0c: mov -0x1d4(%rbp),%eax ; loop index 0x67ef12: cmp %eax,%edx 0x67ef14: jbe 0x67f134 ; break ... 0x67f111: movzwl -0x1e0(%rbp),%eax ; aggregate size 0x67f118: add %eax,-0x1d8(%rbp) ;  use to increment pointer that's written to. 0x67f11e: jmp 0x67f121 0x67f120: nop 0x67f121: addl $0x1,-0x1d4(%rbp) ;  increment index 0x67f128: jmpq 0x67ef02
For each iteration of the loop, there are multiple places where the aggregated pointer is used to write into the undersized heap buffer. In the following code at , the application will write a null-byte into the heap buffer. As the loop will iterate more times than the amount of space that was allocated on the heap, eventually this pointer will point outside the heap buffer. The store instructions within the loop will then write outside the bounds of the buffer causing a heap-based buffer overflow and corrupting memory. This could lead to code execution under the context of the application.
0x67ef1a: mov -0x1c0(%rbp),%rax 0x67ef21: mov 0x18(%rax),%rax 0x67ef25: mov -0x1d4(%rbp),%edx ; loop index 0x67ef2b: movslq %edx,%rdx 0x67ef2e: shl $0x3,%rdx 0x67ef32: add %rdx,%rax ; adjust pointer 0x67ef35: movq $0x0,(%rax) ;  write null byte to pointer
In the provided proof-of-concept, the uint32_t is set to 0x20000001. When multiplied by 8, this will result in a heap buffer of 8 bytes and a loop that iterates 0x20000001 times. This ends up corrupting memory belonging to the heap allocator which upon it being used, will access the heap allocators corrupted metadata.
Thread 1 "planmaker" received signal SIGSEGV, Segmentation fault. 0x00007ffff7651a8f in unlink_chunk (p=p@entry=0x2d35040, av=0x7ffff77a4b80 <main_arena>) at malloc.c:1453 1453 malloc.c: No such file or directory. (gdb) bt #0 0x00007ffff7651a8f in unlink_chunk (p=p@entry=0x2d35040, av=0x7ffff77a4b80 <main_arena>) at malloc.c:1453 #1 0x00007ffff76547d3 in _int_malloc (av=av@entry=0x7ffff77a4b80 <main_arena>, bytes=bytes@entry=0x2020) at malloc.c:4041 #2 0x00007ffff7656479 in __GI___libc_malloc (bytes=0x2020) at malloc.c:3066 #3 0x0000000000ab7090 in ?? () #4 0x0000000000ab6332 in ?? () #5 0x0000000000ab6bed in ?? () #6 0x0000000000ab7a35 in ?? () #7 0x00000000004e4618 in ?? () #8 0x000000000067ef99 in ?? () #9 0x000000000068324b in ?? () #10 0x0000000000637421 in ?? () #11 0x0000000000638c72 in ?? () #12 0x00000000006962fc in ?? () #13 0x00000000007ea26e in ?? () #14 0x0000000000802753 in ?? () #15 0x0000000000802915 in ?? () #16 0x0000000000800495 in ?? () #17 0x0000000000a1e85c in ?? () #18 0x0000000000a21dee in ?? () #19 0x00000000010996cd in ?? () #20 0x00007ffff75e00b3 in __libc_start_main (main=0x109963e, argc=0x2, argv=0x7fffffffeb08, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffeaf8) at ../csu/libc-start.c:308 #21 0x0000000000411c69 in ?? ()
2020-11-02 - Vendor Disclosure
2021-01-19 - Vendor Patched
2021-02-03 - Public Release
Discovered by a member of Cisco Talos.