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-tracepara 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.

Nossa comando final é:
frida-trace -U Signal -m "*[*Message* *]" -M "*[* copyWithZone:]" 
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.

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]" 
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 é basicamenteconst 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 objetoargs[1]é um seletor ou nome do métodoargs[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 comTSMessage - Escriver
args[1]para confirmar que estamos trabalhando realmente com o seletorbody - 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:

Dentro do aplicativo Signal:

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.

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*”

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”

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:

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
NativeFunctionpara a funçãodlopen(para carregar a biblioteca) - criar outro
NativeFunctionpara a funçãodlsym(para buscar o endereço do símbolo) - chamar
dlsymcom o manipulador dedlopene o nome do símbolo (swift_demangle) - criar
NativeFunctiondeswift_demangleusando 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.

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);
}
});


x0=>0x796548x1=>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.

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 = 0x6f6c6c6548ex1 = 0xe500000000000000x0 = 0x6874206f6c6c6548ex1 = 0xeb00000000657265x0 = 0xf000000000000025ex1 = 0x2849db390
Caso tentemos converter esses valores hexadecimais para ASCII, obteremos o seguinte:
0x6f6c6c6548= olleH0xe500000000000000= não imprimível0x6874206f6c6c6548= ht olleH0xeb00000000657265= ere0xf000000000000025= não imprimível0x2849db390= 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
x0ex1 - Mensagens maiores que 15 caracteres => algo no registrador
x0e algo no registradorx1
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.attachno endereço - Vamos pegar a primeira byte dentro de
x0 - Caso a primeira byte dentro de
x0seja0xf0, estamos lidando com uma string maior que 15 bytes, então lêremos a string no endereço da memória dentro do registradorx1 - 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
x1como fizemos parax0 - Anexe as mensagens de
x0ex1juntas 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:

Dentro do aplicativo Telegram:

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.

