Segurança

Frida Parte 2: Signal e Telegram no iOS | 8kSec

O texto explora a análise de objetos de mensagens nos aplicativos Signal e Telegram usando ferramentas como Frida para rastrear métodos e funções relacionadas a mensagens. Inicialmente, o autor utiliza comandos específicos do Frida para identificar classes e métodos relevantes em cada aplicativo, focando no conteúdo das mensagens. Para Signal, é possível encontrar métodos promissores como TSMessage body, enquanto Telegram exige um enfoque diferente devido à utilização de Swift, resultando em nomes de função mangled que precisam ser filtrados.

Compartilhar
Frida Part 2: Signal & Telegram on iOS | 8kSec

Introdução

Neste post do blog, exploraremos os objetos de mensagem em duas aplicações de chat populares: Signal e Telegram. Usaremos abordagens técnicas semelhantes para analisá-las e aprenderemos como inspecionar strings Swift, que diferem dos tipos de objeto típicos.

Fiquem prontos para uma exploração direta e reveladora do mundo dos componentes de aplicativos de chat.

Os passos que vamos seguir são:

  • Rode o comando frida-trace para tentar encontrar métodos/funções interessantes;
  • Apos identificar os potenciais métodos/funções, inspecionaremos seus argumentos;
  • Tentaremos fazer sentido dos dados;
  • Escriviremos um script do Frida para isso;
  • Testaremos para ver se o script funciona corretamente.

Signal

Rastreamento

A primeira coisa que queremos fazer é usar a aplicação e rastrear os métodos/funções chamados usando a ferramenta frida-trace. Primeiro, vamos tentar encontrar todas as classes ObjC que contêm message dentro delas.

Para filtrar nos métodos ObjC podemos usar a bandeira -m.

Apos rodarmos o frida-trace, podemos ver que não temos métodos que correspondem à nossa critério *[message *].

Uma vez que mudamos nosso critério para corresponder a *[*Message* *], podemos ver que começamos a rastrear 19224 funções. Também podemos ver que estamos recebendo muitas dessas chamadas de copyWithZone, então vamos excluí-las. Podemos excluir métodos ObjC usando a bandeira -M.

frida-trace output showing 19224 traced Message methods with copyWithZone calls

Nossa comando final é:

frida-trace -U Signal -m "*[*Message* *]" -M "*[* copyWithZone:]"

frida-trace output with copyWithZone excluded, showing filtered Message method calls

Apos a conversa ser aberta e recebermos a mensagem, podemos ver uma série de chamadas sendo feitas para -[MFMessageInfo copyMessageInfo]. Vamos agora rapidamente analisar esta classe para ver se temos algo interessante lá.

Para examiná-la, criaremos um novo objeto da classe MFMessageInfo e examinaremos seus próprios métodos (não os dos classes pai).

Para criar um novo objeto desta classe, chamaremos:

[[MFMessageInfo alloc] init].

No Frida, isso é equivalente a:

ObjC.classes.MSFMessageInfo.alloc().init().

Apos termos o objeto ObjC, podemos examinar seus próprios métodos usando a propriedade $ownMethods, que é exposta pelo Frida.

Frida showing $ownMethods of MFMessageInfo class listing all instance methods

Apos dar uma olhada nisso, podemos ver que temos vários métodos interessantes mas não o conteúdo da mensagem bruta. Então, vamos excluir este método também. O comando frida-trace agora se parece com isso:

frida-trace -U Signal -m "*[*Message* *]" -M "*[* copyWithZone:]" -M "-[MFMessageInfo copyMessageInfo]"

frida-trace output showing TSMessage body method calls after excluding MFMessageInfo copyMessageInfo

Apos excluir -[MFMessageInfo copyMessageInfo], podemos ver que estamos recebendo algumas chamadas de métodos da classe TSMessage e podemos ver que temos um body dentro dele, o que parece promissor.

Analisando argumentos

Antes de tentar inspecionar o argumento, vamos falar brevemente sobre como os métodos são chamados na verdade em ObjC.

