2

I have an apk that was written in xamarin. The meta data suggests there are lots of dlls files this application uses. I found a file called libmonodroid_bundle_app.so which when disassembled in IDA appears to be a packer/unpacker with functions like inflate, my_inflate, install_dll_config_files etc.

I want to unpack whatever dll files are contained in this, can anyone give any suggestions how I can do this? There is an x86 compiled .so file, so perhaps I can execute it in windows somehow.

edit: It appears it is packed with zlib. There are nice named headers in the file so I will try exporting the raw data then using a zlib library to decompress.

3
  • 2
    if you figure it out, post an answer so others can learn how to do it.
    – Igor Skochinsky
    Oct 8, 2017 at 12:10
  • Found the mkbundle source to create the source of the so. Should be easy to write an unpacker as it just uses a simple deflate stream. Will put it up on github when complete. github.com/mono/mono/blob/master/mcs/tools/mkbundle/mkbundle.cs
    – rollsch
    Oct 8, 2017 at 19:31
  • Ok I unpacked them, but I had to hard code some offsets as I couldn't figure out how to pull them out of the ELF. Eg I had the _data segment aligned on 64 byte boundary with every word with an offset of -0x1000 and every second dword with an offset of -0xD60, the 0x1000 was easy to figure out from the ELF header as it contained this offset, but the D60 I could not figure out where it came from.
    – rollsch
    Oct 8, 2017 at 23:46

4 Answers 4

3

Thanks to the original poster who figured it out (and the other suggestion for foremost). I had to audit some of our own xamarin apps and wanted to unpack the code so I have what an attacker would be able to get to. After decompiling the .so and checking out the headers, I'm certain it's possible to derive the magic offset from the file (with the number of dlls you have). After spending a couple of hours though, it turned out quicker to just extract all of the gzip streams in the file :) So, thank you all, here's what code I ended up with to get work done:

using System;
using System.IO;
using System.IO.Compression;
using System.Collections.Generic;
using System.Text;

using Kaitai; //http://formats.kaitai.io/elf/csharp.html

public class unpack {
static void Main(string[] args)
    {
        var unpacked = "unpacked";
        Directory.CreateDirectory(unpacked);

        var path = @"path\libmonodroid_bundle_app.so";
        var bytes = File.ReadAllBytes(path);

        var elf = Elf.FromFile(path);

        var rodata = elf.Header.SectionHeaders.FirstOrDefault(x => x.Name.Equals(".rodata"));
        if(rodata == null)
        {
            Console.WriteLine(".rodata not found");
            Console.ReadKey();
            return;
        }

        int dllCount = 0;

        //read in all the packed file names
        var packedFiles = new List<string>();
        uint addr = (uint)rodata.Addr;
        while(true)
        {
            //up to 16 bytes of alignment
            uint i;
            for(i = 0; i < 16; i++)
                if(bytes[addr + i] != 0)
                    break;

            if(i == 16)
                break; //We found all the files
            else
                addr += i;

            var name = GetString(bytes, addr);
            if(string.IsNullOrWhiteSpace(name))
                break;

            //We only care about dlls
            if(!name.EndsWith(".dll"))
                break;

            packedFiles.Add(name);
            addr += (uint)name.Length + 1u;
            ++dllCount;
        }


        var data = elf.Header.SectionHeaders.FirstOrDefault(x => x.Name.Equals(".data"));
        if(data == null)
        {
            Console.WriteLine(".data not found");
            Console.ReadKey();
            return;
        }

        int ixGzip = 0;

        //Read in all the packed file pointers and sizes and then decompress and dump them
        addr = (uint)(data.Offset);
        for(int i = 0; i < packedFiles.Count; )
        {
            ixGzip = findNextGZIPIndex(bytes, ixGzip);
            if(ixGzip > 0)
            {
                var ptr = ixGzip;
                var length = GetBigEndianUInt32(bytes, addr + 8);

                var compressedbytes = new byte[length];
                if(ptr + length <= bytes.LongLength)
                {
                    Array.Copy(bytes, ptr, compressedbytes, 0, length);
                    try
                    {
                        var decompbytes = Decompress(compressedbytes);
                        File.WriteAllBytes($@"{unpacked}\{packedFiles[i]}", decompbytes);
                        addr += 0x10; //sizeof assembly_bundle struct
                        i++;
                    }
                    catch(Exception e)
                    {
                        Console.WriteLine($"Failed to decompress file: {packedFiles[i]} {e}");
                    }
                }
            }
        }
    }

