Layered obfuscation: a taxonomy of software obfuscation techniques for layered security

This section surveys the obfuscation techniques for specific code elements. Esta camada cobre a maior parte das publicações na área de ofuscação de software. Como mostrado na Fig. 4, de acordo com os elementos que uma técnica de ofuscação Visa, dividimos esta categoria em cinco subcategorias: layouts ofuscantes, controles ofuscantes, funções ofuscantes de dados e classes ofuscantes.

Fig. 4
figura4

As técnicas de ofuscação de código-elemento da camada de

Ofuscando layout

Layout de ofuscamento embaralha o esquema de códigos ou instruções, mantendo a sintaxe original intacto. Esta seção discute quatro estratégias de ofuscação de layout: classificadores sem sentido, símbolos redundantes de stripping, separando códigos relacionados, e códigos junk.

identificadores sem significado

esta abordagem é também conhecida como ofuscação lexical que transforma identificadores significativos em identificadores sem significado. Para a maioria das linguagens de programação, a adoção de regras de nomenclatura significativas e uniformes (por exemplo, notação húngara (Simonyi 1999)) é necessária como uma boa prática de programação. Embora tais nomes sejam especificados em códigos fonte, alguns permaneceriam no software lançado por padrão. Por exemplo, os nomes de variáveis globais e funções em C/C++ são mantidos em binários, e todos os nomes de Java são reservados em bytecodes. Como esses nomes significativos podem facilitar a análise de programas contraditórios, devemos baralhá-los. Para tornar os identificadores ofuscados mais confusos, Chan e Yang (2004) propuseram deliberadamente empregar os mesmos nomes para objetos de diferentes tipos ou dentro de diferentes domínios. Tais abordagens foram adotadas pela ProGuard (2016) como um esquema de ofuscação padrão para programas Java.

a remoção de símbolos redundantes

esta estratégia retira informação simbólica redundante do software lançado, como a informação de depuração para a maioria dos propgramas (baixo 1998). Além disso, existem outros símbolos redundantes para formatos particulares de programas. Por exemplo, os arquivos ELF contêm tabelas de símbolos que registram os pares de identificadores e endereços. Ao adotar opções de compilação padrão para compilar programas C / C++, como o uso do LLVM (Lattner e Adve 2004), os binários gerados contêm tais tabelas de símbolos. Para remover essas informações redundantes, os desenvolvedores podem usar a ferramenta strip do Linux. Outro exemplo com informações redundantes é o Android smali codes. Por padrão, os códigos smali gerados contêm informações iniciadas com .linha e … fonte, que pode ser removida para efeitos de ofuscação (Dalla Preda e Maggi 2017).

separar códigos relacionados

um programa é mais fácil de ler se os seus códigos logicamente relacionados também são fisicamente próximos (Collberg et al. 1997). Assim, a separação de códigos ou instruções relacionados pode aumentar as dificuldades de leitura. É aplicável tanto aos códigos-fonte (por exemplo, variáveis de reordenamento (baixo 1998)) como aos códigos de montagem (por exemplo, Instruções de reordenamento (Wroblewski 2002)). Na prática, empregar saltos incondicionais para reescrever um programa é uma abordagem popular para alcançar isso. Por exemplo, os desenvolvedores podem baralhar os códigos de montagem e, em seguida, empregar goto para reconstruir o fluxo de controle original (você e Yim 2010). Esta abordagem é popular para códigos de montagem e bytecodes Java com a disponibilidade de instruções goto (Dalla Preda e Maggi 2017).

códigos Junk

esta estratégia acrescenta instruções junk que não são funcionais. Para binários, podemos adicionar instruções de não Operação (Nop ou 0x00) (Dalla Preda e Maggi 2017; Marcelli et al. 2018). Além disso, também podemos adicionar métodos junk, como a adição de métodos extintos em códigos smali Android (Dalla Preda e Maggi 2017). Os códigos junk podem tipicamente alterar as assinaturas dos códigos, e, portanto, escapar do reconhecimento padrão estático.

Because layout ofuscation does not adultered with the original code syntax, it is less prone to compatibility issues or bugs. Portanto, tais técnicas são as mais favoritas na prática. Além disso, as técnicas de identificadores sem sentido e símbolos redundantes podem reduzir o tamanho dos programas, o que os torna ainda mais atraentes (ProGuard 2016). No entanto, a potência do layout ofuscation é limitada. Ele promete resiliência a ataques de desobfuscação porque algumas transformações são unidirecionais, que não podem ser revertidas. No entanto, algumas informações de layout dificilmente podem ser alteradas, como os identificadores de métodos do SDK Java. Essa informação residual é essencial para os adversários para recuperar a informação ofuscada. Por exemplo, Bichsel et al. (2016) tentou desobfuscar aplicativos progressivos, e eles recuperaram com sucesso cerca de 80% dos nomes.

