Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"elftools.construct.core.FieldError: expected 1, found 0 " and "elftools.construct.core.ArrayError: ('missing terminator', FieldError('expected 1, found 0'))" #591

Closed
0xsyj opened this issue Jan 4, 2025 · 15 comments · Fixed by #592

Comments

@0xsyj
Copy link

0xsyj commented Jan 4, 2025

from pwn import *
e = ELF('nsctf_online_2019_pwn1')
Traceback (most recent call last):
File "/home/syj/.local/lib/python3.10/site-packages/elftools/construct/core.py", line 573, in _parse
subobj = self.subcon._parse(stream, context)
File "/home/syj/.local/lib/python3.10/site-packages/elftools/construct/core.py", line 316, in _parse
return _read_stream(stream, self.length)
File "/home/syj/.local/lib/python3.10/site-packages/elftools/construct/core.py", line 293, in _read_stream
raise FieldError("expected %d, found %d" % (length, len(data)))
elftools.construct.core.FieldError: expected 1, found 0

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "", line 1, in
File "/home/syj/.local/lib/python3.10/site-packages/pwnlib/elf/elf.py", line 362, in init
self._describe()
File "/home/syj/.local/lib/python3.10/site-packages/pwnlib/elf/elf.py", line 457, in _describe
self.checksec(*a, **kw)
File "/home/syj/.local/lib/python3.10/site-packages/pwnlib/elf/elf.py", line 2117, in checksec
if self.shadowstack:
File "/home/syj/.local/lib/python3.10/site-packages/pwnlib/elf/elf.py", line 2192, in shadowstack
for prop in self.iter_properties():
File "/home/syj/.local/lib/python3.10/site-packages/pwnlib/elf/elf.py", line 532, in iter_properties
for note in self.iter_notes():
File "/home/syj/.local/lib/python3.10/site-packages/pwnlib/elf/elf.py", line 523, in iter_notes
for note in seg.iter_notes():
File "/home/syj/.local/lib/python3.10/site-packages/elftools/elf/notes.py", line 31, in iter_notes
CString('').parse(elffile.stream.read(disk_namesz)))
File "/home/syj/.local/lib/python3.10/site-packages/elftools/construct/core.py", line 180, in parse
return self.parse_stream(BytesIO(data))
File "/home/syj/.local/lib/python3.10/site-packages/elftools/construct/core.py", line 190, in parse_stream
return self._parse(stream, Container())
File "/home/syj/.local/lib/python3.10/site-packages/elftools/construct/core.py", line 261, in _parse
return self.subcon._parse(stream, context)
File "/home/syj/.local/lib/python3.10/site-packages/elftools/construct/core.py", line 276, in _parse
return self._decode(self.subcon._parse(stream, context), context)
File "/home/syj/.local/lib/python3.10/site-packages/elftools/construct/core.py", line 578, in _parse
raise ArrayError("missing terminator", ex)
elftools.construct.core.ArrayError: ('missing terminator', FieldError('expected 1, found 0'))
nsctf_online_2019_pwn1.zip

@0xsyj
Copy link
Author

0xsyj commented Jan 4, 2025

我的系统版本是ubuntu22.04,pyelftools版本是:
pip3 list | grep pyelf
pyelftools 0.31

@0xsyj
Copy link
Author

0xsyj commented Jan 4, 2025

当我换成ubuntu20.04的docker就不会报错,ubuntu20.04的虚拟机:e = ELF('./nsctf_online_2019_pwn1') 也会报错

@sevaa
Copy link
Contributor

sevaa commented Jan 4, 2025

The file nsctf_online_2019_pwn1 is wonky.

It's a 64-bit, little endian ELF file. Its segment header (aka "program headers") table contains entries of size 0x38, starting at offset 0x40 (just past the file header), a total of 11 entries (0xb), occupying the file offsets from 0x40 to 0x2A8.

The segment entry at index 3 is of type PT_NOTE (4) and has p_offset (offset in file) set to 0x254, size 0x44. That points in the middle of the segment header table. That is not cool, but technically allowed.

If interpreted as a note header, the bytes go:

