domingo, 5 de abril de 2015

Como embutir arquivos binários dentro de um executável do Windows

Olá, programadores. Depois de muito tempo em "off", venho trazer uma dica muito interessante e útil para quem programa em C/C++ no Windows.

Eu sempre tive curiosidade de saber como embutir um arquivo binário inteiro dentro de um programa, e recentemente descobri que essa tarefa é relativamente simples. Este é um artigo técnico que descreve os conceitos principais por trás deste processo e o passo-à-passo que você precisa seguir se deseja fazer isso.

Já havia reparado que alguns programas (especialmente os mais antigos) aparentemente se utilizam de muitos dados porém sem ter sido instalado nenhum arquivo binário adicional junto com eles. Então de onde eles lêem os dados que precisam? A resposta pode ser que eles não lêem os dados de nenhum arquivo em disco, mas sim de arquivos que foram "embutidos" dentro do executável durante a compilação. Isso é útil para ocultação de informações, já que o usuário não vê nenhum arquivo de dados na pasta do programa. Obviamente, um usuário com conhecimento técnico conseguiria ver as informações textuais (texto em código ASCII) embutidas abrindo o executável em um visualizador/editor binário, porém ele não saberia identificar os dados binários que só o programa sabe interpretar.

O que significa isso?


Acho importante esclarecer o que significa "embutir arquivos binários no executável", para quem ainda não entendeu. Quando você precisa acessar informações de algum arquivo no disco, você sabe que isso é muito fácil, basta usar a biblioteca padrão do C++, já que as funções fopen, fseek, fgetc e fread (ou de forma mais moderna com os streams do C++) são suficientes para abrir um arquivo binário e ler os bytes dele tanto de forma sequencial ou aleatória. Mas para isso, obviamente você precisa ter esses arquivos salvos em algum lugar no disco. Até aí nenhuma novidade.

Suponha agora que você tem uma série de arquivos binários que não serão modificados em nenhum momento pelo programa (precisam ser apenas lidos), e que por algum motivo qualquer você não quer que o usuário do seu programa os veja, geralmente porque não há necessidade, como no caso da grande maioria dos arquivos binários (porque somente o programa consegue interpretar os dados), ou porque há o risco de o usuário resolver abrí-los e ver alguma coisa que não deve ou de alguma maneira acabar corrompendo os dados, fazendo com que o programa funcione anormalmente ou mesmo deixe de funcionar. Aí surge a questão: como mandar o programa para o usuário sem que ele tenha acesso aos arquivos de dados? À primeira vista isso parece impossível, porque o programa precisa que eles estejam disponíveis no disco para a leitura, então é necessário instalá-los junto com o programa. No entanto, existe sim uma solução que envolve apenas algumas alterações no código-fonte do programa.

A solução


A solução vem através de arquivos de recurso e da API nativa do Windows. Observe que por razões óbvias essa solução não é portável, então infelizmente você não pode fazer o que este artigo sugere se estiver programando para outro sistema, como o Linux. Para quem já conhece e morre de medo de acessar a API nativa do Windows, porque acha muito complexa e confusa (e muitas partes realmente são), não tenha medo. A API que lida com recursos binários é extremamente simples e envolve no mínimo 3 funções com pouquíssimos parâmetros. Resumindo o passo-à-passo seria o seguinte:

1. Criar um arquivo de recurso e incluí-lo no seu projeto;

2. Editar o arquivo de recurso para instruir o compilador a embutir os arquivos binários desejados;

3. Alterar o código-fonte do programa para que ao invés de abrir e acessar arquivos em disco, ele acesse os dados dos arquivos que serão embutidos no executável.

Criando um arquivo de recurso (.rc)