objc_msgSend

Toda chamada de método, por exemplo [SomeClass methodName], é na verdade chamada da função objc_msgSend fornecida pelo runtime do ObjC.

objc_msgSend aceita 3 argumentos:

  • ponteiro para o objeto
  • seletor (SEL) ou nome do método, que é basicamente const char[]
  • argumentos a serem passados ao método

Nossa chamada [SomeClass methodName] parece algo como:

objc_msgSend(SomeClass, methodName)

Caso o método aceite alguns argumentos, por exemplo [SomeClass hello:@”8ksec”], isso ficaria assim:

objc_msgSend(SomeClass, hello, “8ksec”)

Portanto, quando interceptamos métodos do ObjC usando o Frida, estamos na verdade interceptando essas chamadas de objc_msgSend.

Sabendo dessa informação, podemos concluir o seguinte:

  • args[0] é um objeto
  • args[1] é um seletor ou nome do método
  • args[2:] são os argumentos passados ao método

Escrevendo o script do Frida

Depois de descrever rapidamente objc_msgSend, nosso script deve fazer o seguinte:

  • Interceptar em [TSMessage body]
  • Escriver args[0] para confirmar que estamos trabalhando realmente com TSMessage
  • Escriver args[1] para confirmar que estamos trabalhando realmente com o seletor body
  • Analisar seu valor de retorno

Um ponto a ter em mente é que, se soubermos que o ponteiro aponta para um objeto do ObjC, podemos convertê-lo em um objeto do ObjC usando a funcionalidade ObjC.Object dentro do Frida. Dessa forma, podemos acessar seus ivars (variáveis de instância) e chamar métodos.

let messageBody = ObjC.classes.TSMessage[- body].implementation;
Interceptor.attach(messageBody, {
    onEnter(args) {
        // converter para ObjC.Object
        var obj = ObjC.Object(args[0]);
        // obter seu nome
        var objName = obj.$className;
        // como o seletor é const char[], podemos ler usando
        // Memory.readUtf8String
        var sel = Memory.readUtf8String(args[1]);
        console.log(`-[${objName} ${sel}]`);
    },
    onLeave(retval) {
        // converter o valor de retorno do método para ObjC.Object
        var body = ObjC.Object(retval);
        console.log(`body: ${body}`);
    }
});

Executando o script do Frida

Saída do script do Frida:

Saída do script do Frida mostrando interceptação do corpo de TSIncomingMessage com seletor de método e valor de retorno

Dentro do aplicativo Signal:

Signal iOS app mostrando a mensagem de teste recebida sendo interceptada

Uma coisa que você pode notar é que estamos recebendo hits para -[TSIncomingMessage body] em vez de -[TSMessage body]. A razão disso é porque TSIncomingMessage é uma subclasse de TSMessage e podemos confirmar isso usando o Frida.

Frida confirmando que TSIncomingMessage é uma subclasse de TSMessage via cadeia de superclasses

Telegram

Rastreamento

Assim como fizemos com o aplicativo Signal, vamos começar rastreando funções, mas desta vez usaremos -i para rastrear funções em vez de métodos.

A execução do seguinte comando nos dá:

frida-trace -U Telegram -i*message*

frida-trace output mostrando nomes de funções Swift-mangled do Telegram

A partir das funções rastreadas, podemos reconhecer imediatamente que estamos lidando com o Swift, pois os símbolos parecem estar mungidos. E como estamos recebendo muitas funções que contêm message7Postbox ou Postbox7Message, vamos excluí-las do rastreamento usando a opção -x frida-trace. Também excluiremos aquelas que contêm messagesByDay em seus nomes mungidos, pois estamos recebendo muitos deles.

frida-trace -U Telegram -i “message” -x “message7Postbox” -x “Postbox7Message” -x “messagesByDay”

frida-trace output com funções Postbox e messagesByDay excluídas, mostrando um conjunto reduzido de rastreamentos de mensagens do Telegram

Reduzimos o número de funções rastreadas e não estamos recebendo tantos hits quanto antes.

