Vulnerability in LZMA implementation

Darek x64x6a at gmail.com
Thu Aug 1 20:20:05 CEST 2024


I have attached a bad LZMA file generated by modifying a valid LZMA's size
to 0 as described.  The attached `create_bad_lzma.py` was used for this.

I have also attached a dummy proof-of-concept that mimic's U-Boots calls to
LzmaDecode().
When ran on linux, this segfaults off the page within the memcpy():
```
root at ubuntu:~/7z/7-Zip/C# gcc -I`pwd` LzmaDec.c test.c -o test
root at ubuntu:~/7z/7-Zip/C# ./test
uncompressed size: 0x0
compressed size:   0xfffffffffffffffa
7z Release: 19.00
Segmentation fault (core dumped)
```

I can get another PoC working in U-Boot to show that it can be used to gain
code execution, but I would need some time to get another environment
setup.

Please let me know if have questions.
Thank you,
Darek

On Wed, Jul 31, 2024 at 4:31 PM Darek <x64x6a at gmail.com> wrote:

> Hello!  I hope this is all the correct contacts for reporting this issue.
>
> I have discovered that there is a vulnerability in the version of 7z LZMA
> that U-Boot is using (9.20).  I've found this to lead to code execution
> when used with `lzmadec`.
> I have found no issued CVEs or public disclosures announcing this
> vulnerability, but it appears to have been patched in 7z LZMA version 21.02
> alpha.  I've patched locally with 24.07 and U-Boot appeared to LZMA
> decompress successfully, but I'm unaware of other use cases within the
> project.  I would also be curious if another LZMA implementation would be
> considered.
>
> I'm separately coordinating disclosure with the 7z maintainers and
> planning on requesting a CVE.  Since your project uses an older version
> that is unlikely to get a backported patch, and the issue is already fixed
> in newer versions, I am disclosing this to U-Boot maintainers at the same
> time.
>
>
> The bug triggers when an LZMA has the 8-byte length field in the header
> set to 0.  This field is located at offset 5 from the start of an LZMA file.
> Due to how the logic of `LzmaDec_DecodeToDic()` works, this eventually
> reaches a `memcpy(p->tempBuf, src, inSize)` on this line -
> https://github.com/u-boot/u-boot/blob/8877bc51a8a4d921ba2f163208b8b1a57ba47c18/lib/lzma/LzmaDec.c#L793
>
> The `inSize` value here is allowed to be greater than the buffer size due
> to the previous if-statement ignoring this value if `checkEndMarkNow` is
> set.
> The return value from `LzmaDec_TryDummy()` also needs to return
> `DUMMY_ERROR` to reach the `memcpy()`.
>
> The value of `dicLimit` in this function is initially the value read from
> the LZMA header, which for this case is 0.  Under my testing, when this was
> 0, it hits the memcpy call, using the passed `srcLen` value (`inSize`).
>
> Within `lmzadec`, this value is originally set to max size_t as the
> default here -
> https://github.com/u-boot/u-boot/blob/8877bc51a8a4d921ba2f163208b8b1a57ba47c18/cmd/lzmadec.c#L24
>
> And passed to the `LzmaDecode()` function here -
> https://github.com/u-boot/u-boot/blob/8877bc51a8a4d921ba2f163208b8b1a57ba47c18/lib/lzma/LzmaTools.c#L110
>
>
> Under my testing, when `lzmadec` is passed an LZMA file with a 0-length
> field, this ends up calling:
> ```
> memcpy(p->tempBuf, src, 0xFFFFFFFFFFFFFFF5)
> ```
> This is no longer max size_t due to previous subtractions in this loop and
> within a previous function:
>
> https://github.com/u-boot/u-boot/blob/8877bc51a8a4d921ba2f163208b8b1a57ba47c18/lib/lzma/LzmaDec.c#L746
> and
>
> https://github.com/u-boot/u-boot/blob/8877bc51a8a4d921ba2f163208b8b1a57ba47c18/lib/lzma/LzmaTools.c#L51
>
> This leads to code execution when the corrupted LZMA file is large enough
> to overwrite `memcpy()`'s instructions, or if the memory past the LZMA file
> is also controlled previously.
>
> In my testing, I created a small LZMA file and modified the header's size
> to 0.  I then appended assembly instructions that performed a loop to this
> file, so that when code execution would occur, I could easily verify by
> seeing the loop. I repeated this until the file was over 1GB.
> In U-Boot CLI, I then used TFTP to load the LZMA file from my server and
> then ran `lzmadec`with my selected source and destination address far away
> from U-Boot's code.  When I attached a debugger at this point, the program
> was stuck in my infinite loop code.
>
>
> I believe this is especially a concern for secure boot implementations
> that utilize U-Boot's LZMA decompression, as it could bypass their secure
> boot implementation.
>
> Please let me know if any additional details are needed or if a
> proof-of-concept video/script would be required for further evidence.
>
> Thank you,
> Darek
>
-------------- next part --------------
/*
gcc -I`pwd` LzmaDec.c test.c -o test
./test

LzmaDecode() calling logic borrowed from https://github.com/u-boot/u-boot/blob/master/lib/lzma/LzmaTools.c#L108
*/
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/mman.h>
#include "sys/stat.h"
#include <fcntl.h>