controlos ofuscantes

este tipo de técnicas de ofuscação transforma os controlos dos códigos para aumentar a complexidade do programa. Ele pode ser alcançado através de fluxos de controle fictícios, fluxos de controle probabilístico, controles baseados em despachantes e controles implícitos.

fluxos de controlo falsos

fluxos de controlo falsos referem-se aos fluxos de controlo que são deliberadamente adicionados a um programa mas nunca serão executados. Ele pode aumentar a complexidade de um programa, e.g., in McCabe complexity (McCabe 1976) or Harrison metrics (Harrison and Magel 1981). Por exemplo, a complexidade de McCabe (McCabe 1976) é calculada como o número de arestas em um grafo de fluxo de controle menos o número de nós, e então mais duas vezes dos componentes conectados. Para aumentar a complexidade de McCabe, podemos introduzir novas arestas ou adicionar novas arestas e nós a um componente conectado.

para garantir a inacessibilidade dos fluxos de controlo falsos, Collberg et al. (1997) sugeriu o emprego de predicados opacos. Eles definiram opaco predizer como o predicado cujo resultado é conhecido durante o tempo de ofuscação, mas é difícil de deduzir pela análise estática do programa. Em geral, um predicado OPACO pode ser constantemente verdadeiro (PT), constantemente falso (PF), ou dependente do contexto (p?). Existem três métodos para criar predicados opacos: esquemas numéricos, esquemas de programação e esquemas contextuais.

Numerical Schemes

Numerical schemes compose opaca predicates with mathematical expressions. Por exemplo, 7×2−1≠y2 é constantemente verdadeiro para todos os inteiros x e Y. Podemos empregar directamente tais predicados opacos para introduzir fluxos de controlo falsos. A figura 5a demonstra um exemplo, no qual o predicado opaco garante que o fluxo de controle falso (i.e., o outro ramo) não será executado. No entanto, os atacantes teriam maiores chances de detectá-los se empregarmos os mesmos predicados opacos frequentemente em um programa ofuscado. Arboit (2002), therefore, proposed to generate a family of such opaque predicates automatically, such that an obfuscator can choose a unique opaque predicate each time.

Fig. 5
a figura5

fluxo de Controle de ofuscamento opaco predicados

Outra abordagem matemática com maior segurança é a utilização de criptografia funções, como a função de hash \(\mathcal {H}\) (Sharif et al. 2008), and homomorphic encryption (Zhu and Thomborson 2005). Por exemplo, podemos substituir um predicado x==c com \(\mathcal {H}(x)=c_{hash}\) para esconder a solução de x para esta equação. Note que tal abordagem é geralmente empregada por malware para evitar a análise dinâmica do programa. Também podemos empregar funções criptográficas para criptografar equações que não podem ser satisfeitas. No entanto, tais predicados opacos incorrem em muita sobrecarga.

para compor constantes opacas resistentes à análise estática, Moser et al. (2007) sugeriu o emprego de problemas de 3 SAT, que são NP-hard. Isto é possível porque se pode ter algoritmos eficientes para compor tais problemas difíceis (Selman et al. 1996). Por exemplo, Tiella e Ceccato (2017) demonstraram como compor tais predicados opacos com problemas k-clique.

para compor constantes opacas resistentes à análise dinâmica, Wang et al. (2011) proposed to compose opaque predicates with a form of unsolved conjectures which loop for many times. Uma vez que os laços são um desafio para a análise dinâmica, a abordagem na natureza deve ser resistente à análise dinâmica. Exemplos de tais conjecturas incluem conjecturas de Collatz, conjectura 5x+1, conjectura de Matthews. A figura 5b demonstra como empregar conjecturas de Collatz para introduzir fluxos de controle falsos. Não importa como inicializamos x, o programa termina com x = 1, e os códigos originais() podem sempre ser executados.Como a análise de programas contraditórios é uma grande ameaça aos predicados opacos, podemos empregar problemas desafiadores de análise de programas para compor predicados opacos. Collberg et al. suggested two classic problems, pointer analysis and simultaneous programs.

