Skip to content

MASTG-TEST-0075: Teste de Custom URL Schemes

Visão Geral

Análise Estática

Existem algumas coisas que podemos fazer usando análise estática. Nas próximas seções veremos o seguinte:

  • Teste de registro de esquemas de URL personalizados
  • Teste de registro de esquemas de consulta de aplicativos
  • Teste de manipulação e validação de URL
  • Teste de solicitações de URL para outros aplicativos
  • Teste de métodos obsoletos

Teste de Registro de Esquemas de URL Personalizados

O primeiro passo para testar esquemas de URL personalizados é descobrir se um aplicativo registra algum manipulador de protocolo.

Se você tem o código-fonte original e quer visualizar os manipuladores de protocolo registrados, basta abrir o projeto no Xcode, ir até a aba Info e abrir a seção URL Types como apresentado na captura de tela abaixo:

Também no Xcode você pode encontrar isso pesquisando pela chave CFBundleURLTypes no arquivo Info.plist do aplicativo (exemplo de iGoat-Swift):

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLName</key>
        <string>com.iGoat.myCompany</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>iGoat</string>
        </array>
    </dict>
</array>

Em um aplicativo compilado (ou IPA), os manipuladores de protocolo registrados são encontrados no arquivo Info.plist na pasta raiz do pacote do aplicativo. Abra-o e procure pela chave CFBundleURLSchemes, se presente, ela deve conter um array de strings (exemplo de iGoat-Swift):

grep -A 5 -nri urlsch Info.plist
Info.plist:45:    <key>CFBundleURLSchemes</key>
Info.plist-46-    <array>
Info.plist-47-        <string>iGoat</string>
Info.plist-48-    </array>

Uma vez que o esquema de URL é registrado, outros aplicativos podem abrir o aplicativo que registrou o esquema e passar parâmetros criando URLs formatadas apropriadamente e abrindo-as com o método UIApplication openURL:options:completionHandler:.

Nota do App Programming Guide for iOS:

Se mais de um aplicativo de terceiros registrar para manipular o mesmo esquema de URL, atualmente não há um processo para determinar qual aplicativo receberá esse esquema.

