Desenvolvimento - Delphi
Assinatura Delphi
Não muito tempo atrás, eu pude ver em um dos fóruns na Internet uma pergunta sobre como seria possível afirmar se um programa teria sido compilado com o Delphi.
por José Manuel RodriguezIntrodução
Não muito tempo atrás, eu pude ver em um dos fóruns na Internet uma pergunta sobre como seria possível afirmar se um programa teria sido compilado com o Delphi. Ainda que tivesse havido um certo número de respostas recomendando o uso de aplicativos shareware e comerciais, como o famoso PE Explorer, não houve resposta direta à pergunta. O fato é que existe uma forma de fazer isso, e relativamente simples: desde o Delphi 3, qualquer arquivo: executável GUI, aplicativo de console ou DLL, incluindo controles ActiveX mas não Packages, tem uma assinatura Delphi embutida.
Recursos do Windows
A maioria dos aplicativos Windows contêm aquilo que chamamos de recursos (resources). Recursos foram introduzidos nas primeiras fases do Windows, dada sua natureza visual gráfica: ainda que seja perfeitamente possível gerar os elementos gráficos inteiramente através de código (exatamente da forma como programas editores de recursos fazem), é uma tarefa dura e complicada. Ao invés disso, desenvolvemos os elementos da interface gráfica visualmente, com ajuda de programas especiais chamados editores de recursos (por exemplo, desenhando uma caixa de diálogo num ambiente gráfico), e então dispomos nos locais adequados os demais elementos da interface como botões, caixas de edição, etc. Quando achamos que nosso desenho da tela está aceitável, o salvamos como um recurso binário e, a partir desse instante, ele está pronto para ser incluído no nosso EXE. Neste artigo, o termo EXE refere-se a qualquer arquivo que esteja no formato PE (Portable Executable) suportado pelo Win32, como EXEs, DLLs, OCXs, etc.
Sabendo que alguns desses tipos de recursos são utilizados repetidamente em quase todas as aplicações, definiu-se um conjunto padrão de recursos:
Tabela de aceleradores (accelerator table)
- Bitmap
- Cursor
- Caixa de Diálogo
- Enhanced metafile
- Fonte
- Ícone
- Menu
- Entrada de tabela de mensagens (message-table entry)
- Entrada de tabela de literais (string-table entry)
- Informação de Versão
Além desses recursos padrão, existe um outro tipo de recurso: recurso personalizado (custom resource) ou definido pelo usuário (user defined resource). Esses recursos são definidos conforme a necessidade do programador e são constituídos de dados binários que, por vezes, fazem sentido apenas para o programador ou aplicativo mas que, outras vezes, são dados em formatos bem conhecidos mas que não fazem parte do padrão como, por exemplo, arquivos WAV ou JPEG.
Voltando ao Delphi, seria de imaginar que cada formulário que criamos no IDE do Delphi seria internamente transformado num recurso padrão do tipo Caixa de Diálogo; entretanto, a verdade é outra. Dada a forma dual de trabalho no IDE (sincronização entre formulário e código), sua forma de interação com os componentes e armazenamento de RTTI, dentre outras, os criadores do Delphi decidiram armazenar cada formulário, juntamente com todos os seus componentes, como um recurso personalizado. Mais que isso, além de incluir bitmaps, ícones, etc, e os recursos personalizados derivados dos formulários, a partir da versão 3.0, o Delphi passou a incluir dois recursos personalizados adicionais: um contendo informações a respeito dos pacotes utilizados e outro que é verdadeiramente uma assinatura do ambiente de programação utilizado. Esse último recurso, específico da Borland, é chamado DVCLAL (Delphi Visual Component Library Access License) e é incluído automaticamente pelo link-editor, sem qualquer intervenção do programador, em todos os programas desenvolvidos com o Delphi. Em minha opinião o termo é confuso já que aplicativos de console que não utilizam a VCL também possuem esse recurso no seu EXE.
Recursos Dentro de um Programa
Como dito anteriormente, recursos são dados binários. De fato, são precisamente esses tipos de dados binários que são considerados recursos (.RES) e que podem ser gerados diretamente por um editor de recursos; contudo, existem também recursos em formato ASCII puro (basicamente, .RC, mas também outros como .DLG, etc). Para gerar os recursos na forma .RES a partir de sua forma textual (o fonte), é preciso compilar esse fonte através do uso de um compilador de recursos (Resource Compiler) como BRCC32. O fato importante é que, uma vez inseridos num PE, um recurso não pode mais ser modificado. Isso significa que é possível abrir um PE e analisar todos os seus recursos. Se for do seu desejo, por exemplo, você pode abrir um PE com o auxílio de um editor de recursos e traduzir todos os literais para o idioma de sua preferência, sem precisar do código fonte ou sequer de uma recompilação. A API do Windows oferece uma rica classe de funções que permitem o acesso e a manipulação desses recursos com um mínimo de esforço (algumas dessas rotinas não funcionam no NT) e, como veremos, a RTL e a VCL do Delphi oferecem funções ainda mais simples de se trabalhar. No nosso caso, para determinar se um certo programa foi desenvolvido com o Delphi, precisamos verificar a existência de um recurso chamado DVCLAL no PE e, existindo, verificar que o recurso corresponde, de fato, à assinatura do Delphi. Assim sendo, podemos ficar certos de que o programa foi compilado pelo Delphi.
A razão para a existência desses tipos de recurso e suas diferenças, não apenas entre as várias versões, mas até mesmo entre as distribuições Professional e C/S (ou Enterprise) de uma mesma versão não me é claro. Aparentemente, a Borland utiliza esses recursos juntamente com uma série de variáveis e rotinas definidas na unidade SysUtils não apenas para determinar se um programa foi compilado com o Delphi, mas também para evitar o uso fraudulento dos termos da licença e impossibilitar, por exemplo, a compra da versão Professional do Delphi 5 e a utilização da RTL/VCL da versão Enterprise na versão Professional para criar programas cujo desenvolvimento seria possível somente através da compra da versão Enterprise. No Delphi, existem duas variáveis do tipo array definidas em SysUtils: AL1s e AL2s. Um conjunto de procedimentos e funções também estão definidos: AL1 e AL2 (parecem ser utilizados para codificação e decodificação), GDAL e ALR (Get Delphi Access License e Access License Resource) e as funções auxiliares RCS e RPS (provavelmente Resource Client/Server e Resource Professional). Essas rotinas são chamadas de certas partes a cada vez que um módulo Delphi (DLL e pacotes incluídos) é carregado para a memória. Caso você não esteja utilizando a biblioteca correspondente a seu ambiente, que implica uma violação dos direitos de licença, o procedimento ALV (Access License Violation?) é chamado, que então impede a execução do programa. Assim, por exemplo, GDAL é chamada por DBTables.pas (que impede o uso do BDE sem a devida licensa) ou por ScktComp.pas (que impede o desenvolvimento de aplicativos TCP/IP sem a devida versão do Delphi).
Ao Trabalho!
Primeiramente, temos que abrir o arquivo. Para fazer isso, temos que fazer uma chamada à API LoadLibraryEx. Por que utilizar essa rotina ao invés da verão mais familiar, LoadLibrary? Bem, a função LoadLibrary é utilizada basicamente para carregar uma DLL no espaço de memória do processo e, por esta razão, além de apenas carregar a DLL, a rotina tem que lidar com o mapeamento da DLL no espaço de endereçamento do processo, localizar e resolver os pontos de entrada dessas DLLs, dentre outras tarefas. Como estamos interessados apenas na abertura do PE para leitura, não há necessidade de arcar com a carga adicional imposta pela chamada a LoadLibrary e, portanto, chamar LoadLibraryEx com o parâmetro LOAD_AS_DATAFILE será essencial para evitar aquelas tarefas adicionais, tornando a tarefa mais simples e rápida:
Listagem 1: Função LoadLibraryEx
hModule := LoadLibraryEx(PChar(cModuleName), 0, LOAD_LIBRARY_AS_DATAFILE);
Onde hModule é uma variável do tipo THandle.
Uma vez estando com o PE aberto e carregado na memória, com um handle para refernciá-lo, o próximo passo é examinar todos os recursos até encontrar os recursos personalizados. Existem identificadores definidos no Windows para todos os tipos de recursos como, por exemplo: RT_BITMAP, RT_STRING, RT_DIALOG, etc. Recursos personalizados são identificados de modo global por RT_RCDATA. Para examinar os diferentes tipos de recursos personalizados, a API do Windows oferece a função EnumResourceTypes. Essa é uma função de enumeração cujo funcionamento pode parecer um tanto estranho ao programador Delphi menos experiente com a API do Windows. O protótipo da função é:
Listagem 2: Função EnumResourceTypes
function EnumResourceTypes(hModule: HMODULE; lpEnumFunc: Pointer; lParam: Longint): BOOL; stdcall;
Os parâmetros esperados são o handle que já conseguimos na chamada a LoadLibraryEx e um ponteiro para uma função de callback. O terceiro parâmetro é qualquer valor de 32 bits que podemos optar por utilizar para fins particulares (ou simplesmente igorar). A pergunta que surge é: o que significa uma função de callback? Curto e grosso, é uma função que, ao invés de ser chamada por um código nosso, é chamada pelo Windows quando apropriado. No caso de uma função de callback para uma API de enumeração, a função de enumeração irá chamar a função de callback para cada item encontrado na enumeração. Tenha cuidado pois, sendo chamada pelo SO, o Windows, e não por código nosso, a função de callback deve seguir a convenção de chamada do Windows e não a do Pascal e, por isso, você deve certificar-se que sua função de callback é declarada como stdcall. Outra questão que pode surgir é: qual o significado de fato do segundo parâmetro, lpEnumFunc? Pode ser qualquer ponteiro? Certamente que não. Deve ser um ponteiro para uma função do tipo apropriado:
Listagem 3: Função EnumResTypeProc
function EnumResTypeProc(hModule: THandle; szType: PChar; lParam: LongInt): Boolean; stdcall;
Para cada tipo de recurso encontrado por EnumResourceTypes, o Windows chama EnumResTypeProc passando o handle do módulo, o tipo do recurso encontrado e o parâmetro opcional. EnumResourceTypes continuará com a enumeração enquanto existirem recursos distintos a serem enumerados e enquanto a função de callback continue retornando true; assim, quando encontrarmos o recurso do tipo RT_RCDATA, devemos retornar false para finalizar a enumeração. É importante que o segundo parâmetro mencionado anteriormente seja um ponteiro do tipo correto de função. Se olharmos EnumResourceTypes no SDK do Windows o que encontramos é:
Listagem 4: Função EnumResourceTypes
function EnumResourceTypes(hModule: HMODULE; lpEnumFunc: ENUMRESTYPEFUNC; lParam: Longint): BOOL; stdcall;
Sendo o tipo ENUMRESTYPEFUNC nada mais que um outro nome para o tipo TFarProc que, por sua vez, é apenas um outro nome para Pointer. Como definida, a função espera um ponteiro e, sendo assim, qualquer ponteiro passado será aceito em tempo de compilação; assim sendo, você deve ter bastante cuidado com o tipo de ponteiro passado. Sendo qualquer outro tipo de ponteiro além daquele definido, nenhum erro será reportado em tempo de compilação mas, em tempo de execução, as conseqüências serão qualquer coisa entre terríveis e desastrosas. No Object Pascal, o tipo TEnumResTypeFunc poderia ser definido como:
Listagem 5: Definição do tipo TEnumResTypeFunc
function(hModule: THandle; szType: PChar; lParam: LongInt): Boolean;stdcall;
Assim, qualquer função que definisse um parâmetro do TEnumResTypeProc e recebesse qualquer outro tipo geraria um erro em tempo de compilação, evitando erros, em tempo de execução, bem mais difíceis de depurar e de conseqüências potencialmente mais desastrosas. O problema aqui é que EnumResourceTypes é uma função da API do Windows e, sendo assim, foi escrita em C. A única forma de se passar uma função como parâmetro em C é através do uso de ponteiros para funções; esse mecanismo não permite verificação em tempo de compilação, ao contrário do Pascal que, além dos ponteiros para funções, suporta também tipos procedurais bem mais robustos.
Em nossa primeira aproximação, nossa função de callback seria algo como:
Listagem 6: Função EnumTypes
function EnumTypes(hModule: THandle; szType: PChar; lParam: LongInt): Bool; stdcall; begin if szType = RT_RCDATA then ... Result := False; else Result := True; end (*EnumTypes*);
Como podemos ver, se o tipo de recurso encontrado não for o tipo que procuramos, RT_RCATA, retornamos True para que a enumeração continue; se for o tipo procurado, fazemos o processamento (código indicado por elipse) e retornamos False sinalizando o término da enumeração.
A chamada seria:
Listagem 7: Chamada à função LoadLibraryEx
hModule := LoadLibraryEx(PChar(cModuleName), 0, LOAD_LIBRARY_AS_DATAFILE); if hModule <> 0 then EnumResourceTypes(hModule, @EnumTypes, 0);
Note o uso do operador @ para passar o endereço (ponteiro para) da função de callback EnumTypes.
Nesse ponto, o esqueleto do programa está praticamente concluído: abrir o arquivo e determinar se existem recursos personalizados. Se houver, devemos verificar se um desses recursos tem o nome DVCLAL. Para fazer isso, o Windows oferece uma outra função de enumeração muito parecida com aquela que acabamos de utilizar mas que, dessa feita, enumera os nomes dos recursos de um tipo específico. Eis a sintaxe dessa função:
Listagem 8: Função EnumResourceNames
function EnumResourceNames(hModule: HMODULE; lpType: PChar; lpEnumFunc: Pointer; lParam: Longint): BOOL; stdcall;
E o protótipo da função de callback:
Listagem 9: Função de callback
function EnumResNamesProc(hModule: THandle; szType: PChar; szName: PChar; lParam: LongInt): Bool;stdcall;
O funcionamento é análogo ao já visto para os tipos de recursos. Assim, para cada tipo de recurso passado, a função de callback será chamada com cada um dos nomes de recurso daquele tipo até que todos os nomes de recurso tenham sido enumerados ou até que a função de callback retorne False. Como o funcionamento é muito parecido com aquele já visto para a outra enumeração, dispensaremos os detalhes.
Assim, nosso programa principal seria algo do tipo:
Listagem 9: Programa principal
function EnumNames(hModule: THandle; szType: PChar; szName: PChar; lParam: LongInt): Bool; stdcall; begin if szName = "DVCLAL" then begin Boolean(Pointer(lParam)^) := True; Result := False; exit; end (*if*); Result := True; end (*EnumNames*); function EnumTypes(hModule: THandle; szType: PChar; lParam: LongInt): Bool; stdcall; begin if szType = RT_RCDATA then Result := EnumResourceNames(hModule, szType, @EnumNames, lParam) else Result := True; end (*EnumTypes*); function IsDelphiModule(const cModuleName: String): Boolean; var hModule: THandle; begin Result := False; hModule := LoadLibraryEx(PChar(cModuleName), 0, LOAD_AS_DATAFILE); if GetLastError = 0 then EnumResourceTypes(hModule, @EnumTypes, LongInt(@Result)) else RaiseLastWin32Error; end (*IsDelphiModule*);
Como pode ser visto, o programa consiste de um aninhamento de funções em dois níveis. Para cada PE testado, a primeira coisa é carregá-lo através de uma chamada a LoadLibraryEx para obter um handle. Em seguida, chamamos EnumResourceTypes com o handle que então chama nossa função de callback EnumType com o tipo de recurso encontrado. Se o tipo de recurso for RT_RCDATA, chamamos EnumResourceNames a partir da própria função de callback; se o nome do recurso RT_RCDATA passado a EnumNames for DVCLAL, presumimos que nossa busca está terminada e que o arquivo foi realmente criado pelo Delphi; assim, podemos encerrar a enumeração. Na verdade, é preciso ler os DADOS BRUTOS contidos no recurso e garantir que confere com a assinatura do Delphi e não é meramente uma coincidência. Para isso, é preciso saber como acessar um recurso e conhecer a exata estrutura e significado da assinatura Delphi. Acesso a um recurso específico não será abordado nesse artigo, mas em um artigo futuro; quanto a estrutura do DVCLAL, tenho que admitir que ainda não deduzi por completo.
Aqui, empregamos um truque (eu chamaria de uma técnica inteligente, mas só porque não tenho uma avó para me elogiar). Se você lembrar bem, havia um parâmetro opcional de 32 bits; sabendo que EnumResourceTypes é chamado a partir de uma função que retorna um valor Boolean, o que devemos passar nesse parâmetro é um ponteiro para esse valor, que seria equivalente a passá-lo por referência (em C, todos os parâmetros são passados por valor de modo que, para simular a passagem por referência é preciso utilizar um ponteiro). Como esse parâmetro é "arrastado" ao longo do aninhamento de chamadas, sempre temos à disposição uma referência ao valor que será retornado pela função principal de forma que, quando um recurso do tipo RT_RCDATA com o nome DVCLAL for encontrado, podemos atribuir o valor de retorno da função principal através de sua referência.
Quanto à estrutura binária do DVCLAL, há pouco o que dizer. Determinei empiricamente alguns valores para as distribuições C/S ou Enterprise de algumas versões do Delphi, mas não tive o tempo necessário para decifrar a estrutura, nem encontrei qualquer informação a respeito em lugar algum. Eis os valores que encontrei:
Listagem 10: Estrutura do DVCLAL no Delphi 3.xx Client/Server
DVCLAL RCDATA { "A2 8C DF 98 7B 3C 3A 79 26 71 3F 09 0F 2A 25 17" }
Listagem 11: Estrutura do DVCLAL no Delphi 4.xx, 5.xx & 6.xx Enterprise
DVCLAL RCDATA { "26 3D 4F 38 C2 82 37 B8 F3 24 42 03 17 9B 3A 83" }
Um problema adicional que não mencionei é que pelo menos as últimas versões do Borland C++ Builder (versões 4.xx e 5.xx) parecem ter as mesmas assinaturas que seus análogos Delphi, dificultando assim a identificação do PE como sendo criado pelo C++ Builder ou Delphi. Talvez as informações contidas no recurso RT_RCDATA chamado Package Info ofereça informações adicionais para a resolução precisa mas até o momento da publicação deste artigo eu ainda não tinha conseguido resolver esse problema.
O Que Vem Agora?
Nesse ponto, devemos estar familiarizados com os recursos do Windows e devemos também ter aprendido a examinar qualquer arquivo PE e determinar os recursos ali contidos. Para dizer a verdade, se precisássemos apenas testar a existência de um recurso bem conhecido e acessar seu conteúdo, poderíamos ter omitido as enumerações e atacado diretamente o problema. A API do Windows, através de FindResource e LoadResource, e a VCL, com a classe TResourceStream, facilitam esse trabalho sobremaneira. Contudo, a forma apresentada permite não apenas verificar a existência de uma assinatura Delphi no PE, mas também acessar todos os recursos de um PE. Num artigo futuro, aprenderemos a examinar todos os recursos de um PE e, se forem construídos pelo Delphi, até mesmo acessar os formulários e exibi-los, mesmo sendo recursos personalizados ou padrão.
NOTA: quaisquer sugestões, correções ou críticas construtivas serão bem-vindas através do e-mail: jmr@clubdelphi.com. Todo o código aqui mencionado é original do autor e está disponível como um aplicativo demo no arquivo zip anexo.
Clique aqui para fazer o download do código referente a este artigo.