Security and optimization in Resource Management

How to organize your app assets and avoid it from being cloned

André Nardelli
6 min readNov 11, 2020

Not rarely do we come across a story about an indie developer who had their app cloned. Or even some casual game that take hours to be downloaded or updated, complicating the players’ relaxing moment. This article proposes solutions regarding Resource Management, as well as promotes the discussion on such a relevant, though many times neglected, theme.

Our assets are like gold. We need to protect it!

For a solitaire game developer who is creating his own game engine, dealing with assets is something I had to concern since my very first app. Here I address some techniques like working with protected zip files, obfuscating its password and how to guarantee the genuine pair App<->Assets. Remember, nothing here is set in stone and, if there is any mistake, please let me know!

Problems

  • When your assets are located at filesystem or packed in assets storage (iOS/Android), the user (or, better saying, that willing hacker) can replace them and quickly publish a clone of your app.
  • Making use of a game engine, frameworks or SDKs is also taking risk of being cloned. To make matters worse, once someone publishes how to hack one of the three above, anyone can do that. Furthermore, how much time will you be uncovered by a security update?
  • How to optimize and guarantee that your app is reading the genuine assets. Worse still, the app downloads the resources at runtime.
  • Nowadays the apps are each time bigger.

Objectives

  • Avoid cloning your application or assets.
  • Group and compact files by affinity, or in order to stream them during client execution. Thus, it is easier to check the integrity of a limited number of zip files than check each asset.
  • Find the files in anywhere by loading them into RAM or VRAM when necessary.
  • Obfuscate passwords and binaries’ “unique ID”.

Proposed Solution

Get to know https://libzip.org/

As the name suggests, this library was created to manipulate zip files and, more important, the password protected ones. Libzip is a C library, which means that you can compile and bind it in any platform.

Firstly, lets define our file structure.

struct ResourceFile
{
const char* name;
char *content;
uint64_t fileSize;
ResourceFile()
{
}
~ResourceFile()
{
if (content)
{
free(content);
content = NULL;
}
}
};

The ResourceManager class (header). To keep this article easily digestible, each ResourceManager is responsible for just one zip file:

class ResourceManager
{
public:
ResourceManager();
~ResourceManager();
bool loadZip(const char* zipFileName, const char* key);
ResourceFile* loadFile(const char* fileName); // For debug purposes
ResourceFile* loadCompressedFile(const char* fileName);
private:
zip *zipFile;
};

ResourceManager.cpp

ResourceManager::ResourceManager()
{
}
ResourceManager::~ResourceManager()
{
if (zipFile)
zip_close(zipFile);
}
bool ResourceManager::loadZip(const char* zipFileName, const char* key)
{
#if defined(__ANDROID__)
AAssetManager* mgr = OS::getInstance()->getAndroidApp()->activity->assetManager;
AAsset *asset = AAssetManager_open(mgr, zipFileName, AASSET_MODE_BUFFER);
size_t fileLength = AAsset_getLength(asset);
fileContent = new char[fileLength+1];
AAsset_read(asset, fileContent, fileLength);
fileContent[fileLength] = '\0';
zip_error zerr;
zip_source_t *src = zip_source_buffer_create(fileContent, fileLength, 0, &zerr); // Thanks for the recent versions of Libzip that can read from buffer
if (!src)
{
//Impossible to open zipFile
}
else
{
zipFile = zip_open_from_source(src, 0, &zerr);
}
AAsset_close(asset);
#else
int err = 0;
zipFile = ::zip_open(zipFileName, 0, &err);
if (err)
{
printf("Impossible to open zipFile %s\n", zipFileName);
return false;
}
#endif
err = zip_set_default_password(zipFile, key);
if (err < 0)
{
printf("Wrong password to open zipFile %s\n", zipFileName);
return false;
}
return true;
}
ResourceFile* ResourceManager::loadFile(const char* fileName)
{
#if defined(__ANDROID__)
AAssetManager* mgr = OS::getInstance()->getAndroidApp()->activity->assetManager;
AAsset *asset = AAssetManager_open(mgr,("conf/" + to_string(fileName)).c_str(), AASSET_MODE_BUFFER);
if (asset) {
uint64_t fileLength = AAsset_getLength(asset);
ResourceFile *rf = new ResourceFile();
rf->name = fileName;
rf->size = fileLength;
rf->content = new char[fileLength + 1];
AAsset_read(asset, rf->content, fileLength);
rf->content[fileLength] = '\0';
AAsset_close(asset);
return rf;
} else
{
return NULL;
}
#else
long size;
std::ifstream File(("" + to_string(fileName)).c_str(), std::ifstream::binary);
if(File.is_open())
{
File.seekg(0, std::ifstream::end);
size=File.tellg();
File.seekg(0);
ResourceFile* rf = new ResourceFile();
rf->name = fileName;
rf->fileSize = size ;
rf->content = (char*) calloc(size + 1, sizeof(char));
File.read (rf->content, size);
File.close();
return rf;
}
else
{
printf("Unable to open %s\n", fileName);
return NULL;
}
#endif
}
ResourceFile* ResourceManager::loadCompressedFile(const char* cFileName)
{
#if defined(DEBUG)
return loadFile(cFileName);
#endif
zip_file *f = zip_fopen(zipFile, cFileName, 0);
if (!f)
{
printf("Unable to open %s\n", cFileName);
return NULL;
}
const char *name = cFileName;
struct zip_stat st;
zip_stat_init(&st);
zip_stat(zipFile, name, 0, &st);
ResourceFile* rf = new ResourceFile();
rf->name = cFileName;
rf->fileSize = st.size;
rf->content = (char*) calloc(st.size + 1, sizeof(char));
zip_fread(f, rf->content, st.size);
zip_fclose(f);
return rf;
}