00 00 00 00
30 00 00 00
00 00 00 00

This reads as a note of type 0 with no name (allowed by the ABI, not handled well by pyelftools) and 0x30 bytes worth of data. Together it's 0x3C bytes of data - short of 0x44. The next note header is cut short (8 bytes where at least 12 are expected) if the segment boundary is a hard one - but pyelftools happily reads from the source file past the segment boundaries. The second note is also funny looking but technically legit. Then the "past the end" condition kicks in and parsing stops.

Whether or not reading past the end of the segment is legitimate is a tricky question. The file is loaded as a whole; all segments are mapped to the process' address space. In this binary, in particular, there is a segment 0 of type PHDR that covers offsets from 0x40 to 0x2A8 - so in a loaded binary, reading past the end of the NOTE segment in question won't crash, seeing that the same memory is mapped as another segment. I'm not sure what's the logic with overlapping segments.

All in all, there are two problems with pyelftools that this binary uncovers:

  • Notes with zero length names (or zero length data for that matter) throw an underflow exception
  • The note parser doesn't respect the end of the segment/section if it comes in the middle of a note - but does it have to?

If the former is fixed but the latter is not, the file will parse. 😄 If the latter is fixed literally (i. e. no reading past the end of the segment), there will be a stream underflow error.


On a side note, the ABI document at https://refspecs.linuxbase.org/elf/gabi4+/ch5.pheader.html#note_section claims that in 64 bit files, the fields of the note section are 64 bit. That is wrong, in the sense that all the tools out there disagree - in both 64 bit and 32 bit ELF binaries, the note header fields are 32 bit, and the name/data alignment is to 4 bytes. An alternative format document at https://docs.oracle.com/cd/E23824_01/html/819-0690/chapter6-18048.html states that header fields are 32 bit always. I wonder who should I bother about that discrepancy...

@eliben - ideas?

@0xsyj
Copy link
Author

0xsyj commented Jan 5, 2025

I tried pyelftools-0.30 on my docker-ubuntu20.04 and it works fine.
But when I downgraded ubuntu20.04 in the vmware virtual machine, it couldn't work properly. So strange.

root@b66:/ctf/work/nsctf_online_2019_pwn1# pip3 list | grep pyelf
pyelftools 0.30
root@b66:/ctf/work/nsctf_online_2019_pwn1# python3
Python 3.8.10 (default, May 26 2023, 14:05:08)
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.

from pwn import *
e = ELF('nsctf_online_2019_pwn1')
[*] '/ctf/work/nsctf_online_2019_pwn1/nsctf_online_2019_pwn1'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

@sevaa
Copy link
Contributor

sevaa commented Jan 5, 2025

Check the versions of pwntools too. It is possible that some versions of pwntools don't try to parse the note segment(s), or catch exceptions there.

@0xsyj
Copy link
Author

0xsyj commented Jan 6, 2025

I encountered the problem again from another binary.
roarctf_2019_realloc_magic.zip

>>> from pwn import *
>>> e = ELF('roarctf_2019_realloc_magic')
Traceback (most recent call last):
  File "/home/syj/.local/lib/python3.10/site-packages/elftools/construct/core.py", line 573, in _parse
    subobj = self.subcon._parse(stream, context)
  File "/home/syj/.local/lib/python3.10/site-packages/elftools/construct/core.py", line 316, in _parse
    return _read_stream(stream, self.length)
  File "/home/syj/.local/lib/python3.10/site-packages/elftools/construct/core.py", line 293, in _read_stream
    raise FieldError("expected %d, found %d" % (length, len(data)))