Isso pode levar a um ataque de sequestro de esquema de URL (veja a página 136 em [#thiel2]).

Teste de Registro de Esquemas de Consulta de Aplicativos

Antes de chamar o método openURL:options:completionHandler:, os aplicativos podem chamar canOpenURL: para verificar se o aplicativo de destino está disponível. No entanto, como este método estava sendo usado por aplicativos maliciosos como uma forma de enumerar aplicativos instalados, a partir do iOS 9.0 os esquemas de URL passados para ele também devem ser declarados adicionando a chave LSApplicationQueriesSchemes ao arquivo Info.plist do aplicativo e um array de até 50 esquemas de URL.

<key>LSApplicationQueriesSchemes</key>
    <array>
        <string>url_scheme1</string>
        <string>url_scheme2</string>
    </array>

canOpenURL sempre retornará NO para esquemas não declarados, independentemente de um aplicativo apropriado estar instalado ou não. No entanto, esta restrição aplica-se apenas a canOpenURL.

O método openURL:options:completionHandler: ainda abrirá qualquer esquema de URL, mesmo que o array LSApplicationQueriesSchemes tenha sido declarado, e retornará YES / NO dependendo do resultado.

Como exemplo, o Telegram declara em seu Info.plist estes Esquemas de Consulta, entre outros:

    <key>LSApplicationQueriesSchemes</key>
    <array>
        <string>dbapi-3</string>
        <string>instagram</string>
        <string>googledrive</string>
        <string>comgooglemaps-x-callback</string>
        <string>foursquare</string>
        <string>here-location</string>
        <string>yandexmaps</string>
        <string>yandexnavi</string>
        <string>comgooglemaps</string>
        <string>youtube</string>
        <string>twitter</string>
        ...

Teste de Manipulação e Validação de URL

Para determinar como um caminho de URL é construído e validado, se você tem o código-fonte original, pode pesquisar pelos seguintes métodos:

  • Método application:didFinishLaunchingWithOptions: ou application:will-FinishLaunchingWithOptions:: verifique como a decisão é tomada e como as informações sobre a URL são recuperadas.
  • application:openURL:options:: verifique como o recurso está sendo aberto, ou seja, como os dados estão sendo analisados, verifique as opções, especialmente se o acesso pelo aplicativo chamador (sourceApplication) deve ser permitido ou negado. O aplicativo também pode precisar de permissão do usuário ao usar o esquema de URL personalizado.

No Telegram você encontrará quatro métodos diferentes sendo usados:

func application(_ application: UIApplication, open url: URL, sourceApplication: String?) -> Bool {
    self.openUrl(url: url)
    return true
}

func application(_ application: UIApplication, open url: URL, sourceApplication: String?,
annotation: Any) -> Bool {
    self.openUrl(url: url)
    return true
}

func application(_ app: UIApplication, open url: URL,
options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
    self.openUrl(url: url)
    return true
}

func application(_ application: UIApplication, handleOpen url: URL) -> Bool {
    self.openUrl(url: url)
    return true
}

Podemos observar algumas coisas aqui:

Teste de Solicitações de URL para Outros Aplicativos

O método openURL:options:completionHandler: e o método obsoleto openURL: de UIApplication são responsáveis por abrir URLs (ou seja, para enviar solicitações / fazer consultas a outros aplicativos) que podem ser locais para o aplicativo atual ou podem ser fornecidos por um aplicativo diferente. Se você tem o código-fonte original, pode pesquisar diretamente por usos desses métodos.

Além disso, se você estiver interessado em saber se o aplicativo está consultando serviços ou aplicativos específicos, e se o aplicativo é bem conhecido, você também pode pesquisar por esquemas de URL comuns online e incluí-los em suas pesquisas. Por exemplo, uma rápida pesquisa no Google revela:

Apple Music - music:// ou musics:// ou audio-player-event://
Calendar - calshow:// ou x-apple-calevent://
Contacts - contacts://
Diagnostics - diagnostics:// ou diags://
GarageBand - garageband://
iBooks - ibooks:// ou itms-books:// ou itms-bookss://
Mail - message:// ou mailto://emailaddress
Messages - sms://phonenumber
Notes - mobilenotes://
...

Pesquisamos por este método no código-fonte do Telegram, desta vez sem usar o Xcode, apenas com egrep:

$ egrep -nr "open.*options.*completionHandler" ./Telegram-iOS/

./AppDelegate.swift:552: return UIApplication.shared.open(parsedUrl,
    options: [UIApplicationOpenURLOptionUniversalLinksOnly: true as NSNumber],
    completionHandler: { value in
./AppDelegate.swift:556: return UIApplication.shared.open(parsedUrl,
    options: [UIApplicationOpenURLOptionUniversalLinksOnly: true as NSNumber],
    completionHandler: { value in

Se inspecionarmos os resultados, veremos que openURL:options:completionHandler: está realmente sendo usado para links universais, então temos que continuar pesquisando. Por exemplo, podemos pesquisar por openURL(:

$ egrep -nr "openURL\(" ./Telegram-iOS/

./ApplicationContext.swift:763:  UIApplication.shared.openURL(parsedUrl)
./ApplicationContext.swift:792:  UIApplication.shared.openURL(URL(
                                        string: "https://telegram.org/deactivate?phone=\(phone)")!
                                 )
./AppDelegate.swift:423:         UIApplication.shared.openURL(url)
./AppDelegate.swift:538:         UIApplication.shared.openURL(parsedUrl)
...

Se inspecionarmos essas linhas, veremos como este método também está sendo usado para abrir "Configurações" ou para abrir a "Página da App Store".

Ao pesquisar apenas por :// vemos:

if documentUri.hasPrefix("file://"), let path = URL(string: documentUri)?.path {
if !url.hasPrefix("mt-encrypted-file://?") {
guard let dict = TGStringUtils.argumentDictionary(inUrlString: String(url[url.index(url.startIndex,
    offsetBy: "mt-encrypted-file://?".count)...])) else {
parsedUrl = URL(string: "https://\(url)")
if let url = URL(string: "itms-apps://itunes.apple.com/app/id\(appStoreId)") {
} else if let url = url as? String, url.lowercased().hasPrefix("tg://") {
[[WKExtension sharedExtension] openSystemURL:[NSURL URLWithString:[NSString
    stringWithFormat:@"tel://%@", userHandle.data]]];

Após combinar os resultados de ambas as pesquisas e inspecionar cuidadosamente o código-fonte, encontramos o seguinte trecho de código:

openUrl: { url in
            var parsedUrl = URL(string: url)
            if let parsed = parsedUrl {
                if parsed.scheme == nil || parsed.scheme!.isEmpty {
                    parsedUrl = URL(string: "https://\(url)")
                }
                if parsed.scheme == "tg" {
                    return
                }
            }

            if let parsedUrl = parsedUrl {
                UIApplication.shared.openURL(parsedUrl)

Antes de abrir uma URL, o esquema é validado, "https" será adicionado se necessário e não abrirá nenhuma URL com o esquema "tg". Quando pronto, usará o método obsoleto openURL.

Se você tiver apenas o aplicativo compilado (IPA), ainda pode tentar identificar quais esquemas de URL estão sendo usados para consultar outros aplicativos:

  • Verifique se LSApplicationQueriesSchemes foi declarado ou pesquise por esquemas de URL comuns.
  • Também use a string :// ou construa uma expressão regular para corresponder a URLs, pois o aplicativo pode não estar declarando alguns esquemas.

Você pode fazer isso primeiro verificando se o binário do aplicativo contém essas strings usando, por exemplo, o comando Unix strings:

strings <yourapp> | grep "someURLscheme://"

ou ainda melhor, use o comando iz/izz do radare2 ou rafind2, ambos encontrarão strings onde o comando Unix strings não encontrará. Exemplo de iGoat-Swift:

$ r2 -qc izz~iGoat:// iGoat-Swift
37436 0x001ee610 0x001ee610  23  24 (4.__TEXT.__cstring) ascii iGoat://?contactNumber=

Teste de Métodos Obsoletos

Pesquise por métodos obsoletos como:

Por exemplo, usando rabin2 encontramos esses três:

$ rabin2 -zzq Telegram\ X.app/Telegram\ X | grep -i "openurl"

0x1000d9e90 31 30 UIApplicationOpenURLOptionsKey
0x1000dee3f 50 49 application:openURL:sourceApplication:annotation:
0x1000dee71 29 28 application:openURL:options:
0x1000dee8e 27 26 application:handleOpenURL:
0x1000df2c9 9 8 openURL:
0x1000df766 12 11 canOpenURL:
0x1000df772 35 34 openURL:options:completionHandler:
...

Análise Dinâmica

Uma vez que você identificou os esquemas de URL personalizados que o aplicativo registrou, existem vários métodos que você pode usar para testá-los:

  • Realizando solicitações de URL
  • Identificando e interceptando o método manipulador de URL
  • Testando a validação de origem dos esquemas de URL
  • Fuzzing de esquemas de URL

Realizando Solicitações de URL

Usando o Safari

Para testar rapidamente um esquema de URL, você pode abrir as URLs no Safari e observar como o aplicativo se comporta. Por exemplo, se você escrever tel://123456789 na barra de endereços do Safari, um pop-up aparecerá com o número de telefone e as opções "Cancelar" e "Ligar". Se você pressionar "Ligar", ele abrirá o aplicativo Telefone e fará a chamada diretamente.

Você também pode conhecer páginas que acionam esquemas de URL personalizados, você pode simplesmente navegar normalmente para essas páginas e o Safari perguntará automaticamente quando encontrar um esquema de URL personalizado.

Usando o App de Notas

Como já visto em "Acionando Links Universais", você pode usar o aplicativo Notas e pressionar longamente os links que você escreveu para testar esquemas de URL personalizados. Lembre-se de sair do modo de edição para poder abri-los. Observe que você pode clicar ou pressionar longamente links incluindo esquemas de URL personalizados apenas se o aplicativo estiver instalado, caso contrário, eles não serão destacados como links clicáveis.

Usando Frida

Se você simplesmente quer que um aplicativo abra o esquema de URL, pode fazer isso usando o Frida. Exemplo usando iGoat-Swift:

$ frida -U iGoat-Swift

[iPhone::iGoat-Swift]-> function openURL(url) {
                            var UIApplication = ObjC.classes.UIApplication.sharedApplication();
                            var toOpen = ObjC.classes.NSURL.URLWithString_(url);
                            return UIApplication.openURL_(toOpen);
                        }
[iPhone::iGoat-Swift]-> openURL("tel://234234234")
true

Neste exemplo do Frida CodeShare o autor usa a API não pública LSApplication Workspace.openSensitiveURL:withOptions: para abrir as URLs (do aplicativo SpringBoard):

function openURL(url) {
    var w = ObjC.classes.LSApplicationWorkspace.defaultWorkspace();
    var toOpen = ObjC.classes.NSURL.URLWithString_(url);
    return w.openSensitiveURL_withOptions_(toOpen, null);
}

Observe que o uso de APIs não públicas não é permitido na App Store, por isso nem mesmo testamos isso, mas temos permissão para usá-las para nossa análise dinâmica.

Identificando e Interceptando o Método Manipulador de URL

Se você não pode olhar o código-fonte original, terá que descobrir por si mesmo qual método o aplicativo usa para manipular as solicitações de esquema de URL que recebe. Você não pode saber se é um método Objective-C ou Swift, ou mesmo se o aplicativo está usando um obsoleto.

Para isso, usaremos o observador de método ObjC do Frida CodeShare, que é um script extremamente útil que permite observar rapidamente qualquer coleção de métodos ou classes apenas fornecendo um padrão simples.

Neste caso, estamos interessados em todos os métodos do aplicativo iGoat-Swift contendo "openURL", portanto nosso padrão será *[* *openURL*]:

  • O primeiro asterisco corresponderá a todos os métodos de instância - e classe +.
  • O segundo corresponde a todas as classes Objective-C.
  • O terceiro e o quarto permitem corresponder a qualquer método contendo a string openURL.
$ frida -U iGoat-Swift --codeshare mrmacete/objc-method-observer

[iPhone::iGoat-Swift]-> observeSomething("*[* *openURL*]");
Observing  -[_UIDICActivityItemProvider activityViewController:openURLAnnotationForActivityType:]
Observing  -[CNQuickActionsManager _openURL:]
Observing  -[SUClientController openURL:]
Observing  -[SUClientController openURL:inClientWithIdentifier:]
Observing  -[FBSSystemService openURL:application:options:clientPort:withResult:]
Observing  -[iGoat_Swift.AppDelegate application:openURL:options:]
Observing  -[PrefsUILinkLabel openURL:]
Observing  -[UIApplication openURL:]
Observing  -[UIApplication _openURL:]
Observing  -[UIApplication openURL:options:completionHandler:]
Observing  -[UIApplication openURL:withCompletionHandler:]
Observing  -[UIApplication _openURL:originatingView:completionHandler:]
Observing  -[SUApplication application:openURL:sourceApplication:annotation:]
...

A lista é muito longa e inclui os métodos que já mencionamos. Se acionarmos agora um esquema de URL, por exemplo "igoat://" do Safari e aceitarmos abri-lo no aplicativo, veremos o seguinte:

[iPhone::iGoat-Swift]-> (0x1c4038280)  -[iGoat_Swift.AppDelegate application:openURL:options:]
application: <UIApplication: 0x101d0fad0>
openURL: igoat://
options: {
    UIApplicationOpenURLOptionsOpenInPlaceKey = 0;
    UIApplicationOpenURLOptionsSourceApplicationKey = "com.apple.mobilesafari";
}
0x18b5030d8 UIKit!__58-[UIApplication _applicationOpenURLAction:payload:origin:]_block_invoke
0x18b502a94 UIKit!-[UIApplication _applicationOpenURLAction:payload:origin:]
...
0x1817e1048 libdispatch.dylib!_dispatch_client_callout
0x1817e86c8 libdispatch.dylib!_dispatch_block_invoke_direct$VARIANT$mp
0x18453d9f4 FrontBoardServices!__FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__
0x18453d698 FrontBoardServices!-[FBSSerialQueue _performNext]
RET: 0x1

Agora sabemos que:

  • O método -[iGoat_Swift.AppDelegate application:openURL:options:] é chamado. Como vimos antes, é a maneira recomendada e não é obsoleta.
  • Ele recebe nossa URL como parâmetro: igoat://.
  • Também podemos verificar a aplicação de origem: com.apple.mobilesafari.
  • Também podemos saber de onde foi chamado, conforme esperado de -[UIApplication _applicationOpenURLAction:payload:origin:].
  • O método retorna 0x1 que significa YES (o delegado manipulou a solicitação com sucesso).

A chamada foi bem-sucedida e agora vemos que o aplicativo iGoat-Swift foi aberto:

Observe que também podemos ver que o chamador (aplicação de origem) foi o Safari se olharmos no canto superior esquerdo da captura