Talos Vulnerability Report

TALOS-2020-1144

Schneider Electric EcoStruxure Control Expert APX project file processing code execution vulnerability

December 8, 2020
CVE Number

CVE-2020-7560

Summary

A local code execution vulnerability exists in the APX project file processing functionality of Schneider Electric EcoStruxure Control Expert 14.1. The opening of a STA project archive containing a specially crafted APX project file can lead to code execution. An attacker can provide a malicious file to trigger this vulnerability.

Tested Versions

Schneider Electric EcoStruxure Control Expert 14.1

Product URLs

https://www.se.com/us/en/product-range-presentation/548-ecostruxure%E2%84%A2-control-expert/

CVSSv3 Score

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

CWE

CWE-123 - Write-what-where Condition

Details

EcoStruxure Control Expert (formerly UnityPro) is Schneider Electric’s flagship software for program development, maintenance and monitoring of industrial networks. With this application, engineers and operators can design strategies, program compatible equipment and monitor process status.

When a Control Expert project file archive (STA file) is loaded into Control Expert the contained project file (APX file) is extracted and parsed. An APX file is comprised of various blocks of information referred to as RTEs. Each of these RTEs contain a header containing information used when loading the project into memory. These fields include, but are not limited to, an ID field, a data length field, an offset field, a couple folio fields, and a CRC field. When an RTE with a large offset value is included within an APX file it is possible to leverage an integer overflow to write arbitrary data to a user-defined offset from a heap address.

As an APX file is being parsed, each of the RTEs are extracted independently and stored on the heap. A snippet of the code responsible used to prepare for this extraction can be seen in the below Linker.dll code. Here five fields are extracted from the RTE header along with one unknown field before being passed into Ordinal_MemAlloc_90 and subsequently sub_3ca2f40 where the offset value is checked.

//
// Linker.dll
// sub_3c0e660
//
...
03c0e960  lea     ecx, [ebp-0x2f4]
03c0e966  call    sub_3c04540
03c0e96b  push    eax                         // arg5 = RTE_length
03c0e96c  mov     eax, dword [ebp-0x2f8]
03c0e972  push    eax                         // arg4 = RTE_offset
03c0e973  lea     ecx, [ebp-0x2f4]
03c0e979  call    sub_3c04340
03c0e97e  push    eax                         // arg3 = RTE_data_p
03c0e97f  lea     ecx, [ebp-0x2d4]
03c0e985  call    sub_3c0faf0
03c0e98a  movzx   cx, al
03c0e98e  movzx   edx, cx
03c0e991  push    edx                         // arg2 = unknown
03c0e992  lea     ecx, [ebp-0x2d4]
03c0e998  call    sub_3c0fad0
03c0e99d  push    eax                         // arg1 = RTE_folio_unknown_p
03c0e99e  movzx   eax, word [ebp-0x2bc]
03c0e9a5  push    eax                         // arg0 = RTE_id
03c0e9a6  lea     ecx, [ebp-0x2c0]
03c0e9ac  call    Ordinal_MemAlloc_90         // call into MemAlloc
03c0e9b1  jmp     0x3c0e9eb
...

After extracting necessary fields from the RTE header, those fields are checked before continuing. To hit the vulnerable condition it is necessary that both the RTE_offset and RTE_length fields are non-null. When this case occurs, the RTE_length value is added to the RTE_offset value and subsequently compared to the extracted size of the RTE_data. When the combined RTE_offset and RTE_length value is greater than the extracted size, execution continues into an error condition. Otherwise it continues processing the RTE.

If the RTE_offset is set to a value large enough that when combined with the RTE_length field exceeds 0xFFFFFFFF, the comparison value will overflow. When this happens a value small enough to pass the check is created while leaving a massive RTE_offset value. This can be seen in the MemAlloc.dll sub_3ca2f40 snippet below.

//
// MemAlloc.dll
// sub_3ca2f40
//
...
03ca3187  cmp     dword [ebp+0x18], 0x0
03ca318b  je      0x3ca3193                   // jump elsewhere if RTE_offset is null
03ca318d  cmp     dword [ebp+0x1c], 0x0
03ca3191  je      0x3ca31b8                   // jump elsewhere if RTE_length is null
03ca3193  movzx   ecx, word [ebp+0x8]
03ca3197  push    ecx
03ca3198  mov     ecx, dword [ebp-0x2f0]
03ca319e  call    sub_3caa0b0                 // returns the RTE base pointer into eax
03ca31a3  mov     ecx, eax
03ca31a5  call    sub_3ce1920                 // returns the size of the RTE into eax
03ca31aa  mov     edx, dword [ebp+0x18]       // load edx with RTE_offset
03ca31ad  add     edx, dword [ebp+0x1c]       // add RTE_length to the RTE_offset
                                              // Integer Overflow
