How to convert Android resource ID back into its name

Few weeks ago I made an attempt to reverse engineer some obscure Android APK. It was available only through some Chinese shop, obviously described in only one language there. Unfortunately, it turned out that every tool designed for reverse engineering APK files outputted source with mysterious resource IDs, as plain integers, which is not the most convenient way to read them. Therefore I started looking for any way to find some meaningful name from these ids. At the end of my development effort I found out, there is one file that usually might be used for that purpose – res/values/public.xml, as produced by apktool (if I remember correctly). However, according to its name it contains only public resources, so some of them are missing there (in my case at least some drawable type resources were missing). Therefore, I am publishing my program to do things even more reliably.

arscutils

This program requires my library created together, but which is separate project – libarsc. It is available, as usually through Github and also as a package to be downloaded from PyPI. Just type:

pip install libarsc

with proper privileges.

This is meant to be utility package, but for now it contains only one such tool: rid2name. Its purpose is to convert resource ID into name in format matching the one, programmers use in their Android apps. Therefore with its help it should be possible to make reversed program looks more similar to compiler input on the developer side. To use it, just feed it with resources.arsc file as first parameter, resource id as second one and optionally one of: fqdn, xmlid or json as third one. As a result you should get resource name as used in Java source, XML files or JSON meant for further processing. Example runs are:

$ python3 rid2name.py ../com.g_zhang.iMiniCam_39/original/resources.arsc 0x7f070000 xmlid
@com.g_zhang.iMiniCam:string/app_name
$ python3 rid2name.py ../com.g_zhang.iMiniCam_39/original/resources.arsc 0x7f070000 fqdn
com.g_zhang.iMiniCam.R.string.app_name
$ python3 rid2name.py ../com.g_zhang.iMiniCam_39/original/resources.arsc 0x7f070000 json
{"package": "com.g_zhang.iMiniCam", "type": "string", "key": "app_name"}
$ python3 rid2name.py ../com.g_zhang.iMiniCam_39/original/resources.arsc 0x7f070000
com.g_zhang.iMiniCam.R.string.app_name

There is also quite convenient interface inside Python source, so the file should be includable into bigger projects.

I have to give one warning now: my implementation of ARSC format is not complete, so some things might not work as expected, but from my tests of libarsc, out of 12 ARSC files, extracted from random APK files, I found on my phone, 3 of them failed (returned different MD5) to rebuild into exactly same binary (did not checked exactly what happened there).

libarsc

This is library that was used underneath arscutils. It is able to parse most of the ARSC file, with special treatment of naming part, that allowed creation of rid2name. It is still missing some important parts and if there will be need from my side to extracting some more things, I will implement the rest of the specification. I am also open to any pull requests to my Github repo.

Future

As you might noticed in usage listing, there is a topic of reverse engineering app, which name was shown there. In case I found something interesting inside, there will be another article, where I will try to share my findings.

Edit: my mistake, this is not my target app, just the package name was similar.

LKV373A: crafting ELF

This article is part of series about reverse-engineering LKV373A HDMI extender. Other parts are available at:

As we should now be able to follow any jump present in the code, it is now time to make analysis more automatic. My target tool for that purpose will be objdump. However, we still have firmware image as raw dump of memory. To be able to use objdump easily, we need to pack our firmware into some container understandable by objdump. Most obvious choice is ELF (Executable and Linkable Format) and this is what I am going to use.

For the purpose of packing data into ELF, I’ve made Python library that makes it easier. For now, it is able to split firmware image into sections, like .text or .data, so objdump will be able to disassembly only the parts of firmware that are in fact a code. Moreover, it can define symbols inside the binary, so it is possible to store information, where certain functions starts and ends, same for any variables, like strings. As of now, there is no CLI interface for the program. If it turns out that such interface is necessary (like for addition of many symbols), it will be added.

Library code can be downloaded from Github. Currently, any LKV373A-specific modifications to this library is stored on branch lkv373a, to not rubbish main – master branch. Throughout this tutorial, I assume, we are using code on this branch, so there might be some LKV373A-specifics, especially regarding enum types (i.e. processor architecture enum).

At this point, I need to warn, that I am not going to describe internal structure of ELF file, nor any features that might be visible from outside, like sections concept, so if you are not familiar with them, it is good time to learn about them, as it might be very difficult to understand, what I am writing about. There are many good resources explaining them. Ones I was using are: this blog post and this documentation.

Creating new ELF

Example code that creates brand new ELF file is as easy as:

 1 #!/usr/bin/python3
 2 # demo script for creating ELF
 3 import os
 4 from elf import *
 5 
 6 elf = ELF(e_machine=EM.EM_LKV373A)
 7 
 8 fp = os.open('lkv373a.fw.elf',os.O_CREAT|os.O_WRONLY)
 9 os.write(fp, bytes(elf))
10 os.close(fp)

This, at first does all necessary imports, then creates new ELF object in line 6, and, finally, converts it to bytes object and immediately writes to file descriptor. That’s it!

After this, you should get valid, empty ELF file for architecture called lkv373a, which, obviously does not exists and no other program know how to handle, but we are going to change that in future.

