Repository: angr/cle (language: python)

Issue:
Many remote file mappings in elfcore is a pain
So the pain of analyzing a remote core dump is that all the file paths of binaries are stored as remote.

`elfcore` already has a way to address this called the `remote_file_mapping` option:
https://github.com/angr/cle/blob/master/cle/backends/elf/elfcore.py#L26

This is a dict that is used to do the mapping when going through the core file:
https://github.com/angr/cle/blob/master/cle/backends/elf/elfcore.py#L236

The PITA is specifying a dict for `remote_file_mapping` when there are a lot of mappings.

What the user wants is a `remote_file_mapper` option to specify a function that is called (which is more generic then a dict):
```
proj = angr.Project("./core-dump",
                    main_opts={"backend": "elfcore",
                                          "remote_file_mapper": lambda s: s.replace("/home/adamd/", "./")},
                    rebase_granularity=0x1000)
```

I talked about this with @ltfish and he's onboard. I'm creating this issue so I don't forget.

Also happy to take any suggestions.


Discussion / hints:

Fix patch:
diff --git a/cle/backends/elf/elfcore.py b/cle/backends/elf/elfcore.py
index d558ee21..33a14729 100644
--- a/cle/backends/elf/elfcore.py
+++ b/cle/backends/elf/elfcore.py
@@ -20,10 +20,26 @@
 class ELFCore(ELF):
     """
     Loader class for ELF core files.
+
+    One key pain point when analyzing a core dump generated on a remote machine is that the paths to binaries are
+    absolute (and may not exist or be the same on your local machine).
+
+    Therefore, you can use the options ```remote_file_mapping`` to specify a ``dict`` mapping (easy if there are a small
+    number of mappings) or ``remote_file_mapper`` to specify a function that accepts a remote file name and returns the
+    local file name (useful if there are many mappings).
+
+    If you specify both ``remote_file_mapping`` and ``remote_file_mapper``, ``remote_file_mapping`` is applied first,
+    then the result is passed to ``remote_file_mapper``.
+
+    :param executable:           Optional path to the main binary of the core dump. If not supplied, ELFCore will
+                                 attempt to figure it out automatically from the core dump.
+    :param remote_file_mapping:  Optional dict that maps specific file names in the core dump to other file names.
+    :param remote_file_mapper:   Optional function that is used to map every file name in the core dump to whatever is
+                                 returned from this function.
     """
     is_default = True # Tell CLE to automatically consider using the ELFCore backend
 
-    def __init__(self, *args, executable=None, remote_file_mapping=None, **kwargs):
+    def __init__(self, *args, executable=None, remote_file_mapping=None, remote_file_mapper=None, **kwargs):
         super().__init__(*args, **kwargs)
 
         self.filename_lookup = []
@@ -32,10 +48,18 @@ def __init__(self, *args, executable=None, remote_file_mapping=None, **kwargs):
         self.auxv = {}
         self.pr_fname = None
         self._main_filepath = executable
-        self._remote_file_mapping = remote_file_mapping if remote_file_mapping is not None else {}
         self._page_size = 0x1000 # a default page size, will be changed later by parsing notes
         self._main_object = None
 
+        if remote_file_mapping is not None:
+            self._remote_file_mapper = lambda x: remote_file_mapping.get(x, x)
+        else:
+            self._remote_file_mapper = lambda x: x
+
+        if remote_file_mapper is not None:
+            orig = self._remote_file_mapper
+            self._remote_file_mapper = lambda x: remote_file_mapper(orig(x))
+
         self.__extract_note_info()
 
         self.__reload_children()
@@ -233,7 +257,7 @@ def __parse_prpsinfo(self, desc):
 
     def __parse_files(self, desc):
         self._page_size = desc.page_size
-        self.filename_lookup = [(ent.vm_start, ent.vm_end, ent.page_offset * desc.page_size, self._remote_file_mapping.get(fn.decode(), fn.decode())) for ent, fn in zip(desc.Elf_Nt_File_Entry, desc.filename)]
+        self.filename_lookup = [(ent.vm_start, ent.vm_end, ent.page_offset * desc.page_size, self._remote_file_mapper(fn.decode())) for ent, fn in zip(desc.Elf_Nt_File_Entry, desc.filename)]
 
     def __parse_x86_tls(self, desc):
         self.__current_thread['segments'] = {}


Test patch:
diff --git a/tests/test_elfcore.py b/tests/test_elfcore.py
new file mode 100644
index 00000000..dd915f94
--- /dev/null
+++ b/tests/test_elfcore.py
@@ -0,0 +1,34 @@
+import os
+
+import cle
+
+def get_coredump_file():
+    return os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                        '../../binaries/tests/x86_64/coredump/true-libc.so.6-ld-linux-x86-64.so.2.core')
+
+def get_binary_directory():
+    return os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../binaries/tests/x86_64')
+
+def check_objects_loaded(ld):
+    # we should have child objects if everything loaded correctly
+    assert ld.elfcore_object.child_objects
+    for (_, _, _, fn) in ld.elfcore_object.filename_lookup:
+        assert not '/tmp/foobar/does-not-exist' in fn
+
+def test_remote_file_mapping():
+    remote_file_mapping = {"/tmp/foobar/does-not-exist/true": f"{get_binary_directory()}/true",
+                           "/tmp/foobar/does-not-exist/libc.so.6": f"{get_binary_directory()}/libc.so.6",
+                           "/tmp/foobar/does-not-exist/ld-linux-x86-64.so.2":
+                           f"{get_binary_directory()}/ld-linux-x86-64.so.2" }
+    ld = cle.Loader(get_coredump_file(),
+                    main_opts={"backend": "elfcore",
+                               "remote_file_mapping": remote_file_mapping})
+    check_objects_loaded(ld)
+
+def test_remote_file_mapper():
+    directory_for_binaries = get_binary_directory()
+    remote_file_mapper = lambda x: x.replace("/tmp/foobar/does-not-exist", directory_for_binaries)
+    ld = cle.Loader(get_coredump_file(),
+                    main_opts={"backend": "elfcore",
+                               "remote_file_mapper": remote_file_mapper})
+    check_objects_loaded(ld)