MASTG-TEST-0070: Testando Universal Links
Visão Geral¶
Análise Estática¶
O teste de universal links em uma abordagem estática inclui fazer o seguinte:
- Verificar o direito de Associated Domains
- Recuperar o arquivo Apple App Site Association
- Verificar o método receptor do link
- Verificar o método manipulador de dados
- Verificar se o app está chamando universal links de outros apps
Verificando o Direito de Associated Domains¶
Universal links exigem que o desenvolvedor adicione o direito de Associated Domains e inclua nele uma lista dos domínios que o app suporta.
No Xcode, vá para a aba Capabilities e procure por Associated Domains. Você também pode inspecionar o arquivo .entitlements procurando por com.apple.developer.associated-domains. Cada um dos domínios deve ser prefixado com applinks:, como applinks:www.mywebsite.com.
Aqui está um exemplo do arquivo .entitlements do Telegram:
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:telegram.me</string>
<string>applinks:t.me</string>
</array>
Informações mais detalhadas podem ser encontradas na Documentação de Desenvolvedor da Apple arquivada.
Se você não tem o código-fonte original, pode extraí-los do arquivo MachO conforme explicado em Extraindo Entitlements de Binários MachO.
Recuperando o Arquivo Apple App Site Association¶
Tente recuperar o arquivo apple-app-site-association do servidor usando os domínios associados obtidos na etapa anterior. Este arquivo precisa estar acessível via HTTPS, sem redirecionamentos, em https://<domain>/apple-app-site-association ou https://<domain>/.well-known/apple-app-site-association.
Você pode recuperá-lo usando seu navegador e navegando para https://<domain>/apple-app-site-association, https://<domain>/.well-known/apple-app-site-association ou usando a CDN da Apple em https://app-site-association.cdn-apple.com/a/v1/<domain>.
Alternativamente, você pode usar o Validador Apple App Site Association (AASA). Após inserir o domínio, ele exibirá o arquivo, verificará para você e mostrará os resultados (por exemplo, se não está sendo servido adequadamente via HTTPS). Veja o seguinte exemplo de apple.com https://www.apple.com/.well-known/apple-app-site-association:

