An exploitable heap-based buffer 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 to copy data from a particular record type into a buffer that is smaller than the size used for the copy which will cause a heap-based buffer overflow. 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-122 - Heap-based Buffer Overflow
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
The vulnerability described by this document is specifically within the handler for the record type 0x800d. When parsing the 0x800d record, the following function is executed. This function first loads the pointer to the record and stores it in a variable within the stack frame. Afterwards at , the function will allocate a 2048-byte buffer and then store it into an object. Immediately afterwards, the length of the record will be read from stream, multiplied by 1 at , and then stored into the
%edx register. This length is then used as the length to the call to
memcpy at . If the length is larger than 2048, then this
memcpy will write the record’s contents past the bounds of the heap allocation that was previously made. Due to the length being explicitly trusted, this can allow an attacker to corrupt memory which can lead to code execution under the context of the application.
0x67d53d: push %rbp 0x67d53e: mov %rsp,%rbp 0x67d541: sub $0x20,%rsp 0x67d545: mov %rdi,-0x18(%rbp) ; object 0x67d549: mov %rsi,-0x20(%rbp) ;  record contents 0x67d54d: movl $0x0,-0x4(%rbp) ... 0x67d577: mov $0x1,%eax 0x67d57c: shl $0xb,%eax ; %eax := 0x800 0x67d57f: mov %eax,%edx 0x67d581: mov -0x18(%rbp),%rax 0x67d585: mov (%rax),%rax 0x67d588: mov %edx,%esi 0x67d58a: mov %rax,%rdi 0x67d58d: callq 0xab7a01 ;  allocate 0x800 bytes 0x67d592: mov %rax,%rdx 0x67d595: mov -0x18(%rbp),%rax 0x67d599: mov %rdx,0x54d0(%rax) ;  store to pointer ... 0x67d5b0: mov -0x20(%rbp),%rax ; pointer to record contents 0x67d5b4: add $0x2,%rax ; shift past record type 0x67d5b8: movzwl (%rax),%eax ; read record length 0x67d5bb: movzwl %ax,%eax 0x67d5be: mov $0x1,%edx 0x67d5c3: imul %edx,%eax ;  multiply by 1 0x67d5c6: mov %eax,%edx ... 0x67d5c8: mov -0x20(%rbp),%rax ; pointer to record contents 0x67d5cc: lea 0x4(%rax),%rcx ; shift past record type and length 0x67d5d0: mov -0x18(%rbp),%rax 0x67d5d4: mov 0x54d0(%rax),%rax ; pointer from allocation 0x67d5db: mov %rcx,%rsi 0x67d5de: mov %rax,%rdi 0x67d5e1: callq 0xab9e39 ;  memcpy
Upon opening up the provided proof-of-concept, the application will crash while trying to dereference user-supplied data.
Thread 1 "planmaker" received signal SIGSEGV, Segmentation fault. 0x0000000000ab7b3d in ?? () (gdb) x/i $pc => 0xab7b3d: mov 0x18(%rax),%rax (gdb) bt #0 0x0000000000ab7b3d in ?? () #1 0x00000000010442da in ?? () #2 0x00000000006795d7 in ?? () #3 0x000000000068324b in ?? () #4 0x0000000000637421 in ?? () #5 0x0000000000638c72 in ?? () #6 0x00000000006962fc in ?? () #7 0x00000000007ea26e in ?? () #8 0x0000000000802753 in ?? () #9 0x0000000000802915 in ?? () #10 0x0000000000800495 in ?? () #11 0x0000000000a1e85c in ?? () #12 0x0000000000a21dee in ?? () #13 0x00000000010996cd in ?? () #14 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 #15 0x0000000000411c69 in ?? () (gdb) i r rax rax 0x4141414141414141 0x4141414141414141
2020-11-02 - Vendor Disclosure
2021-01-19 - Vendor Patched
2021-02-03 - Public Release
Discovered by a member of Cisco Talos.