While creating ELF object, few things can be defined, in addition to architecture id. They are all described in documentation, I will mention near the end of this tutorial. You are also free to dig in structure of ELF object. There is no encapsulation in it and structure validation is very permissive, so even completely broken ELFs could be produced, if needed.

Adding section

Next step is to add some sections to our ELF file.

fw = os.open('LKV373A_TX_V3.0c_d_20161116_bin.bin',os.O_RDONLY)
fw_blob = os.read(fw, 0xffffffff)

irq_blob = fw_blob[:0x1000]
text_blob = fw_blob[0x7d100:0x7d100+0x0b53c0]
data_blob = fw_blob[0x0b53c0:0x0b53c0+0x102060]
smedia_blob = fw_blob[0x200000:0x200000+0x283105]

irq_id = elf.append_section('.irq',irq_blob,0)
txt_id = elf.append_section('.text',text_blob,0x7d100)
data_id = elf.append_section('.data',data_blob,0x0b53c0)
smedia_id = elf.append_section('.smedia',smedia_blob, 0x200000)

At first, I am extracting them from firmware image and then inserting them to ELF object. append_section is a handy wrapper to low-level modifications that must be done on ELF structure, hidden under what we can see as ELF instance (these low-level structures are, however still available to the user as ELF.Elf member).

Modifying section attributes

Ok, so now we have sections in our ELF file, ready to save to disk. Before that, one thing can yet be done: setting proper attributes. They tell readers, if program is able to write or execute sections of memory, among other features, I am going to ignore here. This might be useful, as some readers might be confused about what is code (text) and what is data. In our case, we have two text sections (.irq and .text), so we are going to set them executable flag (SHF_EXECINSTR). Furthermore, we will set SHF_ALLOC flag for any section that is going to be loaded into memory (so all of them).

This can be done with:

elf.Elf.Shdr_table[irq_id].sh_flags = SHF.SHF_ALLOC | SHF.SHF_EXECINSTR
elf.Elf.Shdr_table[txt_id].sh_flags = SHF.SHF_ALLOC | SHF.SHF_EXECINSTR
elf.Elf.Shdr_table[data_id].sh_flags = SHF.SHF_ALLOC
elf.Elf.Shdr_table[smedia_id].sh_flags = SHF.SHF_ALLOC

Adding segment

Segments are another concept, existing beside sections. They are stored in program header of ELF file and are somehow linked to section data. They allow to define another set of attributes to areas in memory. I don’t think they will be required to define, to perform analysis in objdump, but since at least one such program header, defining segment must exist in ELF file of type executable, there is interface similar to this for sections.

To define new segment, based on .text section, you can issue:

elf.append_segment(txt_id, flags='rx')

This also marks the segment as read and executable, but not writable.

Loading existing ELF

Loading existing ELF can be easily done from file with:

newelf, b = ELF.from_file('lkv373a.fw.elf')

Alternatively, it can also be loaded from bytes object:

fd = os.open('some.elf', os.O_RDONLY)
b = os.read(fd, 0xffff)
os.close(fd)
manualelf, b = Elf32.from_bytes(b)

In latter case, I assumed that os library is already imported into python.

Adding a symbol

This is very useful for making analysis of code. New symbol can be added using calls like:

elf.append_symbol('irq0', irq_id, 0, 0x44, STB.STB_GLOBAL, STT.STT_FUNC)
elf.append_symbol('sprintf', txt_id, 0x9b9f8-0x7d100, 0x78, STB.STB_GLOBAL,
        STT.STT_FUNC)
elf.append_symbol('thread_c_path', data_id, 0xba78a-0x0b53c0, 0xba7bb-0xba78a,
        STB.STB_LOCAL, STT.STT_OBJECT)

First call defines function of length 0x44 in .irq section. To do this, ID of .irq section must be known. Luckily, we want to add symbol at the beginning of the section, so as offset, 0 was provided.

In the second case, we also want to define a function, but now we only know absolute address of the function (0x9b9f8), but what we need to pass is offset in .text section. To achieve this, we need to subtract address of the start of .text section (0x7d100).

In the last example, we define a string as an object of certain address and length. Both address and length are computed by subtracting absolute addresses. This symbol will be marked as local, which is default behavior for append_symbol function.

Library documentation

There are many more things possible to do using makeelf library. What I showed here is mostly, what is possible using high-level wrappers, doing many things under the hood. But as there is also low-level interface, virtually anything is possible.

To make exploring interfaces easier, I’ve made doxygen documentation for most of the library. It can be found on my server, here. Feel free to use the library for anything you want.

Conclusion

The library presented here should allow us make one step further to easy to use reverse engineering environment. It will by the way allow to store new findings in easily-modifiable Python scripts.

What I showed in examples to library interfaces split LKV373A firmware image into 4 sections. At this moment I already know that there are at least 6 sections, where code and data are in two parts (forming ICDCDS layout, where I-irq, C-code and so on). Also there should be some more symbols possible to place at this moment.

If I succeed in porting objdump, or any other tool able to disassemble ELF file, next step would be to publish Python script, utilizing library presented here, that annotates LKV373A firmware. So stay tuned, I hope there will be many further interesting findings throughout this reversing process!