Using the class

For a simple or standalone app you can use ResourceManager as a singleton, which is not the case in this example:

ResourceManager *rm = new ResourceManager();
rm->loadZip("MainMenu.zip", "83218ac34c183c426781fe4bde918ee4");
resourceFile *rf = rm->loadCompressedFile(FILE_NAME);

Setting a password to your zip file is the first attempt to avoid clonning your app. Also, as some decompilers can display the strings of your code, it is recommended to obfuscate the key, as proposed by Stephan Brumme: https://create.stephan-brumme.com/hide-strings-executable/. I believe a “Util” class is the better place for this static function:

class Util
{
public:
// Function from https://create.stephan-brumme.com/hide-strings-executable/
inline static std::string stringObfuscate(const std::string& input)
{
const size_t passwordLength = 16;
static const char password[passwordLength] = "invalid pointer";
std::string result = input;
for (size_t i = 0; i < input.length(); i++)
result[i] ^= ~password[i % passwordLength];
return result;
}
};

First let’s generate the obscure key. You can create a separated executable just to create the obfuscated strings:

std::string sha1 = Util::stringObfuscate("83218ac34c183c426781fe4bde918ee4");
printf("\ndecode(\"");
for (size_t i = 0; i < sha1.length(); i++)
printf("\\x%02x", sha1[i] & 0xFF);
printf("\");");

Replacing the zip password (83218ac34c183c426781fe4bde918ee4)…

rm->loadZip("MainMenu.zip", Util::stringObfuscate("\xae\xa2\xbb\xaf\xab\xf7\xf8\xec\xbb\xf3\xa7\xa9\xb8\xf9\xb9\xcd\xa0\xa6\xb1\xaf\xf5\xf3\xaf\xbd\xeb\xf5\xaf\xa0\xb3\xff\xe8\xcb").c_str());

Don’t forget to set the zip file password as the original value, e.g. 83218ac34c183c426781fe4bde918ee4.

Hmmm, is it 100% safe? Of course not.

“Never underestimate the determination of a kid who is time-rich and cash-poor.”
― Cory Doctorow, Little Brother

Ensuring the pair Code/Assets

You can still make the hacker’s life harder if you compare the checksum hash of the zip with the hash that your code is expecting.

For that, let me introduce the PicoSHA2: https://github.com/okdshin/PicoSHA2 from Shintarou Okada. So the final version of our “Util” class will be like this:

class Util
{
public:
// Function from https://create.stephan-brumme.com/hide-strings-executable/
inline static std::string stringObfuscate(const std::string& input)
{
const size_t passwordLength = 16;
static const char password[passwordLength] = "invalid pointer";
std::string result = input;
for (size_t i = 0; i < input.length(); i++)
result[i] ^= ~password[i % passwordLength];
return result;
}
inline static std::string hashString(const char * content)
{
return picosha2::hash256_hex_string(to_string(content));
}
// Workaround to std::to_string
template <typename T>
inline static std::string to_string(T value)
{
std::ostringstream os ;
os << value ;
return os.str() ;
}
};