em geral, a análise do ponteiro refere-se a determinar se dois ponteiros podem ou podem apontar para o mesmo endereço. Alguns problemas de análise de ponteiros podem ser NP-hard para análise estática ou mesmo indecidível (Landi e Ryder 1991). Outra vantagem é que as operações de ponteiros são muito eficientes durante a execução. Portanto, os desenvolvedores podem compor previsões opacas resilientes e eficientes com problemas de análise de ponteiros bem projetados, como a manutenção de ponteiros para alguns objetos com estruturas de dados dinâmicos (Collberg et al. 1998a).

programas simultâneos ou programas paralelos é outra questão desafiadora. Em geral, uma região paralela de declarações n tem n! diferentes formas de execução. A execução não é determinada apenas pelo programa, mas também pelo Estado de execução de um computador host. Collberg et al. (1998a) propôs empregar programas concorrentes para melhorar a abordagem baseada em ponteiros, atualizando simultaneamente os ponteiros. Majumdar and Thomborson (2006) proposed to employ distributed parallel programs to compose opaque predicates.

além disso, algumas abordagens compõem predicados opacos com truques de programação, tais como alavancar mecanismos de manuseio de exceção. Por exemplo, Dolz e Parra (2008) propuseram usar o mecanismo try-catch para compor predicados opacos para.NET e Java. Os eventos de exceção incluem divisão por zero, ponteiro nulo, índice fora de alcance, ou mesmo exceções particulares de hardware (Chen et al. 2009). A semântica original do programa pode ser alcançada através de esquemas de tratamento de exceções sob medida. No entanto, esses predicados opacos não têm base de segurança,e eles são vulneráveis a ataques feitos à mão.

esquemas contextuais

esquemas contextuais podem ser empregados para compor predicados opacos variantes (isto é, {P?}). Os predicados devem conter algumas propriedades determinísticas de modo que possam ser empregados para ofuscar programas. Por exemplo, Drape e et al. (2009) proposed to compose such opaque predicates which are invariant under a contextual constraint, e.g., the opaque predicate x mod3==1 is constantly true if x mod3:1?x++: x = x+3. Palsberg et al. (2000) proposed dynamic opaque predicates, which include a sequence of correlated predicates. O resultado de avaliação de cada predicado pode variar em cada execução. Entretanto, enquanto os predicados estiverem correlacionados, o comportamento do programa é determinístico. A figura 5c demonstra um exemplo de predicados dinâmicos opacos. Não importa como inicializamos * p E * q,o programa é equivalente a y=x+3, x=y+3.

a resistência dos fluxos de controlo falsos depende principalmente da segurança dos predicados opacos. Uma propriedade de segurança ideal para predicados opacos é que eles requerem o pior caso de tempo exponencial para quebrar, mas apenas tempo polinomial para construir. Note que alguns predicados opacos são projetados com tais preocupações de segurança, mas podem ser implementados com falhas. Por exemplo, os problemas 3-SAT propostos por Ogiso et al. (2003) are based on trivial problem settings which can be easily simplified. Se esses predicados opacos forem implementados corretamente, eles prometerão ser resilientes.

fluxos de controle probabilístico

fluxos de controle fictícios podem causar problemas à análise de programas estáticos. No entanto, eles são vulneráveis à análise dinâmica do programa porque os fluxos de controle falsos são inativos. A ideia de fluxos de controle probabilístico adota uma estratégia diferente para enfrentar a ameaça (Pawlowski et al. 2016). Introduz replicações de fluxos de controle com a mesma semântica, mas sintaxe diferente. Ao receber a mesma entrada várias vezes, o programa pode se comportar de forma diferente para diferentes tempos de execução. A técnica também é útil para combater ataques de canal lateral (Crane et al. 2015).

Note that the strategy of probabilistic control flows is similar to bogus control flows with contextual opaque predicates. Mas eles são diferentes na natureza como predicados contextuais opacos introduzem caminhos mortos, embora eles não introduzam códigos de lixo.

controlos baseados em expedidores

um controlo baseado em expedidores determina os próximos blocos de códigos a serem executados durante o período de execução. Tais controles são essenciais para a ofuscação de fluxo de controle porque eles podem esconder os fluxos de controle originais contra a análise de programa estático.