Agora, vamos abrir uma conversa e começar a digitar uma mensagem e imediatamente veremos um monte de funções xpc_ sendo chamadas. Por enquanto, é suficiente saber que o XPC é um método de IPC(Inter Process Communication) que é usado extensivamente no iOS para todas as espécies de coisas. Então, vamos adicionar -x “xpc_*” à nossa anterior comando frida-trace.

Uma vez que digitamos a nossa mensagem e a enviamos, as seguintes chamadas de função são feitas:

frida-trace output mostrando funções Swift-mangled chamadas quando uma mensagem do Telegram é enviada

Isso realmente parece assustador, pois estamos vendo nomes mungidos, mas se olharmos para todas essas funções, podemos ver que a última é útil porque podemos ver que envolve algum formatação de string e como as mensagens são basicamente strings, examinaremos essa função.

O nome completo mungido é: __$s10TextFormat25stringWithAppliedEntities_8entities9baseColor04linkI00H4Font0jK004boldK006italicK00l6ItalicK005fixedK0010blockQuoteK014underlineLinks8external7messageSo18NSAttributedStringCSS_Say12TelegramCore07MessageA6EntityVGSo7UIColorCAWSo6UIFontCA6YS2b7Postbox0Z0CSgtF__.

Mas antes de prosseguirmos com a análise dos argumentos, vamos ver como obter mais sentido deste nome mungido da função.

Desmungificação Swift

A desmungificação é o processo inverso de mungificação onde pegamos um nome de função mungido e o tornamos mais legível.

Tal funcionalidade está disponível para nós dentro do libswiftCore.dylib através de sua função swift_demangle.

A função swift_demangle aceita 5 argumentos:

  • string deformada
  • tamanho da string deformada
  • local onde escrever o nome desmangulado (podemos passar NULL)
  • tamanho do buffer para o nome desmangulado (podemos passar NULL)
  • sinalizadores

Dado que estamos trabalhando com Frida, usaremos sua funcionalidade para desmanglar este nome de função. As etapas que vamos seguir são as seguintes:

  • definir o nome da biblioteca (libswiftCore.dylib)
  • definir sinalizadores
  • criar NativeFunction para a função dlopen (para carregar a biblioteca)
  • criar outro NativeFunction para a função dlsym (para buscar o endereço do símbolo)
  • chamar dlsym com o manipulador de dlopen e o nome do símbolo (swift_demangle)
  • criar NativeFunction de swift_demangle usando o endereço do passo anterior
  • envolver tudo isso em uma função que aceita um nome deformado

O código completo para nosso script é o seguinte:

let swiftCore = Memory.allocUtf8String("/usr/lib/swift/libswiftCore.dylib");
let RTLD_LAZY = 0x00001;
let RTLD_GLOBAL = 0x00100;
// dlopen aceita dois parâmetros, caminho da biblioteca e sinalizadores
// e retorna o manipulador para nós
let dlopen = new NativeFunction(Module.findExportByName(null, "dlopen"), "pointer", ["pointer", "int"]);
// dlsym aceita dois parâmetros, manipulador de dlopen e

// nome do símbolo; retorna para nós um ponteiro para o símbolo
let dlsym = new NativeFunction(Module.findExportByName(null, "dlsym"), "pointer", ["pointer", "pointer"]);
let hndl = dlopen(swiftCore, RTLD_LAZY | RTLD_GLOBAL);
let addr = dlsym(hndl, Memory.allocUtf8String("swift_demangle"));
// criar função nativa usando o endereço de dlsym

let swift_demangle = new NativeFunction(addr, "pointer", ["pointer", "int", "pointer", "pointer", "int"]);
function demangle(mangled) {
    // se o terceiro parâmetro for NULL, swift_demangle irá alocar

    // o buffer e retorná-lo para nós

    var ret = swift_demangle(Memory.allocUtf8String(mangled), mangled.length, ptr("0x0"), ptr("0x0"), 0);
    // ler a string e retorná-la

    return Memory.readUtf8String(ret);
}

Agora vamos carregar nosso script e testá-lo.

