Solved two PWN challenges, documenting the process here.
Click here to visit the original Chinese version.
Extremely Lame Filters 1
The challenge provides two files: fairy.py
and elf.py
. elf.py
is an ELF file parser, while fairy.py
is the main program.
1 | #!/usr/bin/python3 |
The program accepts a base64-encoded string as input, decodes it into an ELF file, and checks whether the file contains any executable sections. If none exist, it executes the ELF.
This involves knowledge of ELF file structure: sections and segments are parallel concepts. The loader ignores section information when loading ELFs. Even if section headers are completely removed using strip --strip-section-headers
, the program can still run normally.
The solution is to manually modify the section information in the ELF file using a hex editor. Compile a program like the following into an ELF, then modify the executable permission flags (originally 06) in the section headers to 02 (non-executable) while preserving the actual executable segments.
1 |
|
Extremely Lame Filters 2
An upgraded version of the previous challenge. The modified fairy.py
now checks: the program will only run if it not contains executable segments with non-zero content.
1 | #!/usr/bin/python3 |
Unlike sections, the loader sets memory permissions based on segment information. If the machine code resides in non-executable memory, it will cause a segmentation fault. Thus, simply modifying permissions won’t work here.
A viable approach (and the one I used) is segment overlapping: construct multiple segments in the ELF where:
- The machine code segment has no execute permission and appears earlier in the program header table.
- A blank executable segment with the same virtual address appears later in the table.
- The machine code entry point starts at offset
n
within its segment, while the blank segment’s size is exactlyn
(n ≠ 0). This ensures the blank segment’s permissions override the original segment’s permissions during loading.
This leverages the loader to copy the machine code into executable memory while passing the challenge’s checks.
To simplify segment management, I wrote assembly code:
1 | BITS 64 |
With a custom linker script:
1 | ENTRY(_start) |
After compilation, I manually modify the ELF:
- Remove executable permission from the
.text
segment. - Add executable permission to the
.text_exe
segment.
The diagram below illustrates the modifications:
Finally, use Python’s base64 encoding and pwntools to upload the file to the competition platform. Executing cat ./flag.txt
yields the flag.
After solving the second challenge, I discussed it with c10uds, who proposed a much simpler approach: Write a program similar to the first challenge, ensure the first n bytes of the executable segment are zeros, then directly set the executable segment’s size to n. This would pass the check in elf.py
while still allowing the loader to properly load and execute the full segment.
I was stunned.