uma abordagem de ofuscação baseada em despachantes é o aplanamento de fluxo de controle, que transforma códigos de profundidade em códigos rasos com mais complexidade. Wang et al. (2000) em primeiro lugar, propôs a abordagem. A figura 6 demonstra um exemplo de seu papel que transforma um laço while em outra forma com switch-case. Para realizar tal transformação, o primeiro passo é transformar o código em uma representação equivalente com declarações if-then-goto como mostrado na Fig. 6; Então eles modificam as declarações goto com declarações de switch-case como mostrado na Fig. 6. Desta forma, a semântica do programa original é realizada implicitamente controlando o fluxo de dados da variável switch. Como a ordem de execução dos blocos de código é determinada pela variável dinamicamente, não se pode conhecer os fluxos de controle sem executar o programa. Cappaert e Preneel (2010) formalizaram achatamento controle-fluxo como empregando um nó de despacho (por exemplo, switch) que controla o próximo bloco de código a ser executado; depois de executar um bloco, o controle é transferido de volta para o nó de despacho. Além disso, há várias melhorias no nivelamento de fluxo de código. Por exemplo, para melhorar a resistência à análise de programa estático na variável switch, Wang et al. (2001) propôs a introdução de problemas de análise de ponteiros. Para complicar ainda mais o programa, Chow et al. (2001) propôs a adição de blocos de código falsos.

Fig. 6
Figura 6

Control-flow flatening approach proposed by Wang et al. (2000)

László e Kiss (2009) propuseram um mecanismo de nivelamento de fluxo de controle para lidar com sintaxe C++ específica, como try-catch, while-do, continue. O mecanismo é baseado em árvore de sintaxe abstrata e emprega um padrão fixo de layout. Para cada bloco de código para ofuscar, ele constrói uma instrução while no laço exterior e um composto switch-case dentro do laço. O composto switch-case implementa a semântica original do programa, e a variável switch também é empregada para terminar o loop externo. Cappaert e Preneel (2010) descobriram que os mecanismos podem ser vulneráveis à análise local, ou seja, a variável switch é imediatamente atribuída de tal forma que os adversários podem inferir o próximo bloco a ser executado apenas olhando para um bloco atual. Eles propuseram um reforço da abordagem com vários truques, como empregar de referência de atribuição (por exemplo, swVar=swVar+1) em vez de atribuição direta (por exemplo, swVar=3), substituindo a atribuição através de if-else, com um uniforme de atribuição de expressão, e empregando uma maneira de funções no cálculo do sucessor de um bloco básico.

além de aplanamento de fluxo de controle, existem várias outras investigações de ofuscação baseadas em despachantes (por exemplo, Linn e Debray 2003; Ge et al. 2005; Zhang et al. 2010; Schrittwieser e Katzenbeisser 2011). Linn and Debray (2003) proposed to ofuscate binários with branch functions that guide the execution based on the stack information. Similarmente, Zhang et al. (2010) proposed to employ branch functions to ofuscate object-oriented programs, which definite a unified method invocation style with an object pool. Para reforçar a segurança desses mecanismos, Ge et al. (2005) proposed to hide the control information in another standalone process and employ inter-process communications. Schrittwieser e Katzenbeisser (2011) propuseram empregar blocos de código diversificados que implementem a mesma semântica.

a ofuscação baseada no Dispatcher é resistente contra a análise estática porque esconde o grafo de controle-fluxo de um programa de software. No entanto, é vulnerável a análise dinâmica do programa ou abordagens híbridas. Por exemplo, Udupa et al. (2005) proposed a hybrid approach to reveal the hidden control flows with both static analysis and dynamic analysis.

controlos implícitos

esta estratégia converte instruções de controlo explícitas em instruções implícitas. Ele pode impedir os engenheiros reversos de abordar os fluxos de controle corretos. Por exemplo, podemos substituir as instruções de controle dos códigos de montagem (por exemplo, jmp e jne) por uma combinação de mov e outras instruções que implementam a mesma semântica de controle (Balachandran e Emmanuel 2011).

Note that all existing control-flow ofuscation approaches focus on syntactic-level transformation, while the semantic-level protection has rarely been discussed. Embora possam demonstrar alguma resiliência aos ataques, a sua eficácia ofuscante em relação à protecção semântica permanece incerta.

dados ofuscantes

técnicas de ofuscação de dados atuais focam-se em tipos de dados comuns, tais como inteiros, cadeias e arrays. Podemos transformar dados através de divisão, fusão, procedurização, codificação, etc.

divisão/fusão de dados

divisão de dados distribui a informação de uma variável em várias variáveis novas. Por exemplo, uma variável booleana pode ser dividida em duas variáveis booleanas, e realizar operações lógicas sobre elas pode obter o valor original.

a junção de dados, por outro lado, agrega várias variáveis numa única variável. Collberg et al. (1998b) demonstrated an example that merges two 32-bit integers into one 64-bit integer. Ertaul and Venkatesh (2005) proposed another method that packs several variables into one space with discrete logarithms.