    private static int findNextGZIPIndex(byte[] bytes, int ixGzip)
    {
        for(int j = ixGzip + 2; j < bytes.Length; j++)
        {
            if(bytes[j - 1] == 0x1f && bytes[j] == 0x8b)
            {
                ixGzip = j - 1;
                return ixGzip;
            }
        }

        return 0;
    }

    static byte[] Decompress(byte[] data)
    {
        using(var compressedStream = new MemoryStream(data))
        using(var zipStream = new GZipStream(compressedStream, CompressionMode.Decompress))
        using(var resultStream = new MemoryStream())
        {
            zipStream.CopyTo(resultStream);
            return resultStream.ToArray();
        }
    }

    public static uint GetBigEndianUInt32(byte[] bytes, uint address)
    {
        uint byte1 = (uint)bytes[(int)address + 3] << 24;
        uint byte2 = (uint)bytes[(int)address + 2] << 16;
        uint byte3 = (uint)bytes[(int)address + 1] << 8;
        uint byte4 = (uint)bytes[(int)address];
        return (byte1 + byte2 + byte3 + byte4);
    }

    public static string GetString(byte[] bytes, uint address)
    {
        int maxLength = 255;
        //Search for a null char up to the limit
        for(int i = (int)address; i < address + maxLength; i++)
        {
            if(bytes[i] == 0)
            {
                maxLength = i - (int)address;
                break;
            }
        }

        var buffer = new byte[maxLength];
        Array.Copy(bytes, address, buffer, 0, maxLength);
        return Encoding.ASCII.GetString(buffer);
    }
}
3
  • Just our of curiosity has this led you to use some form of obfuscator on your dll files prior to packing?
    – rollsch
    Oct 26, 2017 at 12:06
  • Kind of - I needed to verify what the obfuscatirs accomplished for our internal audit :) Basically, you don't want obfuscation as it's sort of useless, you'd much prefer not to have anything in an app that's not safe to be public :) But, we still use some obfuscation, mainly to prevent constantly having to explain why the code is not obfuscated to external "audits" (the nearly-free-we-want-to-sell-our-very-great-secure-tool kind)
    – JR-Fire
    Oct 27, 2017 at 13:45
  • The main reason I see for obfuscation is to stop people blatantly stealing an entire compilable project. Most obfuscators means simply making the time taken to reverse engineer on par with writing from scratch. If you achieve that time delay most the time you prevent theft. Though if your code is valuabke enough then as your said an obfuscator is useless.
    – rollsch
    Oct 27, 2017 at 19:25
3

This works very well cross-platform. Make sure to install these python packages:

sudo pip install pyelftools
sudo pip install yara-python

this will export all dlls from libmonodroid_bundle_app.so:

from elftools.elf.elffile import ELFFile
from zipfile import ZipFile
from cStringIO import StringIO
import gzip, string

data = open('libmonodroid_bundle_app.so').read()
f = StringIO(data)
elffile = ELFFile(f)
section = elffile.get_section_by_name('.dynsym')

for symbol in section.iter_symbols():
  if symbol['st_shndx'] != 'SHN_UNDEF' and symbol.name.startswith('assembly_data_'):
    print symbol.name
    dll_data = data[symbol['st_value']:symbol['st_value']+symbol['st_size']]
    dll_data = gzip.GzipFile(fileobj=StringIO(dll_data)).read()
    outfile = open(symbol.name[14:].replace('_dll', '.dll'), 'w')
    outfile.write(dll_data)
    outfile.close()

Adapted from https://github.com/maldroid/maldrolyzer/blob/master/plugins/z3core.py

2

Here is my solution to unpack the files in C#.

I could not figure out how the 64 byte alignment works so I hard coded an offset of 0xD60 for the words that did not lie on the alignment boundary. I believe this is the offset of the next free page.

