CVE-2020-7560
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.
Schneider Electric EcoStruxure Control Expert 14.1
https://www.se.com/us/en/product-range-presentation/548-ecostruxure%E2%84%A2-control-expert/
8.6 - CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CWE-123 - Write-what-where Condition
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.
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
2020-09-01 - Vendor Disclosure
2020-11-11 - Vendor assigned CVE
2020-12-08 - Public Release
Discovered by Jared Rittle of Cisco Talos.