Data proceduriation

Data proceduriation substitutes data static data with procedure calls. Collberg et al. (1998b) proposed to substitute strings with a function which can produce all strings by specifying patuscular parameter values. Drape e et al. (2004) proposed to encode numerical data with two inverse functions f and G. Para atribuir um valor v a uma variável i, atribuímo-la a uma variável injectada j como j=f(v). Para usar i, invocamos g (j) em vez disso.

codificação de dados

codificação de dados codifica dados com funções matemáticas ou Cifras. Ertaul and Venkatesh (2005) proposed to encode strings with Affine cifers (e.g., Caser cipher cipher) and employ discrete logarithms to pack words. Fukushima et al. (2008) proposed to encode the clear numbers with exclusive or operations and then decrypt the computation result before output. Kovacheva (2013) propôs criptografar strings com a cifra RC4 e, em seguida, descriptografá-los durante a execução.

Array transformation

Array is one most commonly employed data structure. Para ofuscar arrays, Collberg et al. (1998b) discutiram várias transformações, tais como a divisão de uma matriz em várias subarrays, a fusão de vários arrays em um array, dobramento de uma matriz para aumentar a sua dimensão, ou aplanar, de uma matriz para reduzir a dimensão. Ertaul and Venkatesh (2005) suggested transforming the array indices with composite functions. Zhu et al. (2006); Zhu (2007) propôs empregar criptografia homomórfica para a transformação de array, incluindo mudança de índice, dobragem e lisonjeamento. Por exemplo, podemos misturar os elementos de uma matriz com i∗m mod n, onde i é o índice original, n é o tamanho da matriz original, e m e n são relativamente primos.

Ofuscando métodos

Método inline/estrutura de tópicos

Um método é um procedimento independente, que pode ser chamado por outras instruções do programa. O método inline substitui a chamada processual original com o próprio corpo da função. O esquema do método opera da maneira oposta que extrai uma sequência de instruções e abstrai um método. São boas empresas que podem ofuscar a abstracção original dos procedimentos (Collberg et al. 1997).

clone do método

se um método é fortemente invocado, podemos criar replicações do método e chamar aleatoriamente um deles. Para confundir a interpretação adversária, cada versão da replicação deve ser única de alguma forma, como adotando diferentes transformações de ofuscação (Collberg et al. 1997) ou diferentes assinaturas (Ertaul e Venkatesh 2004).

método de agregação / dispersão

a ideia é semelhante à ofuscação de dados. Podemos agregar métodos irrelevantes em um método ou dispersar um método em vários métodos (Collberg et al. 1997; baixo 1998).

método proxy

esta abordagem cria métodos proxy para confundir engenharia reversa. Por exemplo, podemos criar os proxies como métodos estáticos públicos com identificadores aleatórios. Pode haver vários proxies distintos para o mesmo método (Dalla Preda e Maggi 2017). A abordagem é extremamente útil quando as assinaturas do método não podem ser alteradas (Protsenko e Muller 2013).Classes ofuscantes

classes ofuscantes partilham algumas ideias semelhantes com métodos ofuscantes, tais como a divisão e o clone (Collberg et al. 1998b). No entanto, uma vez que a classe só existe em linguagens de programação orientadas a objetos, como JAVA e.NET, nós as discutimos como uma categoria única. Abaixo apresentamos as principais estratégias para as classes ofuscantes.

largando modificadores

programas orientados a objetos contêm modificadores (e.g., público, privado) para restringir o acesso a classes e membros de classes. Eliminar modificadores remove essas restrições e torna todos os membros públicos (Protsenko e Muller 2013). Esta abordagem pode facilitar a implementação de outros métodos de ofuscação de classe.

class Splitting/Coalescing

The idea of coalescing / splitting is to ofuscate the intent of developers when design the classes (Sosonkin et al. 2003). Quando se coalesce classes, podemos transferir variáveis locais ou grupos de instrução locais para outra classe (Fukushima et al. 2003).

classe hierarchy achatening

Interface is a powerful tool for object-oriented programs. Similar ao método proxy, podemos criar proxies para classes COM interfaces (Sosonkin et al. 2003). No entanto, uma maneira mais potente é quebrar a relação de herança original entre classes com interfaces. Ao deixar cada nó de uma sub-árvore na hierarquia de classes implementando a mesma interface, podemos achatar a hierarquia (Foket et al. 2012).

Deixe uma resposta

O seu endereço de email não será publicado.