elftools.construct.core.FieldError: expected 1, found 0

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/syj/.local/lib/python3.10/site-packages/pwnlib/elf/elf.py", line 362, in __init__
    self._describe()
  File "/home/syj/.local/lib/python3.10/site-packages/pwnlib/elf/elf.py", line 457, in _describe
    self.checksec(*a, **kw)
  File "/home/syj/.local/lib/python3.10/site-packages/pwnlib/elf/elf.py", line 2117, in checksec
    if self.shadowstack:
  File "/home/syj/.local/lib/python3.10/site-packages/pwnlib/elf/elf.py", line 2192, in shadowstack
    for prop in self.iter_properties():
  File "/home/syj/.local/lib/python3.10/site-packages/pwnlib/elf/elf.py", line 532, in iter_properties
    for note in self.iter_notes():
  File "/home/syj/.local/lib/python3.10/site-packages/pwnlib/elf/elf.py", line 523, in iter_notes
    for note in seg.iter_notes():
  File "/home/syj/.local/lib/python3.10/site-packages/elftools/elf/notes.py", line 28, in iter_notes
    CString('').parse(elffile.stream.read(disk_namesz)))
  File "/home/syj/.local/lib/python3.10/site-packages/elftools/construct/core.py", line 180, in parse
    return self.parse_stream(BytesIO(data))
  File "/home/syj/.local/lib/python3.10/site-packages/elftools/construct/core.py", line 190, in parse_stream
    return self._parse(stream, Container())
  File "/home/syj/.local/lib/python3.10/site-packages/elftools/construct/core.py", line 261, in _parse
    return self.subcon._parse(stream, context)
  File "/home/syj/.local/lib/python3.10/site-packages/elftools/construct/core.py", line 276, in _parse
    return self._decode(self.subcon._parse(stream, context), context)
  File "/home/syj/.local/lib/python3.10/site-packages/elftools/construct/core.py", line 578, in _parse
    raise ArrayError("missing terminator", ex)
elftools.construct.core.ArrayError: ('missing terminator', FieldError('expected 1, found 0'))
>>> 

@sevaa
Copy link
Contributor

sevaa commented Jan 6, 2025

This one has a PT_NOTE segment at the same offset (0x254) with the same size (0x44).

Do they come straight from a compiler/linker, or were they modified manually? This bogus note could mean something beyond the obvious, but I fail to see what it is.

@0xsyj
Copy link
Author

0xsyj commented Jan 6, 2025

It should be compiled directly, not manually modified, because these are two different binary files from two different CTF competitions, but both binary files contain the same issue.

@0xsyj
Copy link
Author

0xsyj commented Jan 6, 2025

PT_NOTE is usually used to describe some additional information or comments in the Program Header Table, which typically includes some debugging information, symbol table information, etc. The appearance of PT_NOTE is to allow the program to obtain some additional information at runtime. For example, a debugger can use this information to help debug the program. It seems that the probability of appearing in a binary file is still quite high.

@sevaa
Copy link
Contributor

sevaa commented Jan 6, 2025

I know the definition and the stated purpose. This particular note looks as if it's not supposed to be treated by the loader like any other segment is treated - as an instruction to create a range of bytes at a certain virtual address in the process space. Chances are, the Linux loader ignores PT_NOTE segments altogether, and some clever vendor decided they'd use them as scratch space, with offset/size/vaddr fields not meaning what they nominally do. The numbers 0x254/0x44 seem to be popping up in different, unrelated binaries - too consistent. Let me ask around.

If that's indeed the case, the whole logic of NoteSegment in pyelftools is in question.

https://stackoverflow.com/questions/79333635/pt-note-segment-in-elf-files-has-bogus-offset-address-size

@sevaa
Copy link
Contributor

sevaa commented Jan 6, 2025

@0xsyj : are you building those binaries yourself?

One of possible explanations is that the generation of a bogus PT_NOTE is/was a linker bug. If you are building them yourself, what is the build environment? One way to confirm or debunk this theory would be reproducing your environment, and following the linker logic in that particular version of the toolchain.

@0xsyj
Copy link
Author

0xsyj commented Jan 8, 2025

It's a pity that these two binaries were not compiled by me.

@0xsyj
Copy link
Author

0xsyj commented Jan 8, 2025

image
image
It seems that this PT_NOTE has no effect.

@sevaa
Copy link
Contributor

sevaa commented Jan 8, 2025

I've performed that analysis already. The next question is, was it a linker bug that pyelftools should work around, or a piece of undocumented linker magic that pyelftools should recognize and parse? For now, the bug option seems more likely, but I would prefer a confirmation from someone with knowledge of the GNU linker's bug tracker.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants