Flutter Hackers: descobrindo a miopia do desenvolvedor (parte 2)

Mar 18 2023
Mergulhe fundo na engenharia reversa Flutter APK Release Mode com Frida
Nem todos os aplicativos Flutter são difíceis de serem analisados ​​estaticamente e para identificar uma vulnerabilidade em si, também podemos começar a incluir o pacote usado como uma variável no escopo da demanda de teste de caixa preta. Um Preâmbulo No post anterior, apresentei como as relações entre Dart e Flutter são correlacionadas entre si e também um dos erros de como o desenvolvedor deveria ter percebido na fase de construção do aplicativo.

Nem todos os aplicativos Flutter são difíceis de serem analisados ​​estaticamente e para identificar uma vulnerabilidade em si, também podemos começar a incluir o pacote usado como uma variável no escopo da demanda de teste de caixa preta.

Um Preâmbulo

No post anterior , apresentei como as relações entre Dart e Flutter são correlacionadas entre si e também um dos erros de como o desenvolvedor deveria ter percebido na fase de construção do aplicativo. Se você ainda não leu sobre isso, eu recomendo que você leia primeiro, especialmente se você nunca ouviu falar sobre essa estrutura de aplicativo.

Há uma empresa que considero uma startup e me convidou para fazer alguns testes de caixa preta em seu servidor web recente e APK em seu programa privado. Eu não tinha certeza disso desde que ouvi falar que Flutter é a escolha deles sobre como seu APK foi construído em cima dele. No entanto, sempre há uma primeira vez para tudo, então arrisquei examinar o aplicativo.

Dentro de aproximadamente 4 a 5 dias, uma das vulnerabilidades que descobri é bastante única, mas a causa não é de seu design, na verdade, era do pacote público que eles foram usados. Essa vulnerabilidade permite que eu entre no modo de desenvolvedor e mostre várias PII dentro desse APK. Eu também indiquei que não é uma prática recomendada armazenar credenciais de "superusuário" codificadas, mesmo que estejam criptografadas e seria melhor se eles gerenciassem todos os usuários não por uma função baseada em função para serem distinguidos assim, talvez um determinado token exclusivo e uma autorização segura seja o melhor para eles usarem e compararem dentro de seu servidor.

Nesta redação, quero compartilhar como descobri essa vulnerabilidade e mostrar a metodologia que usei para me ajudar a descobrir a miopia de segurança do desenvolvedor, recriando o próprio cenário em um desafio CTF em ITS (ARA CTF 2023).

Conceito Prévio

Suponha que somos usuários não autorizados que não têm capacidade de acessar esses ativos privados de determinado usuário, ou mesmo privilegiados. Precisamos primeiro saber como o aplicativo funciona, aproveitando a falta de opções de perímetro seguro quando o desenvolvedor tenta criar o release-build do próprio APK.

flutter build apk --release

No Gradle(gradle.properties) , eles podem esquecer de adicionar esta linha devido às suas preferências ou simplesmente não se importam em expor a nomenclatura da função.

extra-gen-snapshot-options=--obfuscate

flutter build apk --release --obfuscate

Este pode ser o primeiro blunderpara o desenvolvedor que não vê em um escopo amplo como lidar com a engenharia reversa, tornando sua vida mais fácil de entender o que está acontecendo dentro do aplicativo.

Neste cenário, farei o Flutter Release APK sem os obfuscatesinalizadores que tem como objetivo recuperar uma string secreta que foi armazenada dentro do APK ( hardcoded ) com uma criptografia de bloco simétrica conhecida .

Imagem retirada de: https://medium.com/@.Qubit/symmetric-key-algorithm-in-cryptography-3d839bba8613

A nomenclatura da análise

Sabemos que os Snapshots do Flutter (nas bibliotecas suportadas) estão em uma formatação de sintaxe diferente e isso dificulta a vida da engenharia reversa. Mas, como eles não habilitaram as propriedades gradle de ofuscação de compilação, isso pode facilitar as coisas.

Vamos dar uma espiada primeiro no aplicativo flutter. Podemos instalar este APK em um dispositivo Android real ou em um emulador de Android preferido. Existem vários emuladores que são gratuitos para usar:

  • NoxPlayerName
  • LDPlayer
  • Genymotion ( com pacotes de tradução ARM )
  • BlueStacksGenericName
  • AVD Manager do Android Studio
  • Emulador de Android baseado em QEMU

adb install -g <path-to-the-apk.apk>

      
                

Agora, a primeira coisa que podemos fazer é analisar os objetos compartilhados instantâneos ( libapp.so ) usando o decompiler IDA . Precisamos descompilar o APK primeiro usando apktool. Em seguida, podemos verificar a pasta lib/ se houver dois objetos compartilhados que consistem em libflutter.so e libapp.so .

apktool d <your-path-to-flutter.apk>

      
                

Para usar o reFlutter , instale-o primeiro como um pacote Python e simplesmente localize o caminho para o APK. Haverá duas opções que serão a escolha de como queremos que o reFlutter se comporte. Esta ferramenta corrigirá as bibliotecas de objetos compartilhados (ELF) de nosso APK e, eventualmente, poderá se vincular à porta 8083 em todas as interfaces (incluindo o modo de proxy invisível). O que estamos procurando é o arquivo dump.dart que será produzido após a execução do APK corrigido e haverá os deslocamentos de código que podemos aproveitar para usar.

if ver>27:
        replaceFileText('src/third_party/dart/runtime/vm/class_table.cc','::Print() {','::Print()  { OS::PrintErr("reFlutter");\n char pushArr[60000]="";\n')
        replaceFileText('src/flutter/BUILD.gn','  if (is_android) {\n    public_deps +=\n        [ "//flutter/shell/platform/android:flutter_shell_native_unittests" ]\n  }','')
        replaceFileText('src/third_party/dart/runtime/vm/class_table.cc','OS::PrintErr("%" Pd ": %s\\n", i, name.ToCString());','\n     auto& funcs = Array::Handle(cls.functions());    if (funcs.Length()>1000) {    continue;    } char classText[100000]="";    String& supname = String::Handle();     name = cls.Name(); strcat(classText,cls.ToCString());     Class& supcls = Class::Handle();    supcls = cls.SuperClass();     if (!supcls.IsNull()) {   supname = supcls.Name();    strcat(classText," extends ");   strcat(classText,supname.ToCString());  }    const auto& interfaces = Array::Handle(cls.interfaces()); auto& interface = Instance::Handle();    for (intptr_t in = 0;in < interfaces.Length(); in++) { interface^=interfaces.At(in); if(in==0){strcat(classText," implements ");}    if(in>0){strcat(classText," , ");}  strcat(classText,interface.ToCString()); }    strcat(classText," {\\n"); const auto& fields = Array::Handle(cls.fields());      auto& field = Field::Handle(); auto& fieldType = AbstractType::Handle();    String& fieldTypeName = String::Handle(); String& finame = String::Handle();    Instance& instance2 = Instance::Handle();    for (intptr_t f = 0; f < fields.Length(); f++)    {    field ^= fields.At(f); finame = field.name(); fieldType = field.type(); fieldTypeName = fieldType.Name(); strcat(classText,"  ");    strcat(classText,fieldTypeName.ToCString());  strcat(classText," "); strcat(classText,finame.ToCString());     if(field.is_static()){   instance2 ^= field.StaticValue();   strcat(classText," = ");     strcat(classText,instance2.ToCString());   strcat(classText," ;\\n");  }    else {   strcat(classText," = ");   strcat(classText," nonstatic;\\n");  } }     for (intptr_t c = 0; c < funcs.Length(); c++) {      auto& func = Function::Handle();    func = cls.FunctionFromIndex(c);     String& signature = String::Handle();    signature = func.InternalSignature();auto& codee = Code::Handle(func.CurrentCode());      if(!func.IsLocalFunction()) {    strcat(classText," \\n  "); strcat(classText,func.ToCString()); strcat(classText," ");    strcat(classText,signature.ToCString());    strcat(classText," { \\n\\n              ");   char append[70];   sprintf(append," Code Offset: _kDartIsolateSnapshotInstructions + 0x%016" PRIxPTR "\\n",static_cast<uintptr_t>(codee.MonomorphicUncheckedEntryPoint()));   strcat(classText,append);    strcat(classText,"       \\n       }\\n");    } else {    auto& parf = Function::Handle(); parf=func.parent_function();    String& signParent = String::Handle();       signParent = parf.InternalSignature();     strcat(classText," \\n  ");     strcat(classText,parf.ToCString()); strcat(classText," "); strcat(classText,signParent.ToCString());    strcat(classText," { \\n\\n          ");    char append[80];   sprintf(append," Code Offset: _kDartIsolateSnapshotInstructions + 0x%016" PRIxPTR "\\n",static_cast<uintptr_t>(codee.MonomorphicUncheckedEntryPoint()));   strcat(classText,append);    strcat(classText,"       \\n       }\\n");  } }       strcat(classText," \\n      }\\n\\n");      const Library& libr = Library::Handle(cls.library());if (!libr.IsNull()) {  auto& owner_class = Class::Handle(); owner_class = libr.toplevel_class();   auto& funcsTopLevel = Array::Handle(owner_class.functions());   char pushTmp[1000];   String& owner_name = String::Handle();   owner_name = libr.url();   sprintf(pushTmp,"\'%s\',",owner_name.ToCString());  if (funcsTopLevel.Length()>0&&strstr(pushArr, pushTmp) == NULL) {  strcat(pushArr,pushTmp);   strcat(classText,"Library:"); strcat(classText,pushTmp); strcat(classText," {\\n");         for (intptr_t c = 0; c < funcsTopLevel.Length(); c++) {      auto& func = Function::Handle();    func = owner_class.FunctionFromIndex(c);     String& signature = String::Handle();       signature = func.InternalSignature();   auto& codee = Code::Handle(func.CurrentCode());    if(!func.IsLocalFunction()) {    strcat(classText," \\n  "); strcat(classText,func.ToCString()); strcat(classText," ");    strcat(classText,signature.ToCString());    strcat(classText," { \\n\\n              ");   char append[70];   sprintf(append," Code Offset: _kDartIsolateSnapshotInstructions + 0x%016" PRIxPTR "\\n",static_cast<uintptr_t>(codee.MonomorphicUncheckedEntryPoint()));   strcat(classText,append);    strcat(classText,"       \\n       }\\n");    } else {    auto& parf = Function::Handle(); parf=func.parent_function();    String& signParent = String::Handle();       signParent = parf.InternalSignature();     strcat(classText," \\n  ");     strcat(classText,parf.ToCString()); strcat(classText," "); strcat(classText,signParent.ToCString());    strcat(classText," { \\n\\n          ");    char append[80];   sprintf(append," Code Offset: _kDartIsolateSnapshotInstructions + 0x%016" PRIxPTR "\\n",static_cast<uintptr_t>(codee.MonomorphicUncheckedEntryPoint()));   strcat(classText,append);    strcat(classText,"       \\n       }\\n");  }   }             strcat(classText," \\n      }\\n\\n");}}   struct stat entry_info;   int exists = 0;   if (stat("/data/data/", &entry_info)==0 && S_ISDIR(entry_info.st_mode)){    exists=1;   }         if(exists==1){    pid_t pid = getpid();    char path[64] = { 0 };    sprintf(path, "/proc/%d/cmdline", pid);        FILE *cmdline = fopen(path, "r");    if (cmdline) {          char chm[264] = { 0 };  char pat[264] = { 0 };        char application_id[64] = { 0 };          fread(application_id, sizeof(application_id), 1, cmdline);  sprintf(pat, "/data/data/%s/dump.dart", application_id);          do { FILE *f = fopen(pat, "a+");   fprintf(f, "%s",classText);   fflush(f);   fclose(f);   sprintf(chm,"/data/data/%s",application_id);  chmod(chm, S_IRWXU|S_IRWXG|S_IRWXO);  chmod(pat, S_IRWXU|S_IRWXG|S_IRWXO);   } while (0);        fclose(cmdline);    }   }                                       if(exists==0){           char pat[264] = { 0 };  sprintf(pat, "%s/Documents/dump.dart", getenv("HOME"));   OS::PrintErr("reFlutter dump file: %s",pat);     do { FILE *f = fopen(pat, "a+");   fprintf(f, "%s",classText);   fflush(f);   fclose(f);      } while (0);            }')
        #replaceFileText('src/third_party/dart/runtime/vm/class_table.cc','OS::PrintErr("%" Pd ": %s\\n", i, name.ToCString());','auto& funcs = Array::Handle(cls.functions());    if (funcs.Length()>1000) {    continue;    } char classText[65000]="";  String& supname = String::Handle();    name = cls.Name(); strcat(classText," "); strcat(classText,cls.ToCString());    Class& supcls = Class::Handle();    supcls = cls.SuperClass();    if (!supcls.IsNull()) {   supname = supcls.Name();   strcat(classText," extends ");   strcat(classText,supname.ToCString());  } const auto& interfaces = Array::Handle(cls.interfaces()); auto& interface = Instance::Handle(); for (intptr_t in = 0;in < interfaces.Length(); in++) { interface^=interfaces.At(in); if(in==0){strcat(classText," implements ");}    if(in>0){strcat(classText," , ");}  strcat(classText,interface.ToCString()); } strcat(classText," {\\n"); const auto& fields = Array::Handle(cls.fields());    auto& field = Field::Handle(); auto& fieldType = AbstractType::Handle();    String& fieldTypeName = String::Handle(); String& finame = String::Handle(); Instance& instance2 = Instance::Handle();  for (intptr_t f = 0; f < fields.Length(); f++) {    field ^= fields.At(f); finame = field.name(); fieldType = field.type(); fieldTypeName = fieldType.Name(); strcat(classText," "); strcat(classText,fieldTypeName.ToCString());  strcat(classText," "); strcat(classText,finame.ToCString());   if(field.is_static()){   instance2 ^= field.StaticValue();   strcat(classText," = ");   strcat(classText,instance2.ToCString());   strcat(classText," ;\\n");  } else {   strcat(classText," = ");   strcat(classText," nonstatic;\\n");  } }    for (intptr_t c = 0; c < funcs.Length(); c++) {      auto& func = Function::Handle();    func = cls.FunctionFromIndex(c);    String& signature = String::Handle();    signature = func.InternalSignature(); if(!func.IsLocalFunction()) { strcat(classText," \\n"); strcat(classText,func.ToCString()); strcat(classText," ");    strcat(classText,signature.ToCString()); strcat(classText," { \\n\\n                  }\\n"); } else { auto& parf = Function::Handle(); parf=func.parent_function(); String& signParent = String::Handle();    signParent = parf.InternalSignature(); strcat(classText," \\n"); strcat(classText,parf.ToCString()); strcat(classText," "); strcat(classText,signParent.ToCString()); strcat(classText," { \\n\\n                  }\\n"); } } OS::PrintErr("reflutter:\\n %s \\n      }\\n",classText);')
    else:
        replaceFileText('src/third_party/dart/runtime/vm/class_table.cc','::Print() {','::Print()  { OS::PrintErr("reFlutter");\n char pushArr[60000]="";\n')
        replaceFileText('src/third_party/dart/runtime/vm/class_table.cc','OS::PrintErr("%" Pd ": %s\\n", i, name.ToCString());','\n      auto& funcs = Array::Handle(cls.functions());    if (funcs.Length()>1000) {    continue;    } char classText[100000]="";    String& supname = String::Handle();     name = cls.Name(); strcat(classText,cls.ToCString());     Class& supcls = Class::Handle();    supcls = cls.SuperClass();     if (!supcls.IsNull()) {   supname = supcls.Name();    strcat(classText," extends ");   strcat(classText,supname.ToCString());  }    const auto& interfaces = Array::Handle(cls.interfaces()); auto& interface = Instance::Handle();    for (intptr_t in = 0;in < interfaces.Length(); in++) { interface^=interfaces.At(in); if(in==0){strcat(classText," implements ");}    if(in>0){strcat(classText," , ");}  strcat(classText,interface.ToCString()); }    strcat(classText," {\\n"); const auto& fields = Array::Handle(cls.fields());      auto& field = Field::Handle(); auto& fieldType = AbstractType::Handle();    String& fieldTypeName = String::Handle(); String& finame = String::Handle();    Instance& instance2 = Instance::Handle();    for (intptr_t f = 0; f < fields.Length(); f++)    {    field ^= fields.At(f); finame = field.name(); fieldType = field.type(); fieldTypeName = fieldType.Name(); strcat(classText,"  ");    strcat(classText,fieldTypeName.ToCString());  strcat(classText," "); strcat(classText,finame.ToCString());     if(field.is_static()){   instance2 = field.StaticValue();   strcat(classText," = ");     strcat(classText,instance2.ToCString());   strcat(classText," ;\\n");  }    else {   strcat(classText," = ");   strcat(classText," nonstatic;\\n");  } }     for (intptr_t c = 0; c < funcs.Length(); c++) {      auto& func = Function::Handle();    func = cls.FunctionFromIndex(c);     String& signature = String::Handle();    signature = func.Signature();auto& codee = Code::Handle(func.CurrentCode());      if(!func.IsLocalFunction()) {    strcat(classText," \\n  "); strcat(classText,func.ToCString()); strcat(classText," ");    strcat(classText,signature.ToCString());    strcat(classText," { \\n\\n              ");   char append[70];   sprintf(append," Code Offset: _kDartIsolateSnapshotInstructions + 0x%016" PRIxPTR "\\n",static_cast<uintptr_t>(codee.MonomorphicUncheckedEntryPoint()));   strcat(classText,append);    strcat(classText,"       \\n       }\\n");    } else {    auto& parf = Function::Handle(); parf=func.parent_function();    String& signParent = String::Handle();       signParent = parf.Signature();     strcat(classText," \\n  ");     strcat(classText,parf.ToCString()); strcat(classText," "); strcat(classText,signParent.ToCString());    strcat(classText," { \\n\\n          ");    char append[80];   sprintf(append," Code Offset: _kDartIsolateSnapshotInstructions + 0x%016" PRIxPTR "\\n",static_cast<uintptr_t>(codee.MonomorphicUncheckedEntryPoint()));   strcat(classText,append);    strcat(classText,"       \\n       }\\n");  } }       strcat(classText," \\n      }\\n\\n");      const Library& libr = Library::Handle(cls.library());if (!libr.IsNull()) {  auto& owner_class = Class::Handle(); owner_class = libr.toplevel_class();   auto& funcsTopLevel = Array::Handle(owner_class.functions());   char pushTmp[1000];   String& owner_name = String::Handle();   owner_name = libr.url();   sprintf(pushTmp,"\'%s\',",owner_name.ToCString());  if (funcsTopLevel.Length()>0&&strstr(pushArr, pushTmp) == NULL) {  strcat(pushArr,pushTmp);   strcat(classText,"Library:"); strcat(classText,pushTmp); strcat(classText," {\\n");         for (intptr_t c = 0; c < funcsTopLevel.Length(); c++) {      auto& func = Function::Handle();    func = owner_class.FunctionFromIndex(c);     String& signature = String::Handle();       signature = func.Signature();   auto& codee = Code::Handle(func.CurrentCode());    if(!func.IsLocalFunction()) {    strcat(classText," \\n  "); strcat(classText,func.ToCString()); strcat(classText," ");    strcat(classText,signature.ToCString());    strcat(classText," { \\n\\n              ");   char append[70];   sprintf(append," Code Offset: _kDartIsolateSnapshotInstructions + 0x%016" PRIxPTR "\\n",static_cast<uintptr_t>(codee.MonomorphicUncheckedEntryPoint()));   strcat(classText,append);    strcat(classText,"       \\n       }\\n");    } else {    auto& parf = Function::Handle(); parf=func.parent_function();    String& signParent = String::Handle();       signParent = parf.Signature();     strcat(classText," \\n  ");     strcat(classText,parf.ToCString()); strcat(classText," "); strcat(classText,signParent.ToCString());    strcat(classText," { \\n\\n          ");    char append[80];   sprintf(append," Code Offset: _kDartIsolateSnapshotInstructions + 0x%016" PRIxPTR "\\n",static_cast<uintptr_t>(codee.MonomorphicUncheckedEntryPoint()));   strcat(classText,append);    strcat(classText,"       \\n       }\\n");  }   }             strcat(classText," \\n      }\\n\\n");}}   struct stat entry_info;   int exists = 0;   if (stat("/data/data/", &entry_info)==0 && S_ISDIR(entry_info.st_mode)){    exists=1;   }         if(exists==1){    pid_t pid = getpid();    char path[64] = { 0 };    sprintf(path, "/proc/%d/cmdline", pid);        FILE *cmdline = fopen(path, "r");    if (cmdline) {          char chm[264] = { 0 };  char pat[264] = { 0 };        char application_id[64] = { 0 };          fread(application_id, sizeof(application_id), 1, cmdline);  sprintf(pat, "/data/data/%s/dump.dart", application_id);          do { FILE *f = fopen(pat, "a+");   fprintf(f, "%s",classText);   fflush(f);   fclose(f);   sprintf(chm,"/data/data/%s",application_id);  chmod(chm, S_IRWXU|S_IRWXG|S_IRWXO);  chmod(pat, S_IRWXU|S_IRWXG|S_IRWXO);   } while (0);        fclose(cmdline);    }   }                                       if(exists==0){           char pat[264] = { 0 };        sprintf(pat, "%s/Documents/dump.dart", getenv("HOME")); OS::PrintErr("reFlutter dump file: %s",pat);          do { FILE *f = fopen(pat, "a+");   fprintf(f, "%s",classText);   fflush(f);   fclose(f);      } while (0);            }')
        #replaceFileText('src/third_party/dart/runtime/vm/class_table.cc','OS::PrintErr("%" Pd ": %s\\n", i, name.ToCString());','#if defined(HOST_ARCH_X64)  uintptr_t instrArch = 0xE000;#elif defined(HOST_ARCH_ARM64)  uintptr_t  instrArch = 0xF000;#else  uintptr_t instrArch = 0xB000;#endif      auto& funcs = Array::Handle(cls.functions());    if (funcs.Length()>1000) {    continue;    } char classText[100000]="";    String& supname = String::Handle();     name = cls.Name(); strcat(classText,cls.ToCString());     Class& supcls = Class::Handle();    supcls = cls.SuperClass();     if (!supcls.IsNull()) {   supname = supcls.Name();    strcat(classText," extends ");   strcat(classText,supname.ToCString());  }    const auto& interfaces = Array::Handle(cls.interfaces()); auto& interface = Instance::Handle();    for (intptr_t in = 0;in < interfaces.Length(); in++) { interface^=interfaces.At(in); if(in==0){strcat(classText," implements ");}    if(in>0){strcat(classText," , ");}  strcat(classText,interface.ToCString()); }    strcat(classText," {\\n"); const auto& fields = Array::Handle(cls.fields());      auto& field = Field::Handle(); auto& fieldType = AbstractType::Handle();    String& fieldTypeName = String::Handle(); String& finame = String::Handle();    Instance& instance2 = Instance::Handle();    for (intptr_t f = 0; f < fields.Length(); f++)    {    field ^= fields.At(f); finame = field.name(); fieldType = field.type(); fieldTypeName = fieldType.Name(); strcat(classText,"  ");    strcat(classText,fieldTypeName.ToCString());  strcat(classText," "); strcat(classText,finame.ToCString());     if(field.is_static()){   instance2 = field.StaticValue();   strcat(classText," = ");     strcat(classText,instance2.ToCString());   strcat(classText," ;\\n");  }    else {   strcat(classText," = ");   strcat(classText," nonstatic;\\n");  } }     for (intptr_t c = 0; c < funcs.Length(); c++) {      auto& func = Function::Handle();    func = cls.FunctionFromIndex(c);     String& signature = String::Handle();    signature = func.Signature();auto& codee = Code::Handle(func.CurrentCode());      if(!func.IsLocalFunction()) {    strcat(classText," \\n  "); strcat(classText,func.ToCString()); strcat(classText," ");    strcat(classText,signature.ToCString());    strcat(classText," { \\n\\n              ");   char append[70];   sprintf(append," Code Offset: 0x%016" PRIxPTR "\\n",static_cast<uintptr_t>(codee.MonomorphicUncheckedEntryPoint())+ instrArch);   strcat(classText,append);    strcat(classText,"       \\n       }\\n");    } else {    auto& parf = Function::Handle(); parf=func.parent_function();    String& signParent = String::Handle();       signParent = parf.Signature();     strcat(classText," \\n  ");     strcat(classText,parf.ToCString()); strcat(classText," "); strcat(classText,signParent.ToCString());    strcat(classText," { \\n\\n          ");    char append[50];   sprintf(append," Code Offset: 0x%016" PRIxPTR "\\n",static_cast<uintptr_t>(codee.MonomorphicUncheckedEntryPoint()) + instrArch);   strcat(classText,append);    strcat(classText,"       \\n       }\\n");  } }       strcat(classText," \\n      }\\n\\n");      const Library& libr = Library::Handle(cls.library());if (!libr.IsNull()) {  auto& owner_class = Class::Handle(); owner_class = libr.toplevel_class();   auto& funcsTopLevel = Array::Handle(owner_class.functions());   char pushTmp[1000];   String& owner_name = String::Handle();   owner_name = libr.url();   sprintf(pushTmp,"\'%s\',",owner_name.ToCString());  if (funcsTopLevel.Length()>0&&strstr(pushArr, pushTmp) == NULL) {  strcat(pushArr,pushTmp);   strcat(classText,"Library:"); strcat(classText,pushTmp); strcat(classText," {\\n");         for (intptr_t c = 0; c < funcsTopLevel.Length(); c++) {      auto& func = Function::Handle();    func = owner_class.FunctionFromIndex(c);     String& signature = String::Handle();       signature = func.Signature();   auto& codee = Code::Handle(func.CurrentCode());    if(!func.IsLocalFunction()) {    strcat(classText," \\n  "); strcat(classText,func.ToCString()); strcat(classText," ");    strcat(classText,signature.ToCString());    strcat(classText," { \\n\\n              ");   char append[70];   sprintf(append," Code Offset: 0x%016" PRIxPTR "\\n",static_cast<uintptr_t>(codee.MonomorphicUncheckedEntryPoint())+ instrArch);   strcat(classText,append);    strcat(classText,"       \\n       }\\n");    } else {    auto& parf = Function::Handle(); parf=func.parent_function();    String& signParent = String::Handle();       signParent = parf.Signature();     strcat(classText," \\n  ");     strcat(classText,parf.ToCString()); strcat(classText," "); strcat(classText,signParent.ToCString());    strcat(classText," { \\n\\n          ");    char append[50];   sprintf(append," Code Offset: 0x%016" PRIxPTR "\\n",static_cast<uintptr_t>(codee.MonomorphicUncheckedEntryPoint()) + instrArch);   strcat(classText,append);    strcat(classText,"       \\n       }\\n");  }   }             strcat(classText," \\n      }\\n\\n");}}   struct stat entry_info;   int exists = 0;   if (stat("/data/data/", &entry_info)==0 && S_ISDIR(entry_info.st_mode)){    exists=1;   }         if(exists==1){    pid_t pid = getpid();    char path[64] = { 0 };    sprintf(path, "/proc/%d/cmdline", pid);        FILE *cmdline = fopen(path, "r");    if (cmdline) {          char chm[264] = { 0 };  char pat[264] = { 0 };        char application_id[64] = { 0 };          fread(application_id, sizeof(application_id), 1, cmdline);  sprintf(pat, "/data/data/%s/dump.dart", application_id);          do { FILE *f = fopen(pat, "a+");   fprintf(f, "%s",classText);   fflush(f);   fclose(f);   sprintf(chm,"/data/data/%s",application_id);  chmod(chm, S_IRWXU|S_IRWXG|S_IRWXO);  chmod(pat, S_IRWXU|S_IRWXG|S_IRWXO);   } while (0);        fclose(cmdline);    }   }                                       if(exists==0){           char pat[264] = "/tmp/dump.dart";          do { FILE *f = fopen(pat, "a+");   fprintf(f, "%s",classText);   fflush(f);   fclose(f);      } while (0);            }')
    replaceFileText('src/third_party/dart/tools/make_version.py','snapshot_hash = MakeSnapshotHashString()', 'snapshot_hash = \''+hashS+'\'')
    replaceFileText('src/third_party/dart/runtime/bin/socket.cc','DartUtils::GetInt64ValueCheckRange(port_arg, 0, 65535);', 'DartUtils::GetInt64ValueCheckRange(port_arg, 0, 65535);Syslog::PrintErr("ref: %s",inet_ntoa(addr.in.sin_addr));if(port>50){port=8083;addr.addr.sa_family=AF_INET;addr.in.sin_family=AF_INET;inet_aton("192.168.133.104", &addr.in.sin_addr);}')
    replaceFileText('src/third_party/boringssl/src/ssl/ssl_x509.cc','static bool ssl_crypto_x509_session_verify_cert_chain(SSL_SESSION *session,\n                                                      SSL_HANDSHAKE *hs,\n                                                      uint8_t *out_alert) {', 'static bool ssl_crypto_x509_session_verify_cert_chain(SSL_SESSION *session,\n                                                      SSL_HANDSHAKE *hs,\n                                                      uint8_t *out_alert) {return true;')
    replaceFileText('src/third_party/boringssl/src/ssl/ssl_x509.cc','static int ssl_crypto_x509_session_verify_cert_chain(SSL_SESSION *session,\n

Podemos verificar imediatamente a função principal do Dart no arquivo dump.dart , responsável por manter as lógicas do aplicativo Flutter. Isso é como um clássico int main()em C ou MainActivityno APK padrão para o contexto do ponto de entrada.

Library:'package:pwndroid/main.dart' Class: _MyHomePageState@806319839 extends State {
 
  Function 'compare':. String: null { 

               Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000188164
       
       }
 
  Function 'encrypt':. String: null { 

               Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000185418
       
       }
 
  Function 'prepare':. String: null { 

               Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000184cd0
       
       }
 
  Function 'build':. String: null { 

               Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000184a78
       
       }
 
      }

Library:'package:pwndroid/main.dart', {
 
  Function 'main': static. () => void { 

               Code Offset: _kDartIsolateSnapshotInstructions + 0x000000000024b0f0
       
       }
 
      }

Observe que o valor de deslocamento do código é baseado em _kDartIsolateSnapshotInstructions , que é o endereço de deslocamento do código base que será atribuído e incrementado pelo xvalor de cada função distinta. Isso significa, por exemplo, que podemos querer localizar a comparefunção, de modo que 0x1707f0 + 0x0000000000188164 seja o endereço dessa função.

Outra coisa importante que precisa ser anotada é o pacote que é usado pelo aplicativo Flutter. Este nome de pacote pode ser lido como strings no objeto compartilhado ELF binário e dump.dart também o reconhece, começando com um prefixo de package:[flutter|dart]/.*.

Podemos voltar ao IDA novamente e verificar os pacotes usados ​​( View -> Open Subviews -> Strings ).

Podemos rolar um pouco para baixo e podemos ver que há um pacote criptografado que é usado. Alguns fatos no arquivo dump.dart é que, uma vez que tenhamos gerado as lógicas por meio desse arquivo, as funções usadas dos pacotes têm características fixas. Isso significa que apenas funções específicas usadas do pacote também serão despejadas, mas o restante das funções não utilizadas será ignorado.

Library:'package:encrypt/encrypt.dart' Class: Encrypter extends Object {
 
  Function 'encryptBytes':. String: null { 

               Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000185544
       
       }
 
  Function 'encrypt':. String: null { 

               Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000188210
       
       }
 
      }

Library:'package:encrypt/encrypt.dart' Class: Encrypted extends Object {
 
  Function 'Encrypted.fromUtf8': constructor. String: null { 

               Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000002664
       
       }
 
  Function 'Encrypted.fromLength': constructor. String: null { 

               Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000002664
       
       }
 
  Function 'get:base64':. String: null { 

               Code Offset: _kDartIsolateSnapshotInstructions + 0x00000000001881cc
       
       }
 
  Function '==':. String: null { 

               Code Offset: _kDartIsolateSnapshotInstructions + 0x00000000001ea96c
       
       }
 
      }

Library:'package:encrypt/encrypt.dart' Class: Key extends Encrypted {
 
  Function 'Key.fromLength': constructor. String: null { 

               Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000002664
       
       }
 
      }

Library:'package:encrypt/encrypt.dart' Class: IV extends Encrypted {
 
  Function 'IV.fromUtf8': constructor. String: null { 

               Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000002664
       
       }
 
      }

Library:'package:encrypt/encrypt.dart' Class: Salsa20 extends Object implements Type: Algorithm {
 
  Function 'Salsa20.': constructor. String: null { 

               Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000002664
       
       }
 
  Function 'encrypt':. String: null { 

               Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000185590
       
       }
 
  Function '_buildParams@132180997':. String: null { 

               Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000188064
       
       }
 
      }

Library:'package:encrypt/encrypt.dart' Class: Algorithm extends Object {
 
      }

Eu experimentei o pacote por um tempo e descobri esse bug no pacote criptografar do Flutter (https://pub.dev/packages/encrypt) na versão atual. Se verificarmos os problemas anteriores e correções de seu GitHub oficial, eles já disseram para implementar bytes seguros gerados aleatoriamente para o método, mas acontece que realmente não foi assim que tentamos usá-lo diretamente no projeto Flutter.fromLength

Vamos tentar usar um projeto Flutter em sandbox usando Zapp . Primeiro, adicionamos a versão mais recente do pacote encrypt para 5.0.1 ao arquivo pubspec.yaml que contém as informações de dependências do projeto Flutter e importa o pacote encrypt para o arquivo main.dart .

Feito isso, compilamos e construímos o projeto Flutter Apps e verificamos os logs.

O fromLengthmétodo retorna um bytearray de zeros à direita e isso significa que ainda é inseguro, pois resultou em null-bytes . Podemos aproveitar esses bytes fracos gerados da chave para descriptografar alguns objetos relacionados posteriormente.

Unboxing através do Hooking

Estamos na etapa final aqui e geralmente usaremos IDA e Frida para análise posterior. O arquivo dump.dart contém o deslocamento específico de cada função e, portanto, renomearemos a função removida de acordo com os dumps. Observe que você também pode seguir os guias do Guardsquare para renomear as funções imediatamente de acordo com a função de dump com referência cruzada.

main.dart 
===========================================================

main() = sub_3bb8e0 [Widgets and Function Calls]
compare() = sub_2f8954 [comparing s1 & s2]
encrypt() = sub_2f5c08
prepare() = sub_2f54c0
build() = sub_2f5268

encrypt.dart [Encrypter]
===========================================================

encryptBytes() = sub_2f5d34
encrypt() = sub_2f8a00

encrypt.dart [Encrypted]
============================================================

Encrypted.fromUtf8() = sub_172e54 [derivate from Salsa20]
Encrypted.fromLength() = sub_172e54 [derivate from Salsa20]
Key.fromLength() = sub_172e54 [derivate from Encrypted.fromLength]
IV.fromUtf8() = sub_172e54 [derivate from Encrypted.fromUtf8]

get:base64() = sub_2f89bc

Salsa20
============================================================

Salsa20 = sub_172e54
encrypt() = sub_2f5d80

function hookFunc() {
    var isolate =  0x00000000001707f0;
    // var target = 0x000000000017c240;
    var target = 0x0000000000185418; //encrypt
    var dumpOffset = isolate + target;

    var argBufferSize = 300

    var address = Module.findBaseAddress('libapp.so') // libapp.so (Android) or App (IOS) 
    console.log('\n\nbaseAddress: ' + address.toString())

    var codeOffset = address.add(dumpOffset)
    console.log('codeOffset: ' + codeOffset.toString())
    console.log('')
    console.log('Wait..... ')

    Interceptor.attach(codeOffset, {
        onEnter: function(args) {

            console.log('')
            console.log('--------------------------------------------|')
            console.log('\n    Hook Function: ' + dumpOffset);
            console.log('')
            console.log('--------------------------------------------|')
            console.log('')

            // for (var argStep = 0; argStep < 20; argStep++) {
            //     try {
            //         dumpArgs(argStep, args[argStep], argBufferSize);
            //     } catch (e) {
            //         break;
            //     }

            // }
            for(let i = 0; i < 8; i++) { 
                try { 
                console.log("addr ",i,args[i]);
                console.log(hexdump(args[i])); 
                console.log("Value")
                console.log(Memory.readCString(ptr(args[i])));
                console.log("Pointer address hexdump")
                console.log(hexdump(ptr(args[i])));
                } catch (error) { 
                console.log("fail",i,(args[i])); 
                } 
            } 

        },
        onLeave: function(retval) {
            console.log('RETURN : ' + retval)
            // console.log(hexdump(retval))
            // dumpArgs(0, retval, 300);

            // for (var argStep = 0; argStep < 50; argStep++) {
            //     try {
            //         dumpArgs(argStep, retval[argStep], argBufferSize);
            //     } catch (e) {

            //         break;
            //     }

            // }
        }
    });

}

function dumpArgs(step, address, bufSize) {

    var buf = Memory.readByteArray(address, bufSize)

    console.log('Argument ' + step + ' address ' + address.toString() + ' ' + 'buffer: ' + bufSize.toString() + '\n\n Value:\n' +hexdump(buf, {
        offset: 0,
        length: bufSize,
        header: false,
        ansi: false
    }));

    console.log("Trying interpret that arg is pointer")
    console.log("=====================================")
    try{

    console.log(Memory.readCString(ptr(address)));
    console.log(ptr(address).readCString());
    console.log(hexdump(ptr(address)));
    }catch(e){
        console.log(e);
    }


    console.log('')
    console.log('----------------------------------------------------')
    console.log('')
}

setTimeout(hookFunc, 1000)

Existem muitos zeros dentro do gancho de memória interceptado atual e também podemos assumir que os processos de criptografia começam aqui com o Salsa20algoritmo e sua chave & IV. No entanto, não sabemos realmente o IV, mas como Salsa20usa apenas 8 bytes de IV, isso significa que podemos forçar brutalmente ou usar bytes conhecidos dentro do hexdump de gancho de memória por 8 bytes.

Vamos fazer outro hooking com compare() , já que queremos saber quais valores estão sendo comparados.

Há apenas uma string codificada em base64 que é ilegível quando é decodificada. Se assumirmos nossa hipótese, a string codificada em base64 é nossa entrada que já foi criptografada e codificada finalmente em base64. Deve haver um valor codificado que é comparado dentro do objeto binário compartilhado.

De volta ao IDA, verificamos novamente se há um valor base64 embutido em código.

Acontece que há um valor base64 codificado lá. Agora tudo o que precisamos fazer é reconstruir o processo de descriptografia do Salsa20algoritmo com a chave de bytes nulos e também um IV desconhecido. Podemos usar força bruta nesses 8 bytes, mas na verdade vimos o IV real no primeiro hexdump ( 0x10, 0x18, 0x5a, 0x22, 0x07, 0x7e, 0x62, 0x41 ).

Aqui está o script python final de acordo com o pacote PyCryptodome .

from Crypto.Cipher import Salsa20
enc = b"cmc2UbeRkpDnZdyfGoiMEtwgf3n9wug4Gd3SB8EouUM4R7c2tBCVJeOmygQqjE5LNy6DmaDRkqEzG0nrkXkYHG77ooISZ23vLqR+LQ=="
key = b"\x00"*32
nonce_iv = b''.join([chr(i).encode() for i in [0x10,0x18,0x5a,0x22,0x7,0x7e,0x62,0x41]])
cip = Salsa20.new(key=key,nonce=nonce_iv)
print(cip.decrypt(base64.b64decode(enc)))

Alguns dizem que seria impossível analisar um aplicativo Flutter construído no modo de lançamento. No entanto, na verdade não! Não tenha medo de experimentar analisá-los novamente pela abordagem estática/dinâmica. Não há muitos aplicativos de segurança do lado do cliente que sejam invioláveis ​​ou mesmo que usem um método anti-engenharia reversa para nos enfrentar como uma engenharia reversa. Se estivermos presos, eu ainda recomendo que você inspecione seus pacotes usados ​​também, quem sabe você pode ter sorte em encontrar uma vulnerabilidade lá.

Sinta-se à vontade para comentar ou compartilhar essas dicas! Se você teve perspectivas ou correções diferentes que acha que ajudarão este conteúdo a mudar o que deveria ter sido implementado corretamente, não hesite em entrar em contato comigo!

Referências

Revertendo uma amostra do Android que usa o Flutter

Ama meu conteúdo? Você pode me apoiar no Trakteer/BMC . Obrigado por ler estes escritos e espero que você obtenha um novo conhecimento a partir daqui!

https://trakteer.id/felix-alexander-swfnt/tip?open=true