← index #13428Issue #13220
Off-topic · high · value 6.410
QUERY · ISSUE

heap-buffer-overflow [micropython@a5bdd39127]

openby zeroone-kropened 2024-01-15updated 2025-04-12
bug

Hi all, I'm Wonil Jang, from the research group S2Lab in UNIST.
We found a heap buffer overflow bug from micropython by our custom tool. The detailed information is as follows.

Environment

  • OS: Ubuntu 22.04
  • Version: micropython at commit ce42c9e
  • Build: unix ports
  • Bug Type: heap buffer overflow
  • Bug Location: py/binary.c:155
  • Credits: Junwha Hong and Wonil Jang, from S2Lab in UNIST

PoC

Index out-of-range (Normal behavior)

import gc

try:
    import os
except (ImportError, AttributeError):
    print("SKIP")
    raise SystemExit

class RAMBlockDevice:
    ERASE_BLOCK_SIZE = 7

    def __init__(self, blocks):
        print(blocks)
        self.data = bytearray(blocks * self.ERASE_BLOCK_SIZE)

    def readblocks(self, block, buf, off):
        addr = block * self.ERASE_BLOCK_SIZE + off
        for i in range(len(buf)):
            buf[i] = self.data[addr + i]

    def writeblocks(self, block, buf, off):
        print(buf[len(buf)])

    def ioctl(self, op, arg):
        if op == 4:  # block count
            return len(self.data) // self.ERASE_BLOCK_SIZE
        if op == 5:  # block size
            return self.ERASE_BLOCK_SIZE
        if op == 6:  # erase block
            return 0

bdev = RAMBlockDevice(30)
os.VfsLfs2.mkfs(bdev)

BOF (Wrong behavior)

import gc

try:
    import os
except (ImportError, AttributeError):
    print("SKIP")
    raise SystemExit

class RAMBlockDevice:
    ERASE_BLOCK_SIZE = 7

    def __init__(self, blocks):
        print(blocks)
        self.data = bytearray(blocks * self.ERASE_BLOCK_SIZE)

    def readblocks(self, block, buf, off):
        addr = block * self.ERASE_BLOCK_SIZE + off
        for i in range(len(buf)):
            buf[i] = self.data[addr + i]

    def writeblocks(self, block, buf, off):
        print(buf[len(buf)-1])

    def ioctl(self, op, arg):
        if op == 4:  # block count
            return len(self.data) // self.ERASE_BLOCK_SIZE
        if op == 5:  # block size
            return self.ERASE_BLOCK_SIZE
        if op == 6:  # erase block
            return 0

bdev = RAMBlockDevice(30)
os.VfsLfs2.mkfs(bdev)

The difference is only ERASE_BLOCK_SIZE is under 8.

config->cache_size = MIN(config->block_size, (4 * MAX(read_size, prog_size)));
config->lookahead_size = lookahead;
config->read_buffer = m_new(uint8_t, config->cache_size);
config->prog_buffer = m_new(uint8_t, config->cache_size);

Above code, the read_size is 32, config_size is 32 and block_size is 7. then, the cache_size becomes MIN(7, 128) = 7.

#0  lfs2_init (lfs2=0x7fffffffdc68, cfg=<optimized out>) at ../../lib/littlefs/lfs2.c:4170
#1  lfs2_rawformat (lfs2=0x7fffffffdc68, cfg=<optimized out>) at ../../lib/littlefs/lfs2.c:4260
#2  lfs2_format (lfs2=lfs2@entry=0x7fffffffdc68, cfg=cfg@entry=0x7fffffffdbf8) at ../../lib/littlefs/lfs2.c:5775
#3  0x00005555557cdf82 in mp_vfs_lfs2_mkfs (n_args=<optimized out>, pos_args=<optimized out>,  kw_args=<optimized out>) at ../../extmod/vfs_lfsx.c:150
// setup program cache
if (lfs2->cfg->prog_buffer) {
    lfs2->pcache.buffer = lfs2->cfg->prog_buffer;
} else {
...
}

Then, at lfs2_init, the prog_buffer was saved as lfs2->pcache.buffer