Pois bem, a primeira coisa a fazer é criar um arquivo de recurso e incluí-lo no seu projeto. Estou considerando que você programa usando o Microsoft Visual Studio Express 2012 (que é a versão gratuita dessa IDE), ou alguma outra versão não muito antiga. Para criar um arquivo de recurso, basta clicar com o botão direito em cima do nome do seu projeto dentro do Solution Explorer, selecionar Add e depois New Item. Na janela que aparece, abra o grupo Visual C++ e clique no subgrupo Utility. Selecione o tipo "Text File (.txt)", mas renomeie ele para Resource.rc. Se você estiver usando a versão paga do Visual Studio, talvez exista a opção "Resource File (.rc)", mas no caso do Visual Studio Express, ele não tem um editor específico para arquivos de recurso, o que não te impede de criar um arquivo qualquer e colocar um nome com extensão .rc. Arquivos com extensão .rc são compilados não pelo compilador de C/C++, mas sim por um compilador especial chamado RC, específico para a compilação de arquivos de recurso. Você não precisa compilar eles separadamente, pois quando você der um build no projeto, o Visual Studio vai invocar o RC automaticamente.

Definindo os arquivos a embutir


Depois de criado o arquivo de recurso, você precisa editá-lo para definir os recursos a serem compilados, isto é, quais arquivos você quer embutir. Isso é feito através de uma linguagem declarativa específica que não tem nada a ver com C++. Mas não se preocupe, dessa linguagem específica você precisa aprender somente um comando para embutir arquivos no programa. Para editar o arquivo, na versão Express não adianta clicar duas vezes nele porque o Visual Studio vai dizer que a edição de arquivos de recurso não é suportada. Isso acontece porque a versão paga tem um editor especializado. Mas você pode editar ele com qualquer editor de texto. O que você precisa fazer então é clicar com o botão direito em cima dele, clicar em "Open With..." e selecionar "C++ Source Code Editor". Clique no botão ao lado onde diz "Set as Default" para manter essa opção como padrão, e clique em OK. A partir daí você pode clicar duas vezes em cima do arquivo .rc para editá-lo normalmente.

Com o arquivo de recurso aberto, você pode começar a incluir os arquivos que deseja embutir. Para isso, você deve utilizar o comando RCDATA. A sintaxe é extremamente simples, conforme segue:

NomeDoRecurso RCDATA "nome_do_arquivo"

Onde NomeDoRecurso é um nome único através do qual você poderá acessar esse arquivo de dentro do código C++. O comando RCDATA no meio é obrigatório e indica que você deseja incluir dados binários. O "nome_do_arquivo" é o nome real do arquivo (com extensão e tudo) exatamente como está salvo no disco, que será embutido. Você pode ter múltiplas linhas RCDATA num único arquivo .rc. Segue um exemplo, para incluir 3 arquivos binários no programa:

DadosDeFuncionarios RCDATA "funcionarios.dat"
DadosFinanceiros RCDATA "financas.dat"
Graficos RCDATA "graficos.bin"

Acessando os arquivos embutidos no programa


Concluída a edição do arquivo de recurso, você deve agora alterar seu código-fonte. Nos trechos onde você acessaria os arquivos em disco usando as funções típicas como fopen, fgetc, fread, fclose, etc. você precisa eliminar tudo isso para acessar os arquivos embutidos, já que eles não vão existir em disco no computador do usuário. Ao invés disso você ira utilizar as funções da API do Windows que são responsáveis por carregar recursos. Lembre-se que para acessar a API do Windows você precisa incluir o arquivo-cabeçalho <Windows.h> no seu código.

As 3 principais funções envolvidas no processo são as seguintes: FindResource, LoadResource e LockResource. Essas funções são suficientes para carregar na memória os dados dos arquivos embutidos e disponibilizá-los para acesso aleatório. Importante lembrar que você estará lidando diretamente com os bytes contidos nos arquivos, ou seja, não são mais exatamente "arquivos" no próprio sentido da palavra, mas sim blocos de bytes. Apesar disso a informação é exatamente a mesma, já que os bytes são os mesmos do arquivo físico, é como se o arquivo fosse mapeado na memória, para quem entende desse jargão.

