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);
}