If anyone else wants to use this solution they will either need to find the page offset manually using IDA etc or figure out how to get it dynamically.

I use the katai struct library to parse the elf file.

using Kaitai; //http://formats.kaitai.io/elf/csharp.html

static void Main(string[] args)
{

    var path = @"C:\path\libmonodroid_bundle_app.so";
    var bytes = File.ReadAllBytes(path);

    var elf = Elf.FromFile(path);

    var rodata = elf.Header.SectionHeaders.FirstOrDefault(x => x.Name.Equals(".rodata"));
    if (rodata == null)
    {
        Console.WriteLine(".rodata not found");
        Console.ReadKey();
        return;
    }

    //read in all the packed file names
    var packedFiles = new List<string>();
    uint addr = (uint)rodata.Addr;
    while (true)
    {
        //up to 16 bytes of alignment
        uint i;
        for (i = 0; i < 16; i++) if (bytes[addr + i] != 0) break;

        if (i == 16) break; //We found all the files
        else addr += i;

        var name = GetString(bytes, addr);
        if (string.IsNullOrWhiteSpace(name)) break;

        //We only care about mono dlls
        if (!name.EndsWith(".dll")) break;

        packedFiles.Add(name);
        addr += (uint)name.Length + 1u;
    }


    var data = elf.Header.SectionHeaders.FirstOrDefault(x => x.Name.Equals(".data"));
    if (data == null)
    {
        Console.WriteLine(".data not found");
        Console.ReadKey();
        return;
    }

    //Read in all the packed file pointers and sizes and then decompress and dump them
    addr = (uint) (data.Offset);
    for (int i = 0; i < packedFiles.Count; i++)
    {
        //0xD60 is a magic offset due to 64 byte alignment. I am not sure how to dynamically generate this
        //offset hence this solution will need you to manually find this offset to work with other SO files.
        var ptr = GetBigEndianUInt32(bytes, addr+0xD60);    
        var length = GetBigEndianUInt32(bytes, addr+8);

        var compressedbytes = new byte[length];
        Array.Copy(bytes, ptr, compressedbytes, 0, length);
        try
        {
            var decompbytes = Decompress(compressedbytes);
            File.WriteAllBytes($@"{packedFiles[i]}", decompbytes);
        }
        catch (Exception e)
        {
            Console.WriteLine($"Failed to decompress file: {packedFiles[i]} {e}");
        }
        addr += 0x10; //sizeof assembly_bundle struct
    }




}

static byte[] Decompress(byte[] data)
{
    using (var compressedStream = new MemoryStream(data))
    using (var zipStream = new GZipStream(compressedStream, CompressionMode.Decompress))
    using (var resultStream = new MemoryStream())
    {
        zipStream.CopyTo(resultStream);
        return resultStream.ToArray();
    }
}

public static uint GetBigEndianUInt32(byte[] bytes, uint address)
{
    uint byte1 = (uint)bytes[(int)address + 3] << 24;
    uint byte2 = (uint)bytes[(int)address + 2] << 16;
    uint byte3 = (uint)bytes[(int)address + 1] << 8;
    uint byte4 = (uint)bytes[(int)address];
    return (byte1 + byte2 + byte3 + byte4);
}

public static string GetString(byte[] bytes, uint address)
{
    int maxLength = 255;
    //Search for a null char up to the limit
    for (int i = (int)address; i < address + maxLength; i++)
    {
        if (bytes[i] == 0)
        {
            maxLength = i - (int)address;
            break;
        }
    }

    var buffer = new byte[maxLength];
    Array.Copy(bytes, address, buffer, 0, maxLength);
    return Encoding.ASCII.GetString(buffer);
}
1

If you want to extract these packed files (assuming that they are, indeed, packed), I would suggest using a File Carver forensics tool - I use a Unix-based program, foremost.

File Carvers can search inside other files and attempt to find file headers, or signatures, that denote the start of a file.

If you succeed in finding files inside of whatever files you search, you might find that they aren't yet accessible because you remarked that they were compressed with zlib - easy! Just rename that file to somename.zip and extract it, or write some code to extract them all yourself if you really want to.

Best of luck!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.