03ca31b0  cmp     eax, edx                    // check if RTE size < RTE_length+RTE_offset
03ca31b2  jae     0x3ca3284
...

As long as the offset check above as well as additional non-offset related checks are passed, execution will continue into MemAlloc.dll sub_3ce2ed0 where a buffer will be allocated and subsequently filled.

//
// MemAlloc.dll
// sub_3ce2ed0
//
...
03ce2f9c  mov     ecx, dword [ebp-0xe4]
03ce2fa2  call    sub_3ce1920                 // returns RTE_length into eax
03ce2fa7  push    eax                         // RTE_length
03ce2fa8  call    sub_3ce6560                 // Allocate space for RTE_data via `operator new`
03ce2fad  add     esp, 0x4
03ce2fb0  mov     dword [ebp-0x108], eax      // store a reference to the allocated heap buffer
...

Execution continues until the RTE_data is to be copied into its previously allocated buffer. To do this a memcpy call is used with values pulled from user-controlled content. The dst and n parameters are filled with RTE_data_p and RTE_length values, respectively. To get the value used for the src parameter, the pointer to the RTE heap buffer from the previous snippet is added to the RTE_offset value. This creates the following call: memcpy(RTE_heap_buffer_p+RTE_offset, RTE_data_p, RTE_length)

//
// MemAlloc.dll
// sub_3ce2ed0
//
...
03ce30d2  mov     al, byte [ebp-0xec]
03ce30d8  mov     byte [ebp-0xe5], al
03ce30de  mov     ecx, dword [ebp+0xc]
03ce30e1  push    ecx                         // arg2 == RTE_length
03ce30e2  mov     edx, dword [ebp+0x8]
03ce30e5  push    edx                         // arg1 == RTE_data_p
03ce30e6  mov     eax, dword [ebp-0xe4]
03ce30ec  mov     ecx, dword [eax+0x14]       // move RTE_heap_buffer_p into ecx
03ce30ef  add     ecx, dword [ebp+0x10]       // add RTE_offset to RTE_heap_buffer_p
03ce30f2  push    ecx                         // arg0 == RTE_heap_buffer_p + RTE_offset
03ce30f3  call    memcpy                      // memcpy RTE data to specified offset
...

By specially crafting the RTE_length, RTE_offset, and RTE_data fields it is possible to control the memcpy, allowing for arbitrary writes and the potential for code execution. An example of using this to overwrite our RTE’s heap metadata can be found below. In this example a RTE_offset of 0xFFFFFFF8 and a RTE_length of 0x18 are used, effectively causing our write to start eight bytes before RTE_heap_buffer_p.

The address for the RTE_data heap buffer can be found by inspecting the eax register following the call to MemAlloc!Ordinal102+0x4a70 (sub_3ce6560 above)

0:000> bp MemAlloc+0x52FA8
0:000> g
Breakpoint 1 hit
eax=00001b85 ebx=0094ecd4 ecx=5ed48c70 edx=00001b85 esi=00000001 edi=04b448c0
eip=03cd2fa8 esp=00949048 ebp=00949178 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
MemAlloc!Ordinal102+0x14b8:
03cd2fa8 e8b3350000      call    MemAlloc!Ordinal102+0x4a70 (03cd6560)
0:000> p
eax=5ec3f2e8 ebx=0094ecd4 ecx=00001b85 edx=5ec40e78 esi=00000001 edi=04b448c0
eip=03cd2fad esp=00949048 ebp=00949178 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
MemAlloc!Ordinal102+0x14bd:
03cd2fad 83c404          add     esp,4
0:000>

In the case shown above the returned pointer is 0x5ec3f2e8. The eight bytes preceeding this address contain heap metadata for the data section to follow. By inspecting this area immediately before allowing the memcpy call (MemAlloc!Ordinal102+0x4ad4) to occur we can see that it is filled with data while the buffer itself is null (from a prior memset).

0:000> bp MemAlloc+0x530F3
0:000> g
Breakpoint 2 hit
eax=5ed48c70 ebx=0094ecd4 ecx=5ec3f2e0 edx=56fffbf0 esi=00000001 edi=04b448c0
eip=03cd30f3 esp=00949040 ebp=00949178 iopl=0         nv up ei pl nz ac po cy
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000213
MemAlloc!Ordinal102+0x1603:
03cd30f3 e8cc340000      call    MemAlloc!Ordinal102+0x4ad4 (03cd65c4)
0:000> db 0x5ec3f2e8-0x08
5ec3f2e0  77 05 d0 44 9b db 07 0b-00 00 00 00 00 00 00 00  w..D............
5ec3f2f0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
5ec3f300  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
5ec3f310  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
5ec3f320  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
5ec3f330  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
5ec3f340  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
5ec3f350  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