Saída do Frida mostrando o nome da função Swift desembrulhada TextFormat.stringWithAppliedEntities com tipos de argumentos

O verdadeiro nome da função é TextFormat.stringWithAppliedEntities e ela aceita vários argumentos, o primeiro sendo Swift.String. Na seção seguinte tentaremos examiná-la para ver se este é nossa mensagem.

Examinando os argumentos

Dada a natureza do Swift, não podemos simplesmente convertê-lo em um objeto ObjC usando a funcionalidade ObjC.Object dentro do Frida.

A primeira coisa que tentaremos é apenas imprimir o argumento, args[0], ou se estamos falando de registradores, isso seria x0 na arquitetura arm64.

Vamos escrever um script simples que interceptará este método e imprimirá os conteúdos desses dois registradores (x0 e x1). No Frida, podemos acessar registradores usando this.context.REGNAME.

var fn  = Module.findExportByName(null, "$s10TextFormat25stringWithAppliedEntities_8entities9baseColor04linkI00H4Font0jK004boldK006italicK00l6ItalicK005fixedK0010blockQuoteK014underlineLinks8external7messageSo18NSAttributedStringCSS_Say12TelegramCore07MessageA6EntityVGSo7UIColorCAWSo6UIFontCA6YS2b7Postbox0Z0CSgtF");
Interceptor.attach(fn, {
    onEnter(args) {
        console.log(this.context.x0);
        console.log(this.context.x1);
    }
});

Saída do interceptador do Frida mostrando valores hexadecimais dos registradores x0 e x1 quando 'Hey' é enviado no Telegram

Aplicativo iOS do Telegram exibindo a mensagem de teste 'Hey' enviada para inspeção dos valores dos registradores

  • x0 => 0x796548
  • x1 => 0xe300000000000000

O registrador x0 parece ter um significado, enquanto o x1 não.

Caso analisemos mais de perto os valores do registrador x0, perceberemos que eles parecem números ASCII e se tentarmos converter esses números hexadecimais para texto, obteremos nossa mensagem Hey.

Conversão Hexadecimal para ASCII mostrando o valor do registrador x0 0x796548 decodificado como 'Hey' em ordem inversa (Little Endian)

Podemos ver que conseguimos obter nossa mensagem, mas invertida, o motivo disso é que estamos trabalhando com Little Endian.

Agora, vamos enviar diferentes mensagens para ver a saída delas. Para testar as entradas, enviaremos três mensagens diferentes:

  • Hello
  • Hello there
  • Hello there, this is a longer message

Ignore a repetição, isso é apenas o modo como a aplicação chama essa função várias vezes. Podemos extrair três mensagens distintas:

  • x0 = 0x6f6c6c6548 e x1 = 0xe500000000000000
  • x0 = 0x6874206f6c6c6548 e x1 = 0xeb00000000657265
  • x0 = 0xf000000000000025 e x1 = 0x2849db390

Caso tentemos converter esses valores hexadecimais para ASCII, obteremos o seguinte:

  • 0x6f6c6c6548 = olleH
  • 0xe500000000000000 = não imprimível
  • 0x6874206f6c6c6548 = ht olleH
  • 0xeb00000000657265 = ere
  • 0xf000000000000025 = não imprimível
  • 0x2849db390 = não imprimível

Podemos concluir o seguinte:

  • Mensagem com menos de 9 caracteres => dentro do registrador x0
  • Mensagem com menos de 8 caracteres => dentro dos registradores x0 e x1
  • Mensagens maiores que 15 caracteres => algo no registrador x0 e algo no registrador x1

Para realmente entender o que está acontecendo, precisamos compreender como as strings Swift funcionam.

Swift.String

Quando as strings Swift são passadas para a função, elas podem ser passadas de duas maneiras:

  • na pilha se o tamanho for menor que 16 bytes
  • no heap se o tamanho for maior que 16 bytes

Caso a string seja menor que 16 bytes, ela será colocada nos dois primeiros registradores, no arm64, esses registradores são x0 e x1.