First we need to generate the checksum SHA256 of the zip file. That separated executable, remember?

printf("\n %s \n", Util::hashString(zipFile->content));

Then compare the keys to check if the expected file was delivered:

if (strcmp(Util::hashString(to_string(zipFile->content)).c_str(),
stringObfuscate("\xa1\xa2\xec\xac\xf1\xf2\xad\xbc\xed\xf2\xaf\xa8\xb9\xac\xbc\xc8\xa5\xa2\xea\xff\xf5\xa7\xa8\xe8\xbf\xa1\xa3\xf5\xbe\xf9\xee\xca\xae\xf4\xba\xf8\xaa\xa0\xa9\xeb\xb7\xf4\xae\xf3\xbb\xab\xe9\x9e\xa0\xa9\xeb\xfb\xab\xa3\xad\xeb\xb6\xa8\xaf\xf5\xef\xa3\xbd\xc9").c_str()) != 0)
{
printf("Invalid Resource File!\n");
}
else
{
resourceFile *rf = rm1->loadCompressedFile(FILE_NAME);
if (rf)
{
printf("%s", rf->content);
delete rf;
}
}

Going further

Besides the suggestions above, I usually invert the obfuscated strings and/or split each one in 2, so it become harder to detect while decoding. What do you usually do about that? Please let me know in a comment :D

Loading the resources asynchronously:

People dislike waiting, so, what about showing something interesting while loading the resources? Then the main function would look like:

#include <iostream>
#include "include/Util.h"
#include <future>
#include <cstring>
#include "include/ResourceManager.h"
#include <unistd.h>
bool resourcesLoaded = false;
bool loadingResources()
{
GGE::ResourceManager *rm1 = new GGE::ResourceManager();
rm1->loadZip("MainMenu.zip", GGE::Util::stringObfuscate("\xae\xa2\xbb\xaf\xab\xf7\xf8\xec\xbb\xf3\xa7\xa9\xb8\xf9\xb9\xcd\xa0\xa6\xb1\xaf\xf5\xf3\xaf\xbd\xeb\xf5\xaf\xa0\xb3\xff\xe8\xcb").c_str());
GGE::ResourceFile *zipFile = rm1->loadFile("MainMenu.zip");
std::string sha1 = GGE::Util::stringObfuscate("83218ac34c183c426781fe4bde918ee4");
// Just to see the thread running
sleep(1);
if (strcmp(GGE::Util::hashString(zipFile->content).c_str(), GGE::Util::stringObfuscate("\xa1\xa2\xec\xac\xf1\xf2\xad\xbc\xed\xf2\xaf\xa8\xb9\xac\xbc\xc8\xa5\xa2\xea\xff\xf5\xa7\xa8\xe8\xbf\xa1\xa3\xf5\xbe\xf9\xee\xca\xae\xf4\xba\xf8\xaa\xa0\xa9\xeb\xb7\xf4\xae\xf3\xbb\xab\xe9\x9e\xa0\xa9\xeb\xfb\xab\xa3\xad\xeb\xb6\xa8\xaf\xf5\xef\xa3\xbd\xc9").c_str()) != 0)
{
printf("Invalid Resource File!\n");
}
else
{
GGE::ResourceFile *rf = rm1->loadCompressedFile("shader.vert");
if (rf)
{
printf("%s", rf->content);
delete rf;
}
}
delete zipFile;
delete rm1;
return true;
}
int main()
{
std::future<bool> fut = std::async (std::launch::async, loadingResources);
while(fut.wait_for(std::chrono::seconds(0)) != std::future_status::ready)
{
printf("Something Interesting... \n");
}
fut.get();
}

Here, at the end of this story…

…Apart from organizing resources being extremely important, Hackers will hack, no matter what. Our concern is being safe in between their search for hackeables and how safe our apps are, besides getting things tidy.

Has this content helped you? Please consider supporting my work here !

--

--

André Nardelli

Game Developer and founder of Gestalt Development Studio