{
"activitycontinuation": {
"apps": [
"W74U47NE8E.com.apple.store.Jolly"
]
},
"applinks": {
"apps": [],
"details": [
{
"appID": "W74U47NE8E.com.apple.store.Jolly",
"paths": [
"NOT /shop/buy-iphone/*",
"NOT /us/shop/buy-iphone/*",
"/xc/*",
"/shop/buy-*",
"/shop/product/*",
"/shop/bag/shared_bag/*",
"/shop/order/list",
"/today",
"/shop/watch/watch-accessories",
"/shop/watch/watch-accessories/*",
"/shop/watch/bands",
] } ] }
}
A chave "details" dentro de "applinks" contém uma representação JSON de um array que pode conter um ou mais apps. O "appID" deve corresponder à chave "application-identifier" dos direitos do app. Em seguida, usando a chave "paths", os desenvolvedores podem especificar certos caminhos a serem tratados por app. Alguns apps, como o Telegram, usam um * independente ("paths": ["*"]) para permitir todos os caminhos possíveis. Somente se áreas específicas do site não devem ser tratadas por algum app, o desenvolvedor pode restringir o acesso excluindo-os prefixando um "NOT " (note o espaço após o T) ao caminho correspondente. Lembre-se também que o sistema procurará correspondências seguindo a ordem dos dicionários no array (a primeira correspondência vence).
Este mecanismo de exclusão de caminhos não deve ser visto como um recurso de segurança, mas sim como um filtro que o desenvolvedor pode usar para especificar quais apps abrem quais links. Por padrão, o iOS não abre nenhum link não verificado.
Lembre-se que a verificação de universal links ocorre no momento da instalação. O iOS recupera o arquivo AASA para os domínios declarados (applinks) em seu direito com.apple.developer.associated-domains. O iOS se recusará a abrir esses links se a verificação não for bem-sucedida. Alguns motivos para falha na verificação podem incluir:
- O arquivo AASA não é servido via HTTPS.
- O AASA não está disponível.
- Os
appIDs não correspondem (este seria o caso de um app malicioso). O iOS impediria com sucesso qualquer possível ataque de sequestro.
Verificando o Método Receptor do Link¶
Para receber links e tratá-los adequadamente, o app delegate deve implementar application:continueUserActivity:restorationHandler:. Se você tem o projeto original, tente pesquisar por este método.
Observe que se o app usar openURL:options:completionHandler: para abrir um universal link para o site do app, o link não será aberto no app. Como a chamada se origina do app, ela não será tratada como um universal link.
Da documentação da Apple: Quando o iOS inicia seu app após o usuário tocar em um universal link, você recebe um objeto
NSUserActivitycom um valoractivityTypedeNSUserActivityTypeBrowsingWeb. A propriedadewebpageURLdo objeto de atividade contém o URL que o usuário está acessando. A propriedade webpage URL sempre contém um URL HTTP ou HTTPS, e você pode usar as APIsNSURLComponentspara manipular os componentes do URL. [...] Para proteger a privacidade e a segurança dos usuários, você não deve usar HTTP quando precisar transportar dados; em vez disso, use um protocolo de transporte seguro como HTTPS.
Da nota acima podemos destacar que:
- O objeto
NSUserActivitymencionado vem do parâmetrocontinueUserActivity, como visto no método acima. - O esquema do
webpageURLdeve ser HTTP ou HTTPS (qualquer outro esquema deve lançar uma exceção). A propriedade de instânciaschemedeURLComponents/NSURLComponentspode ser usada para verificar isso.
Se você não tem o código-fonte original, pode usar radare2 para iOS ou rabin2 para pesquisar as strings binárias pelo método receptor do link:
$ rabin2 -zq Telegram\ X.app/Telegram\ X | grep restorationHan
0x1000deea9 53 52 application:continueUserActivity:restorationHandler:
Verificando o Método Manipulador de Dados¶
Você deve verificar como os dados recebidos são validados. A Apple adverte explicitamente sobre isso:
Universal links oferecem um vetor de ataque potencial em seu app, portanto, certifique-se de validar todos os parâmetros de URL e descartar quaisquer URLs malformados. Além disso, limite as ações disponíveis àquelas que não arriscam os dados do usuário. Por exemplo, não permita que universal links excluam conteúdo diretamente ou acessem informações sensíveis sobre o usuário. Ao testar seu código de manipulação de URL, certifique-se de que seus casos de teste incluam URLs formatados incorretamente.
Conforme declarado na Documentação do Desenvolvedor da Apple, quando o iOS abre um app como resultado de um universal link, o app recebe um objeto NSUserActivity com um valor activityType de NSUserActivityTypeBrowsingWeb. A propriedade webpageURL do objeto de atividade contém o URL HTTP ou HTTPS que o usuário acessa. O seguinte exemplo em Swift verifica exatamente isso antes de abrir o URL:
func application(_ application: UIApplication, continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
// ...
if userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL {
application.open(url, options: [:], completionHandler: nil)
}
return true
}
Além disso, lembre-se de que se o URL incluir parâmetros, eles não devem ser confiáveis antes de serem cuidadosamente sanitizados e validados (mesmo quando vindos de um domínio confiável). Por exemplo, eles podem ter sido falsificados por um invasor ou podem incluir dados malformados. Se for esse o caso, todo o URL e, portanto, a solicitação de universal link deve ser descartada.
A API NSURLComponents pode ser usada para analisar e manipular os componentes do URL. Isso também pode fazer parte do método application:continueUserActivity:restorationHandler: em si ou pode ocorrer em um método separado sendo chamado a partir dele. O seguinte exemplo demonstra isso:
func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let incomingURL = userActivity.webpageURL,
let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true),
let path = components.path,
let params = components.queryItems else {
return false
}
if let albumName = params.first(where: { $0.name == "albumname" })?.value,
let photoIndex = params.first(where: { $0.name == "index" })?.value {
// Interact with album name and photo index
return true
} else {
// Handle when album and/or album name or photo index missing
return false
}
}
Finalmente, conforme declarado acima, certifique-se de verificar se as ações acionadas pelo URL não expõem informações sensíveis ou arriscam os dados do usuário de qualquer forma.
Verificando se o App Está Chamando Universal Links de Outros Apps¶
Um app pode estar chamando outros apps via universal links para simplesmente acionar algumas ações ou transferir informações. Nesse caso, deve ser verificado se não está vazando informações sensíveis.
Se você tem o código-fonte original, pode pesquisá-lo pelo método openURL:options: completionHandler: e verificar os dados sendo manipulados.
Observe que o método
openURL:options:completionHandler:não é usado apenas para abrir universal links, mas também para chamar esquemas de URL personalizados.
Este é um exemplo do app Telegram:
}, openUniversalUrl: { url, completion in
if #available(iOS 10.0, *) {
var parsedUrl = URL(string: url)
if let parsed = parsedUrl {
if parsed.scheme == nil || parsed.scheme!.isEmpty {
parsedUrl = URL(string: "https://\(url)")
}
}
if let parsedUrl = parsedUrl {
return UIApplication.shared.open(parsedUrl,
options: [UIApplicationOpenURLOptionUniversalLinksOnly: true as NSNumber],
completionHandler: { value in completion.completion(value)}
)
Observe como o app adapta o scheme para "https" antes de abri-lo e como usa a opção UIApplicationOpenURLOptionUniversalLinksOnly: true que abre o URL apenas se o URL for um universal link válido e houver um app instalado capaz de abrir esse URL.
Se você não tem o código-fonte original, pesquise nos símbolos e nas strings do binário do app. Por exemplo, pesquisaremos por métodos Objective-C que contenham "openURL":
$ rabin2 -zq Telegram\ X.app/Telegram\ X | grep openURL
0x1000dee3f 50 49 application:openURL:sourceApplication:annotation:
0x1000dee71 29 28 application:openURL:options:
0x1000df2c9 9 8 openURL:
0x1000df772 35 34 openURL:options:completionHandler:
Como esperado, openURL:options:completionHandler: está entre os encontrados (lembre-se de que também pode estar presente porque o app abre esquemas de URL personalizados). Em seguida, para garantir que nenhuma informação sensível esteja sendo vazada, você terá que realizar análise dinâmica e inspecionar os dados sendo transmitidos. Consulte Teste de Custom URL Schemes para alguns exemplos de hooking e rastreamento deste método.
Análise Dinâmica¶
Se um app está implementando universal links, você deve ter as seguintes saídas da análise estática:
- os domínios associados
- o arquivo Apple App Site Association
- o método receptor do link
- o método manipulador de dados
Você pode usar isso agora para testá-los dinamicamente:
- Acionando universal links
- Identificando universal links válidos
- Rastreando o método receptor do link
- Verificando como os links são abertos
Acionando Universal Links¶
Diferente dos esquemas de URL personalizados, infelizmente você não pode testar universal links do Safari apenas digitando-os na barra de pesquisa diretamente, pois isso não é permitido pela Apple. Mas você pode testá-los a qualquer momento usando outros apps como o app Notas:
- Abra o app Notas e crie uma nova nota.
- Escreva os links incluindo o domínio.
- Deixe o modo de edição no app Notas.
- Pressione longamente os links para abri-los (lembre-se que um clique padrão aciona a opção padrão).
Para fazer isso a partir do Safari, você terá que encontrar um link existente em um site que, uma vez clicado, será reconhecido como um Universal Link. Isso pode ser um pouco demorado.
Alternativamente, você também pode usar o Frida para isso, veja Teste de Custom URL Schemes para mais detalhes.
Identificando Universal Links Válidos¶
Primeiro, veremos a diferença entre abrir um Universal Link permitido e um que não deveria ser permitido.
Do apple-app-site-association da apple.com que vimos acima, escolhemos os seguintes caminhos:
"paths": [
"NOT /shop/buy-iphone/*",
...
"/today",
Um deles deve oferecer a opção "Abrir no app" e o outro não.
Se pressionarmos longamente no primeiro (http://www.apple.com/shop/buy-iphone/iphone-xr), ele só oferece a opção de abri-lo (no navegador).

Se pressionarmos longamente no segundo (http://www.apple.com/today), ele mostra opções para abri-lo no Safari e na "Apple Store":

Observe que há uma diferença entre um clique e um pressionamento longo. Uma vez que pressionamos longamente um link e selecionamos uma opção, por exemplo, "Abrir no Safari", esta se tornará a opção padrão para todos os cliques futuros até pressionarmos longamente novamente e selecionarmos outra opção.
Se repetirmos o processo no método application:continueUserActivity: restorationHandler: através de hooking ou rastreamento, veremos como ele é chamado assim que abrimos o universal link permitido. Para isso, você pode usar, por exemplo, frida-trace:
frida-trace -U "Apple Store" -m "*[* *restorationHandler*]"
Rastreando o Método Receptor do Link¶
Esta seção explica como rastrear o método receptor do link e como extrair informações adicionais. Para este exemplo, usaremos o Telegram, pois não há restrições em seu arquivo apple-app-site-association:
{
"applinks": {
"apps": [],
"details": [
{
"appID": "X834Q8SBVP.org.telegram.TelegramEnterprise",
"paths": [
"*"
]
},
{
"appID": "C67CF9S4VU.ph.telegra.Telegraph",
"paths": [
"*"
]
},
{
"appID": "X834Q8SBVP.org.telegram.Telegram-iOS",
"paths": [
"*"
]
}
]
}
}
Para abrir os links, também usaremos o app Notas e frida-trace com o seguinte padrão:
frida-trace -U Telegram -m "*[* *restorationHandler*]"
Escreva https://t.me/addstickers/radare (encontrado através de uma rápida pesquisa na Internet) e abra-o a partir do app Notas.

Primeiro, deixamos o frida-trace gerar os stubs em __handlers__/:
$ frida-trace -U Telegram -m "*[* *restorationHandler*]"
Instrumenting functions...
-[AppDelegate application:continueUserActivity:restorationHandler:]
Você pode ver que apenas uma função foi encontrada e está sendo instrumentada. Acione agora o universal link e observe os rastreamentos.
298382 ms -[AppDelegate application:0x10556b3c0 continueUserActivity:0x1c4237780
restorationHandler:0x16f27a898]
Você pode observar que a função está de fato sendo chamada. Você pode agora adicionar código aos stubs em __handlers__/ para obter mais detalhes:
// __handlers__/__AppDelegate_application_contin_8e36bbb1.js
onEnter: function (log, args, state) {
log("-[AppDelegate application: " + args[2] + " continueUserActivity: " + args[3] +
" restorationHandler: " + args[4] + "]");
log("\tapplication: " + ObjC.Object(args[2]).toString());
log("\tcontinueUserActivity: " + ObjC.Object(args[3]).toString());
log("\t\twebpageURL: " + ObjC.Object(args[3]).webpageURL().toString());
log("\t\tactivityType: " + ObjC.Object(args[3]).activityType().toString());
log("\t\tuserInfo: " + ObjC.Object(args[3]).userInfo().toString());
log("\trestorationHandler: " +ObjC.Object(args[4]).toString());
},
A nova saída é:
298382 ms -[AppDelegate application:0x10556b3c0 continueUserActivity:0x1c4237780
restorationHandler:0x16f27a898]
298382 ms application:<Application: 0x10556b3c0>
298382 ms continueUserActivity:<NSUserActivity: 0x1c4237780>
298382 ms webpageURL:http://t.me/addstickers/radare
298382 ms activityType:NSUserActivityTypeBrowsingWeb
298382 ms userInfo:{
}
298382 ms restorationHandler:<__NSStackBlock__: 0x16f27a898>
Além dos parâmetros da função, adicionamos mais informações chamando alguns métodos deles para obter mais detalhes, neste caso sobre o NSUserActivity. Se olharmos na Documentação do Desenvolvedor da Apple, podemos ver o que mais podemos chamar deste objeto.
Verificando Como os Links São Abertos¶
Se você quiser saber mais sobre qual função realmente abre o URL e como os dados estão sendo tratados, você deve continuar investigando.
Estenda o comando anterior para descobrir se há outras funções envolvidas na abertura do URL.
frida-trace -U Telegram -m "*[* *restorationHandler*]" -i "*open*Url*"
-iinclui qualquer método. Você também pode usar um padrão glob aqui (por exemplo,-i "*open*Url*"significa "incluir qualquer função contendo 'open', depois 'Url' e algo mais")
Novamente, primeiro deixamos o frida-trace gerar os stubs em __handlers__/:
$ frida-trace -U Telegram -m "*[* *restorationHandler*]" -i "*open*Url*"
Instrumenting functions...
-[AppDelegate application:continueUserActivity:restorationHandler:]
$S10TelegramUI0A19ApplicationBindingsC16openUniversalUrlyySS_AA0ac4OpenG10Completion...
$S10TelegramUI15openExternalUrl7account7context3url05forceD016presentationData18application...
$S10TelegramUI31AuthorizationSequenceControllerC7account7strings7openUrl5apiId0J4HashAC0A4Core19...
...
Agora você pode ver uma longa lista de funções, mas ainda não sabemos quais serão chamadas. Acione o universal link novamente e observe os rastreamentos.
/* TID 0x303 */
298382 ms -[AppDelegate application:0x10556b3c0 continueUserActivity:0x1c4237780
restorationHandler:0x16f27a898]
298619 ms | $S10TelegramUI15openExternalUrl7account7context3url05forceD016presentationData
18applicationContext20navigationController12dismissInputy0A4Core7AccountC_AA
14OpenURLContextOSSSbAA012PresentationK0CAA0a11ApplicationM0C7Display0
10NavigationO0CSgyyctF()
Além do método Objective-C, agora há uma função Swift que também é do seu interesse.
Provavelmente não há documentação para essa função Swift, mas você pode simplesmente desmascarar seu símbolo usando swift-demangle via xcrun:
xcrun pode ser usado para invocar ferramentas de desenvolvedor do Xcode a partir da linha de comando, sem tê-las no path. Neste caso, ele localizará e executará swift-demangle, uma ferramenta do Xcode que desmascara símbolos Swift.
$ xcrun swift-demangle S10TelegramUI15openExternalUrl7account7context3url05forceD016presentationData
18applicationContext20navigationController12dismissInputy0A4Core7AccountC_AA14OpenURLContextOSSSbAA0
12PresentationK0CAA0a11ApplicationM0C7Display010NavigationO0CSgyyctF
Resultando em:
---> TelegramUI.openExternalUrl(
account: TelegramCore.Account, context: TelegramUI.OpenURLContext, url: Swift.String,
forceExternal: Swift.Bool, presentationData: TelegramUI.PresentationData,
applicationContext: TelegramUI.TelegramApplicationContext,
navigationController: Display.NavigationController?, dismissInput: () -> ()) -> ()
Isso não apenas fornece a classe (ou módulo) do método, seu nome e os parâmetros, mas também revela os tipos de parâmetros e o tipo de retorno, então, caso você precise mergulhar mais fundo, agora sabe por onde começar.
Por enquanto, usaremos essas informações para imprimir corretamente os parâmetros editando o arquivo stub:
// __handlers__/TelegramUI/_S10TelegramUI15openExternalUrl7_b1a3234e.js
onEnter: function (log, args, state) {
log("TelegramUI.openExternalUrl(account: TelegramCore.Account,
context: TelegramUI.OpenURLContext, url: Swift.String, forceExternal: Swift.Bool,
presentationData: TelegramUI.PresentationData,
applicationContext: TelegramUI.TelegramApplicationContext,
navigationController: Display.NavigationController?, dismissInput: () -> ()) -> ()");
log("\taccount: " + ObjC.Object(args[0]).toString());
log("\tcontext: " + ObjC.Object(args[1]).toString());
log("\turl: " + ObjC.Object(args[2]).toString());
log("\tpresentationData: " + args[3]);
log("\tapplicationContext: " + ObjC.Object(args[4]).toString());
log("\tnavigationController: " + ObjC.Object(args[5]).toString());
},
Desta forma, da próxima vez que executarmos, obteremos uma saída muito mais detalhada:
298382 ms -[AppDelegate application:0x10556b3c0 continueUserActivity:0x1c4237780
restorationHandler:0x16f27a898]
298382 ms application:<Application: 0x10556b3c0>
298382 ms continueUserActivity:<NSUserActivity: 0x1c4237780>
298382 ms webpageURL:http://t.me/addstickers/radare
298382 ms activityType:NSUserActivityTypeBrowsingWeb
298382 ms userInfo:{
}
298382 ms restorationHandler:<__NSStackBlock__: 0x16f27a898>
298619 ms | TelegramUI.openExternalUrl(account: TelegramCore.Account,
context: TelegramUI.OpenURLContext, url: Swift.String, forceExternal: Swift.Bool,
presentationData: TelegramUI.PresentationData, applicationContext:
TelegramUI.TelegramApplicationContext, navigationController: Display.NavigationController?,
dismissInput: () -> ()) -> ()
298619 ms | account: TelegramCore.Account
298619 ms | context: nil
298619 ms | url: http://t.me/addstickers/radare
298619 ms | presentationData: 0x1c4e40fd1
298619 ms | applicationContext: nil
298619 ms | navigationController: TelegramUI.PresentationData
Lá você pode observar o seguinte:
- Ele chama
application:continueUserActivity:restorationHandler:do app delegate conforme esperado. application:continueUserActivity:restorationHandler:trata o URL, mas não o abre; ele chamaTelegramUI.openExternalUrlpara isso.- O URL sendo aberto é
https://t.me/addstickers/radare.
Você pode agora continuar e tentar rastrear e verificar como os dados estão sendo validados. Por exemplo, se você tem dois apps que comunicam via universal links, pode usar isso para ver se o app remetente está vazando dados sensíveis fazendo hooking desses métodos no app receptor. Isso é especialmente útil quando você não tem o código-fonte, pois será capaz de recuperar o URL completo que não veria de outra forma, pois pode ser o resultado de clicar em algum botão ou acionar alguma funcionalidade.
Em alguns casos, você pode encontrar dados em userInfo do objeto NSUserActivity. No caso anterior, não havia dados sendo transferidos, mas pode ser o caso para outros cenários. Para ver isso, certifique-se de fazer hooking da propriedade userInfo ou acessá-la diretamente do objeto continueUserActivity em seu hook (por exemplo, adicionando uma linha como esta log("userInfo:" + ObjC.Object(args[3]).userInfo().toString());).
Notas Finais sobre Universal Links e Handoff¶
Universal links e o recurso Handoff da Apple estão relacionados:
- Ambos dependem do mesmo método ao receber dados:
application:continueUserActivity:restorationHandler:
- Como os universal links, a Continuação de Atividade do Handoff deve ser declarada no direito
com.apple.developer.associated-domainse no arquivoapple-app-site-associationdo servidor (em ambos os casos via a palavra-chave"activitycontinuation":). Veja "Recuperando o Arquivo Apple App Site Association" acima para um exemplo.
Na verdade, o exemplo anterior em "Verificando Como os Links São Abertos" é muito similar ao cenário "Web Browser-to-Native App Handoff" descrito no "Guia de Programação Handoff":
Se o usuário estiver usando um navegador web no dispositivo de origem, e o dispositivo receptor for um dispositivo iOS com um app nativo que reivindica a parte do domínio da propriedade
webpageURL, então o iOS inicia o app nativo e envia a ele um objetoNSUserActivitycom um valoractivityTypedeNSUserActivityTypeBrowsingWeb. A propriedadewebpageURLcontém o URL que o usuário estava visitando, enquanto o dicionáriouserInfoestá vazio.
Na saída detalhada acima, você pode ver que o objeto NSUserActivity que recebemos atende exatamente aos pontos mencionados:
298382 ms -[AppDelegate application:0x10556b3c0 continueUserActivity:0x1c4237780
restorationHandler:0x16f27a898]
298382 ms application:<Application: 0x10556b3c0>
298382 ms continueUserActivity:<NSUserActivity: 0x1c4237780>
298382 ms webpageURL:http://t.me/addstickers/radare
298382 ms activityType:NSUserActivityTypeBrowsingWeb
298382 ms userInfo:{
}
298382 ms restorationHandler:<__NSStackBlock__: 0x16f27a898>
Este conhecimento deve ajudá-lo ao testar apps que suportam Handoff.