When the memcpy call is allowed to execute it is possible to see the overwritten metadata by inspecting the same area as before.

0:000> p
eax=5ec3f2e0 ebx=0094ecd4 ecx=00000000 edx=00001b85 esi=00000001 edi=04b448c0
eip=03cd30f8 esp=00949040 ebp=00949178 iopl=0         nv up ei pl nz ac po cy
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000213
MemAlloc!Ordinal102+0x1608:
03cd30f8 83c40c          add     esp,0Ch
0:000> db 0x5ec3f2e8-0x08
5ec3f2e0  41 41 41 41 42 42 42 42-43 43 43 43 43 43 43 43  AAAABBBBCCCCCCCC
5ec3f2f0  43 43 43 43 43 43 43 43-43 43 43 43 43 43 43 43  CCCCCCCCCCCCCCCC
5ec3f300  43 43 43 43 43 43 43 43-43 43 43 43 43 43 43 43  CCCCCCCCCCCCCCCC
5ec3f310  43 43 43 43 43 43 43 43-43 43 43 43 43 43 43 43  CCCCCCCCCCCCCCCC
5ec3f320  43 43 43 43 43 43 43 43-43 43 43 43 43 43 43 43  CCCCCCCCCCCCCCCC
5ec3f330  43 43 43 43 43 43 43 43-43 43 43 43 43 43 43 43  CCCCCCCCCCCCCCCC
5ec3f340  43 43 43 43 43 43 43 43-43 43 43 43 43 43 43 43  CCCCCCCCCCCCCCCC
5ec3f350  43 43 43 43 43 43 43 43-43 43 43 43 43 43 43 43  CCCCCCCCCCCCCCCC

If execution is then allowed to run until just before the crash, we can see that the 0x5ec3f2e8 pointer is passed to a operator delete call (MemAlloc!Ordinal102+0x4b04).

0:000> bp MemAlloc+0x525CF
0:000> g
Breakpoint 3 hit
eax=5ec3f2e8 ebx=00000000 ecx=5ed48c70 edx=5ec3f2e8 esi=00948af4 edi=00949584
eip=03cd25cf esp=00948808 ebp=00948814 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
MemAlloc!Ordinal102+0xadf:
03cd25cf e820400000      call    MemAlloc!Ordinal102+0x4b04 (03cd65f4)

When this is allowed to continue an exception occurs due to a corrupted heap.

Crash Information

0:000> r
eax=5ec3f2e8 ebx=00000000 ecx=5ed48c70 edx=5ec3f2e8 esi=00948af4 edi=00949584
eip=03cd25cf esp=00948808 ebp=00948814 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
MemAlloc!Ordinal102+0xadf:
03cd25cf e820400000      call    MemAlloc!Ordinal102+0x4b04 (03cd65f4)
0:000> p
Critical error detected c0000374
WARNING: This break is not a step/trace completion.
The last command has been cleared to prevent
accidental continuation of this unrelated event.
Check the event, location and thread before resuming.
(bbc.82c): Break instruction exception - code 80000003 (first chance)
eax=00000000 ebx=00000000 ecx=77c6a798 edx=0094835d esi=00b30000 edi=5ec3f2e0
eip=77cbe625 esp=009485b0 ebp=00948628 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
ntdll!RtlReportCriticalFailure+0x29:
77cbe625 cc              int     3

0:000> k
 # ChildEBP RetAddr  
00 00948628 77cbf559 ntdll!RtlReportCriticalFailure+0x29
01 00948638 77cbf639 ntdll!RtlpReportHeapFailure+0x21
02 0094866c 77cbf8a2 ntdll!RtlpLogHeapFailure+0xa1
03 009486c4 77c7ab47 ntdll!RtlpAnalyzeHeapFailure+0x25b
04 009487b8 77c23472 ntdll!RtlpFreeHeap+0xc6
05 009487d8 76aa14dd ntdll!RtlFreeHeap+0x142
06 009487ec 6d5bdcc2 kernel32!HeapFree+0x14
07 00948800 03cd25d4 MSVCR110!free+0x1a
08 00948814 03cd2661 MemAlloc!Ordinal102+0xae4
09 00948824 03c9381d MemAlloc!Ordinal102+0xb71
0a 00949590 03ca9861 MemAlloc!Ordinal29+0x1381d
0b 009496ec 03bee9b1 MemAlloc!Ordinal90+0x111
0c 00949acc 03bed83a Linker+0xe9b1
0d 00949b90 03bac30b Linker+0xd83a

Timeline

2020-09-01 - Vendor Disclosure
2020-11-11 - Vendor assigned CVE
2020-12-08 - Public Release

Credit

Discovered by Jared Rittle of Cisco Talos.