static int lfs2_bd_flush(lfs2_t *lfs2,
        lfs2_cache_t *pcache, lfs2_cache_t *rcache, bool validate) {
    if (pcache->block != LFS2_BLOCK_NULL && pcache->block != LFS2_BLOCK_INLINE) {
        LFS2_ASSERT(pcache->block < lfs2->block_count);
        lfs2_size_t diff = lfs2_alignup(pcache->size, lfs2->cfg->prog_size);
        int err = lfs2->cfg->prog(lfs2->cfg, pcache->block,
                pcache->off, pcache->buffer, diff);

At lfs2_bd_flush function lib/littlefs/lfs2.c:180, it progresses with pcache_size.

int mp_vfs_blockdev_write_ext(mp_vfs_blockdev_t *self, size_t block_num, size_t block_off, size_t len, const uint8_t *buf) {
    if (self->writeblocks[0] == MP_OBJ_NULL) {
        // read-only block device
        return -MP_EROFS;
    }

    mp_obj_array_t ar = {{&mp_type_bytearray}, BYTEARRAY_TYPECODE, 0, len, (void *)buf};
    self->writeblocks[2] = MP_OBJ_NEW_SMALL_INT(block_num);
    self->writeblocks[3] = MP_OBJ_FROM_PTR(&ar);
    self->writeblocks[4] = MP_OBJ_NEW_SMALL_INT(block_off);
    mp_obj_t ret = mp_call_method_n_kw(3, 0, self->writeblocks);
    if (ret == mp_const_none) {
        return 0;
    } else {
        return MP_OBJ_SMALL_INT_VALUE(ret);
    }
}

mp_vfs_blockdev_write_ext function is called with following arguments.

  • block_num: ?
  • block_off: 0
  • len: diff=32
  • buf: prog_buf (pcache→buffer)

Above code, the bytearray was constructed with wrong information! Real buffer_size is 7. but, given buffer size is 32.
In summary, The index out-of-bound over len(buf)-1 could be captured, but not for the under len(buf)-1.

Crash Log

    #0 0x55c5a177e7cf in mp_binary_get_val_array /home/qbit/testing-2023/micropython/ports/unix/../../py/binary.c:155:19
    #1 0x55c5a173bce7 in mp_obj_subscr /home/qbit/testing-2023/micropython/ports/unix/../../py/obj.c:536:24
    #2 0x55c5a178ca28 in mp_execute_bytecode /home/qbit/testing-2023/micropython/ports/unix/../../py/vm.c:446:21
    #3 0x55c5a174c84b in fun_bc_call /home/qbit/testing-2023/micropython/ports/unix/../../py/objfun.c:273:42
    #4 0x55c5a17d7e41 in mp_vfs_blockdev_write_ext /home/qbit/testing-2023/micropython/ports/unix/../../extmod/vfs_blockdev.c:105:20
    #5 0x55c5a184261e in lfs2_bd_flush /home/qbit/testing-2023/micropython/ports/unix/../../lib/littlefs/lfs2.c:181:19
    #6 0x55c5a184261e in lfs2_bd_prog /home/qbit/testing-2023/micropython/ports/unix/../../lib/littlefs/lfs2.c:251:27
    #7 0x55c5a184261e in lfs2_dir_commitprog /home/qbit/testing-2023/micropython/ports/unix/../../lib/littlefs/lfs2.c:1555:15
    #8 0x55c5a1842ff1 in lfs2_dir_compact /home/qbit/testing-2023/micropython/ports/unix/../../lib/littlefs/lfs2.c:1954:19
    #9 0x55c5a1836183 in lfs2_dir_splittingcompact /home/qbit/testing-2023/micropython/ports/unix/../../lib/littlefs/lfs2.c:2174:12
    #10 0x55c5a1836183 in lfs2_dir_relocatingcommit /home/qbit/testing-2023/micropython/ports/unix/../../lib/littlefs/lfs2.c:2292:13
    #11 0x55c5a182f575 in lfs2_dir_orphaningcommit /home/qbit/testing-2023/micropython/ports/unix/../../lib/littlefs/lfs2.c:2373:17
    #12 0x55c5a181f16a in lfs2_dir_commit /home/qbit/testing-2023/micropython/ports/unix/../../lib/littlefs/lfs2.c:2545:19
    #13 0x55c5a181f16a in lfs2_rawformat /home/qbit/testing-2023/micropython/ports/unix/../../lib/littlefs/lfs2.c:4293:15
    #14 0x55c5a181f16a in lfs2_format /home/qbit/testing-2023/micropython/ports/unix/../../lib/littlefs/lfs2.c:5775:11
    #15 0x55c5a17dff81 in mp_vfs_lfs2_mkfs /home/qbit/testing-2023/micropython/ports/unix/../../extmod/vfs_lfsx.c:150:15
    #16 0x55c5a174bd98 in fun_builtin_var_call /home/qbit/testing-2023/micropython/ports/unix/../../py/objfun.c:114:16
    #17 0x55c5a178d59e in mp_execute_bytecode /home/qbit/testing-2023/micropython/ports/unix/../../py/vm.c:1042:21
    #18 0x55c5a174c84b in fun_bc_call /home/qbit/testing-2023/micropython/ports/unix/../../py/objfun.c:273:42
    #19 0x55c5a178c4b4 in mp_execute_bytecode /home/qbit/testing-2023/micropython/ports/unix/../../py/vm.c:957:21
    #20 0x55c5a174c84b in fun_bc_call /home/qbit/testing-2023/micropython/ports/unix/../../py/objfun.c:273:42
    #21 0x55c5a1911557 in execute_from_lexer /home/qbit/testing-2023/micropython/ports/unix/main.c:161:13
    #22 0x55c5a19100b5 in do_file /home/qbit/testing-2023/micropython/ports/unix/main.c:310:12
    #23 0x55c5a19100b5 in main_ /home/qbit/testing-2023/micropython/ports/unix/main.c:722:19
    #24 0x7f0d0ec29d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #25 0x7f0d0ec29e3f in __libc_start_main csu/../csu/libc-start.c:392:3
    #26 0x55c5a15a5a14 in _start (/home/qbit/testing-2023/micropython/ports/unix/build-standard/micropython+0x3fa14)

Thank you for reading bug report!!

CANDIDATE · ISSUE

heap buffer overflow found at micropython/lib/re1.5/compilecode.c:68 [micropython@a5bdd39127]

openby zeroone-kropened 2023-12-18updated 2024-01-31
bug

Hi all, I'm Wonil Jang, from the research group S2Lab in UNIST.
We found a heap buffer overflow bug from micropython by our custom tool. The detailed information is as follows.

Environment

  • OS: Ubuntu 22.04
  • Version: micropython at commit a5bdd39127
  • Build: unix ports
  • Bug Type: heap-buffer-overflow
  • Bug Location: micropython/lib/re1.5/compilecode.c:68
  • Credits: Junwha Hong and Wonil Jang, from S2Lab in UNIST

Problem Statement

...
case '[': {
            int cnt;
            term = PC;
            re++;
            if (*re == '^') {
                EMIT(PC++, ClassNot);
                re++;
            } else {
                EMIT(PC++, Class);
            }
            PC++; // Skip # of pair byte
            prog->len++;
            for (cnt = 0; *re != ']'; re++, cnt++) {    <== [1]
                char c = *re;
                if (c == '\\') {
                    ++re;
                    c = *re;
                    if (MATCH_NAMED_CLASS_CHAR(c)) {
                        c = RE15_CLASS_NAMED_CLASS_INDICATOR;
                        goto emit_char_pair;
                    }
                }
                if (!c) return NULL;
                if (re[1] == '-' && re[2] != ']') {    <== [2]
                    re += 2;
                }
            emit_char_pair:
                EMIT(PC++, c);
                EMIT(PC++, *re);
            }
            EMIT_CHECKED(term + 1, cnt);
            break;
        }
...

At [1], We found a heap-buffer-overflow bug. This is because there is no verification of regex string length.
For instance, If we have a regex string ([a-zA-Z_0-(null), it will process 0-(null) finally.
and then, at [2], re pointer will point to next byte after 0-(null).
Because It doesn’t have verification logic of regex string length, it will try to read a byte after 0-(null), which means out-of-bound read.

Example (replication)

>>> import re
>>> re.sub(r"([a-zA-Z_0-", "b", "1a2a3a")
=================================================================
    #0 0x5555557b9461 in _compilecode /home/qbit/testing-2023/micropython/ports/unix/../../lib/re1.5/compilecode.c:68:27
    #1 0x5555557b97ed in _compilecode /home/qbit/testing-2023/micropython/ports/unix/../../lib/re1.5/compilecode.c:103:18
    #2 0x5555557b78da in re1_5_sizecode /home/qbit/testing-2023/micropython/ports/unix/../../lib/re1.5/compilecode.c:194:9
    #3 0x5555557b78da in mod_re_compile /home/qbit/testing-2023/micropython/ports/unix/../../extmod/modre.c:427:16
    #4 0x5555557b6cd7 in re_sub_helper /home/qbit/testing-2023/micropython/ports/unix/../../extmod/modre.c:287:16

Patch

The simplest patch will be adding verification of regex re length.

Log

    #0 0x5555557b9461 in _compilecode /home/qbit/testing-2023/micropython/ports/unix/../../lib/re1.5/compilecode.c:68:27
    #1 0x5555557b97ed in _compilecode /home/qbit/testing-2023/micropython/ports/unix/../../lib/re1.5/compilecode.c:103:18
    #2 0x5555557b78da in re1_5_sizecode /home/qbit/testing-2023/micropython/ports/unix/../../lib/re1.5/compilecode.c:194:9
    #3 0x5555557b78da in mod_re_compile /home/qbit/testing-2023/micropython/ports/unix/../../extmod/modre.c:427:16
    #4 0x5555557b6cd7 in re_sub_helper /home/qbit/testing-2023/micropython/ports/unix/../../extmod/modre.c:287:16
    #5 0x555555782c1c in mp_execute_bytecode /home/qbit/testing-2023/micropython/ports/unix/../../py/vm.c:1042:21
    #6 0x55555574261b in fun_bc_call /home/qbit/testing-2023/micropython/ports/unix/../../py/objfun.c:273:42
    #7 0x555555903f3d in execute_from_lexer /home/qbit/testing-2023/micropython/ports/unix/main.c:161:13
    #8 0x555555902ad5 in do_file /home/qbit/testing-2023/micropython/ports/unix/main.c:310:12
    #9 0x555555902ad5 in main_ /home/qbit/testing-2023/micropython/ports/unix/main.c:722:19
    #10 0x7ffff7c29d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #11 0x7ffff7c29e3f in __libc_start_main csu/../csu/libc-start.c:392:3
    #12 0x555555593a34 in _start (/home/qbit/testing-2023/micropython/ports/unix/build-standard/micropython+0x3fa34)

Thank you for reading my report.

Keyboard

j / / n
next pair
k / / p
previous pair
1 / / h
show query pane
2 / / l
show candidate pane
c
copy suggested comment
r
toggle reasoning
g i
go to index
?
show this help
esc
close overlays

press ? or esc to close

copied