Se a string é maior que 16 bytes, dentro de x0, teremos informações sobre o tamanho da string na LSB (Least Significant Byte) e x1 conterá o ponteiro para a estrutura Swift.String. A estrutura Swift.String consiste em um cabeçalho de 32 bytes, seguido pela string crua.

Agora que sabemos como analisar as strings, vamos escrever nosso script Frida para a função desejada.

Escrevendo o script Frida

Agora vem a parte real: criar um script Frida para interceptar essa função e imprimir seu primeiro argumento (Swift.String).

Etapas que precisamos seguir:

  • Encontrar o endereço da função
  • Criar uma função que reverterá o array e parará ao encontrar 0x00
  • Rodar Interceptor.attach no endereço
  • Vamos pegar a primeira byte dentro de x0
  • Caso a primeira byte dentro de x0 seja 0xf0, estamos lidando com uma string maior que 15 bytes, então lêremos a string no endereço da memória dentro do registrador x1
  • Caso contrário, leia os bytes de x0 e armazene-os em um array
  • Reverta esse array (porque estamos trabalhando com Little Endian) e converta os bytes em caracteres e retorne a string
  • Caso não tenhamos encontrado 0x00 na primeira byte, siga as etapas para x1 como fizemos para x0
  • Anexe as mensagens de x0 e x1 juntas e imprima a mensagem
var fn = Module.findExportByName(null, "$s10TextFormat25stringWithAppliedEntities_8entities9baseColor04linkI00H4Font0jK004boldK006italicK00l6ItalicK005fixedK0010blockQuoteK014underlineLinks8external7messageSo18NSAttributedStringCSS_Say12TelegramCore07MessageA6EntityVGSo7UIColorCAWSo6UIFontCA6YS2b7Postbox0Z0CSgtF");
function messageFromArray(arr) {
    var reversed = arr.reverse();
    var m = '';
    for (var i = 0; i < reversed.length; i++) {
        if (reversed[i] == 0) {
            break;
        }
        m += String.fromCharCode(reversed[i]);
    }
    return m;
}
Interceptor.attach(fn, {
    onEnter(args) {
        var firstByte = this.context.x0.toString().slice(0,4);
        var message = '';
        if (firstByte == 0xf0) {
            // add 32 because of the header
            var loc = this.context.x1.add(32);
            message = Memory.readUtf8String(loc);
        } else {
            // small string, less than 16 bytes
            var firstArg = this.context.x0.toString().slice(2);
            var firstChars = [];
            // read bytes from x0 and convert them to int
            for (var i = 0; i < firstArg.length; i += 2) {
                var ch = parseInt(firstArg.slice(i, i+2), 16);
                firstChars.push(ch);
            }
            // convert those bytes to string
            var firstMessage = messageFromArray(firstChars);
            // we start reading from the second byte because
            // maximum number of characters is 7 in x1 for Swift.String
            var secondArg = this.context.x1.toString().slice(4);
            var secondChars = [];
            // read bytes from x1 and convert them to int
            for (var i = 0; i < secondArg.length; i += 2){
                var ch = parseInt(secondArg.slice(i, i+2), 16);
                secondChars.push(ch);
            }
            // append the strings from both x0 and x1
            var secondMessage = messageFromArray(secondChars);
            message = firstMessage + secondMessage;
        }
        console.log(`message: ${message}`);
    }
});

Executando o Script Frida

Saída do script Frida:

Saída do script Frida mostrando mensagens Telegram em texto plano extraídas dos registradores Swift string

Dentro do aplicativo Telegram:

Aplicativo Telegram iOS mostrando as três mensagens de teste (Olá, Olá aí, Olá aí esta é uma mensagem mais longa)

Você pode notar que estamos vendo algumas mensagens várias vezes, isso provavelmente é devido ao aplicativo chamando essa função toda vez que uma nova mensagem chega para formatar o texto.

Fonte original

Conteúdo traduzido e adaptado pela redação do Notícias Mobile. Confira também a matéria na fonte original.

Leia a matéria completa