#include "LzmaDec.h"
#include "7zVersion.h"

#define LZMA_SIZE_OFFSET       LZMA_PROPS_SIZE
#define LZMA_DATA_OFFSET       LZMA_SIZE_OFFSET+sizeof(uint64_t)

static void *SzAlloc(const ISzAlloc *p, size_t size) { return malloc(size); }
static void SzFree(const ISzAlloc *p, void *address) { free(address); }

int get_file(const char * filename, void **addr){
    int fd, ret;
    size_t len_file, len;
    struct stat st;

    ELzmaStatus state;
    memset(&state, 0, sizeof(state));

    if ((fd = open(filename, O_RDWR | O_CREAT, S_IRWXU | S_IRGRP | S_IROTH)) < 0){
        perror("Error in file opening");
        return -1;
    }

    if ((ret = fstat(fd, &st)) < 0){
        perror("Error in fstat");
        return -1;
    }
    len_file = st.st_size;

    if ((*addr = mmap(NULL, len_file, PROT_READ, MAP_SHARED,fd,0)) == MAP_FAILED){
        perror("Error in mmap");
        return -1;
    }

    return 0;
}

void main(){
    char *src, *dst;
    size_t uncompressedSize, compressedSize;
    size_t outProcessed;
    ELzmaStatus state;
    ISzAlloc g_Alloc;
    
    memset(&state, 0, sizeof(state));

    g_Alloc.Alloc = SzAlloc;
    g_Alloc.Free = SzFree;

    if (get_file("/tmp/bad_test.lzma", (void**)&src) == -1) {
        return;
    }

    // read value from LZMA header
    uncompressedSize = *(uint64_t *)(&src[LZMA_SIZE_OFFSET]);
    printf("uncompressed size: 0x%lx\n", uncompressedSize);

    // set to max size_t, as it was done in U-Boot here with `dst_len` - https://github.com/u-boot/u-boot/blob/master/cmd/lzmadec.c#L24
    compressedSize = ~0UL;
    // this is followed by a subtract of props size - https://github.com/u-boot/u-boot/blob/master/lib/lzma/LzmaTools.c#L51
    compressedSize = (compressedSize - LZMA_PROPS_SIZE);
    printf("compressed size:   0x%lx\n", compressedSize);

    printf("7z Release: %s\n", MY_VERSION);
    LzmaDecode(
        dst, &uncompressedSize,
        src + LZMA_DATA_OFFSET, &compressedSize,
        src, LZMA_PROPS_SIZE, LZMA_FINISH_END, &state, &g_Alloc);
}
-------------- next part --------------
A non-text attachment was scrubbed...
Name: bad_test.lzma
Type: application/octet-stream
Size: 31 bytes
Desc: not available
URL: <https://lists.denx.de/pipermail/u-boot/attachments/20240801/42aacc39/attachment.obj>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: create_bad_lzma.py
Type: application/octet-stream
Size: 671 bytes
Desc: not available
URL: <https://lists.denx.de/pipermail/u-boot/attachments/20240801/42aacc39/attachment-0001.obj>


More information about the U-Boot mailing list