Agora descrevendo as 3 funções envolvidas, geralmente você vai chamá-las uma seguida imediatamente pela outra, pois cada uma precisa do valor retornado pela anterior. FindResource recebe 3 parâmetros, o primeiro é NULL, o segundo é o nome do recurso conforme foi definido no arquivo de recurso, e o terceiro é RT_RCDATA. Essa função retorna uma estrutura do Windows chamada HRSRC. Não importa o que é essa estrutura. Basta saber que você deve recebê-la e em seguida passá-la como o segundo parâmetro para a função LoadResource. O primeiro parâmetro dela é NULL. Essa função retorna uma outra estrutura do Windows chamada HGLOBAL. Também não interessa o que ela é. Basta recebê-la e passá-la como o único parâmetro para a função LockResource. Essa função é a que finalmente retorna um ponteiro void, que aponta para o primeiro byte do "arquivo embutido" mapeado na memória. A partir deste ponto, você já tem acesso aos dados binários. Como o ponteiro retornado é do tipo void, obviamente você precisa efetuar um cast (conversão de tipo) dele para o tipo apropriado. Por exemplo, se o arquivo continha texto em formato ASCII, você pode dar um cast para um ponteiro de char e atribuir a um objeto string, assim terá disponível a string completa de caracteres. Isso seria equivalente a carregar o arquivo e ler todos os caracteres dele usando fread ou fgetc.

Um exemplo prático


Confira a seguir um exemplo completo deste processo, desde a definição do arquivo de recurso. Neste exemplo, considere que existe um arquivo em disco chamado Teste.dat na pasta de trabalho do programa e que ele contêm texto em codificação ASCII. O texto do arquivo deve terminar com um caractere nulo (byte de valor 0), já que o programa de exemplo converte o ponteiro para uma string e strings devem terminar com o caractere nulo.

Arquivo de recurso (resource.rc)

RecursoTeste RCDATA "Teste.dat"

Arquivo-fonte C++ (main.cpp)

#include <Windows.h>
#include <string>
#include <iostream>
#include <cstdlib>
using namespace std;

int main(int argc, char* args[])
{
HRSRC handle = FindResource(NULL, "RecursoTeste", RT_RCDATA);
HGLOBAL resource = LoadResource(NULL, handle);
void* data = LockResource(resource);

string textoDoArquivo = (char*)data;

cout << textoDoArquivo << endl;

system("pause");
return 0;
}

Executando este programa você deve obter como saída em uma janela de console (tipo o CMD, o prompt de comando) o texto do arquivo seguido por uma mensagem específica ("Pressione qualquer tecla para continuar...") que aparece por causa da linha onde invocamos o comando "pause" através da função system.

Lendo estruturas inteiras do arquivo binário


Importante lembrar que, no caso de você ter salvo alguma estrutura do programa (struct) em um arquivo em disco através das funções típicas do C++ (fwrite, por exemplo), é possível converter o ponteiro void do recurso para o mesmo tipo da estrutura, para ter acesso os dados. Obviamente, se isso vai funcionar ou não, depende muito do que são os membros da estrutura. Se a estrutura armazena ponteiros, você não deve salvá-la por inteiro em um arquivo usando a função fwrite, já que isso fará com que os ponteiros sejam armazenados, e como ponteiros são diferentes para cada execução do programa, na hora de ler a estrutura de volta, os ponteiros lá não serão válidos.

Por exemplo, considere a seguinte estrutura:

struct Pessoa
{
char nome[100];
int idade;
};

Suponha que você salvou uma variável do tipo dessa estrutura em um arquivo em disco através da função fwrite, como por exemplo:

Pessoa p;
p.nome = "Fulano de Tal";
p.idade = 26;

FILE* file = fopen("Teste.dat", "wb");
fwrite(&p, sizeof(p), 1, file);
fclose(file);

Você pode embutir o arquivo gerado com a estrutura no executável através do processo descrito neste artigo, e depois obter os valores dela de volta em uma variável do mesmo tipo, bastando converter o ponteiro void que é retornado pela função LockResource para um ponteiro deste tipo, como por exemplo:

void* data = LockResource(resource);
Pessoa* p = (Pessoa*)data;

A partir deste ponto, a variável p contém os mesmos valores que foram salvos no arquivo que foi embutido. Este resultado é equivalente a usar a função fread para ler a estrutura inteira do arquivo no caso dele estar em disco ao invés de embutido.

Enfim, aqui termina este longo artigo. Pelo que você pode perceber, o processo é muito simples. Quaisquer dúvidas à respeito do assunto, estou à disposição via e-mail.

Espero que tenham gostado!