Выкрасть телефон – было малым делом, а вот взломать его сложнее. Но тут в дело вступает Том, который умеет взламывать такие приложения, словно это орешки!
Тома под рукой у меня не оказалось, поэтому как всегда пришлось делать все самому.
tl;dr
Вызвать метод AIDL интерфейса чтобы стригерить получение картинки из нативной библиотеки
открыть activity DraftPerArtistActivity_90vbsf45
посмотреть имя художника
взять от него sha256 и отправить в качестве ключа.
Как завещали деды - начинать нужно с манифеста. Поэтому набиваем в консоли jadx-gui NBI_ART_DEMO_ARM64.apk и смотрим, что там нафантазировали организаторы:
Куча экспортированных activity с хитрыми именами и непонятным назначением, пара с более-менее понятным назначением и очень интересный экспортированный сервис, который так и говорит нам - напихай в меня чего-нибудь запрещенного. С него и начнем.
Говорят, что однажды автор таски для android CTF не сделал нативную либу и у него отвалилась Android Studio… Ладно, с библиотекой разберемся потом. Судя по говорящему названию класса - где-то здесь должна быть реализация AIDL интерфейса.
The Android Interface Definition Language (AIDL) is similar to other IDLs you might have worked with. It allows you to define the programming interface that both the client and service agree upon in order to communicate with each other using interprocess communication (IPC). On Android, one process cannot normally access the memory of another process. So to talk, they need to decompose their objects into primitives that the operating system can understand, and marshall the objects across that boundary for you. The code to do that marshalling is tedious to write, so Android handles it for you with AIDL.
https://developer.android.com/guide/components/aidl
Лежит она в абстрактном классе AbstractBinderC0641a, от которого наследуется BinderC0595a.
publicabstractclassAbstractBinderC0641aextendsBinderimplementsIInterface{publicAbstractBinderC0641a(){attachInterface(this,"com.newbrainindustrial.aidl.ILogoDecoderAIDL");}@Override// android.os.IInterfacepublicIBinderasBinder(){returnthis;}@Override// android.os.BinderpublicbooleanonTransact(intcode,Parcelparcel,Parcelparcel2,intflags){if(code==1){parcel.enforceInterface("com.newbrainindustrial.aidl.ILogoDecoderAIDL");intreadInt=parcel.readInt();intreadInt2=parcel.readInt();intreadInt3=parcel.readInt();Log.i("LogoDecoderAIDL","com.newbrainindustrial.aidl.ILogoDecoderAIDL.ShowTheLogoOfTruth(args): called by some app!");C0642a.f2694a=LogoDecoderAIDL.this.getData();parcel2.writeNoException();parcel2.writeInt(readInt+readInt2+readInt3);returntrue;}elseif(code!=Binder.INTERFACE_TRANSACTION){returnsuper.onTransact(code,parcel,parcel2,flags);}else{parcel2.writeString("com.newbrainindustrial.aidl.ILogoDecoderAIDL");returntrue;}}}
Интерес для нас представляют две строчки - 18 и 19. Первая любезно раскрывает нам название метода, который нужно дернуть, а вторая запускает получение данных из нативной библиотеки. Судя по остальному коду, метод можно вызывать с любыми параметрами, т.к. они нигде не используются и ни на что не влияют. Отлично. Записываем этот факт в блокнотик и смотрим, как полученный из метода getData() байтовый массив используется дальше. Для этого ищем все ссылки на поле f2694a.
А вот и одна из тех activity с хитрыми именами, которые мы видели в манифесте. Посмотрим, что там есть.
Судя по всему байтовый массив содержит какую-то картинку. Поиск ссылок на эту activity ничего не дает, поэтому придется вызывать руками. Итого, на текущем этапе можно составить такой алгоритм атаки на это приложение:
Создать свое приложение с AIDL интерфейсом
Вызвать метод ShowTheLogoOfTruth чтобы библиотека отдалась нам байтовый массив с картинкой
Запустить activity c помощью adb и посмотреть на результат
С приложением все просто. Создаем AIDL интерфейс c именем ILogoDecoderAIDL через меню File->New->AIDL->AIDL File
Суть эксплойта в том, чтобы забиндить удаленный сервис с нужной командой, что приведет к срабатыванию метода onServiceConnected, в котором вызывается метод ShowTheLogoOfTruth(). При вызове этого метода, также будет вызван метод onTransact(), который описан выше, и будет заполнен байтовый массив.
Чтобы убедиться в успешности операции, включим сбор логов приложения и запустим код на выполнение.
Судя по логам - эксплойт сработал. Теперь можно запустить activity чтобы посмотреть изображение:
1
adb shell am start -W -n com.newbrainindustrial.galery/com.newbrainindustrial.galery.DraftPerArtistActivity_90vbsf45
А вот и флаг!
Нас интересует надпись на картинке: Joey Welch. Для получения ключа нужно взять от этого имени sha256 хэш и соединить его с префиксом NQ2022
Все описанное выше мне показалось явно недостаточным, и ружье в виде нативной библиотеки повешенное в самом начале акта должно же было когда-то выстрелить… Поэтому недолго думая, я загрузил библиотеку в гидру и решил посмотреть, что там еще напрятано.
В секции экспорта сразу была найдена функция Java_com_newbrainindustrial_galery_LogoDecoderAIDL_getData
undefined8*Java_com_newbrainindustrial_galery_LogoDecoderAIDL_getData(long*param_1){longlVar1;char*__ptr;undefined8*puVar2;ulonguVar3;longlVar4;ulonguVar5;undefined8uVar6;void*local_88;void*local_80;void*local_70;void*local_68;undefined8*local_58;undefined8*local_50;undefined8*local_48;ulonglocal_40;longlocal_38;lVar1=cRead_8(tpidr_el0);local_38=*(long*)(lVar1+0x28);__ptr=(char*)pathHelperGetPath();if(__ptr==(char*)0x0){puVar2=(undefined8*)0x0;}else{local_40=0;puVar2=(undefined8*)unzipHelperGetEntryHash(__ptr,&local_40);if(puVar2==(undefined8*)0x0){free(__ptr);}else{local_50=(undefined8*)0x0;local_48=(undefined8*)0x0;local_58=(undefined8*)0x0;local_58=(undefined8*)operator.new(0x10);local_50=local_58+2;uVar6=*puVar2;local_58[1]=puVar2[1];*local_58=uVar6;local_48=local_50;std::__ndk1::vector<unsigned_char,std::__ndk1::allocator<unsigned_char>>::vector((vector<unsigned_char,std::__ndk1::allocator<unsigned_char>>*)&local_70,(vector*)g_dirty);uVar3=is_under_frida();if((uVar3&1)==0){restore_png((vector*)&local_58,(vector*)g_dirty);if(local_70!=(void*)0x0){local_68=local_70;operator.delete(local_70);}local_70=local_88;local_68=local_80;}puVar2=(undefined8*)(**(code**)(*param_1+0x580))(param_1,(int)local_68-(int)local_70);lVar4=(**(code**)(*param_1+0x5c0))(param_1,puVar2,0);if(local_68==local_70){uVar5=0;}else{uVar3=0;do{*(undefined*)(lVar4+uVar3)=*(undefined*)((long)local_70+uVar3);uVar3=uVar3+1;uVar5=(long)local_68-(long)local_70;}while(uVar3<uVar5);}(**(code**)(*param_1+0x680))(param_1,puVar2,0,uVar5,lVar4);(**(code**)(*param_1+0x600))(param_1,puVar2,lVar4,0);if(local_70!=(void*)0x0){local_68=local_70;operator.delete(local_70);}if(local_58!=(undefined8*)0x0){local_50=local_58;operator.delete(local_58);}}}if(*(long*)(lVar1+0x28)!=local_38){/* WARNING: Subroutine does not return */__stack_chk_fail();}returnpuVar2;}
Ну вот! Начинается интересное! Оказывается в приложении была защита от фриды, которую я благополучно не заметил ;) Не будем расстраивать организаторов и героически наступим на эти грабли! Для простоты проведения эксперимента заведем приложение под objection и посмотрим что будет при получении флага.
Так-так-так =) Решили испортить картинку значит. Давайте посмотрим поближе как реализованы проверки на фриду.
undefined4is_under_frida(void){longlVar1;boolbVar2;...longlocal_70;lVar1=cRead_8(tpidr_el0);local_70=*(long*)(lVar1+0x28);pDVar4=opendir("/proc/self/task");if(pDVar4!=(DIR*)0x0){pdVar5=readdir(pDVar4);if(pdVar5==(dirent*)0x0){closedir(pDVar4);}else{bVar2=false;do{uStack136=0;...uStack352=0;iVar3=strcmp(pdVar5->d_name,".");if((iVar3!=0)&&(iVar3=strcmp(pdVar5->d_name,".."),iVar3!=0)){FUN_00161350(&local_170);iVar3=openat(-100,(char*)&local_170,0x80000,0);if(iVar3!=0){lVar9=0;lStack616=0;...uStack384=0;do{sVar6=read(iVar3,local_2f0,1);if((sVar6!=1)||(local_2f0[0]=='\n'))break;*(char*)((long)&local_270+lVar9)=local_2f0[0];lVar9=lVar9+1;}while(lVar9!=0xff);pcVar7=strstr((char*)&local_270,"gum-js-loop");if((pcVar7!=(char*)0x0)||(pcVar7=strstr((char*)&local_270,"gmain"),pcVar7!=(char*)0x0)){bVar2=true;}close(iVar3);}}pdVar5=readdir(pDVar4);}while(pdVar5!=(dirent*)0x0);iVar3=closedir(pDVar4);if(bVar2){uVar8=1;gotoLAB_00161318;}}}pDVar4=opendir("/proc/self/fd");if(pDVar4==(DIR*)0x0){uVar8=0;}else{pdVar5=readdir(pDVar4);uVar8=0;while(pdVar5!=(dirent*)0x0){_Stack648=0;...uStack608=0;FUN_00161350(&local_270);lstat((char*)&local_270,(stat*)local_2f0);if(((uint)local_2e0&0xf000)==0xa000){readlinkat(-100,(char*)&local_270,(char*)&local_170,0x100);pcVar7=strstr((char*)&local_170,"linjector");if(pcVar7!=(char*)0x0){uVar8=1;}}pdVar5=readdir(pDVar4);}}iVar3=closedir(pDVar4);LAB_00161318:if(*(long*)(lVar1+0x28)==local_70){returnuVar8;}/* WARNING: Subroutine does not return */__stack_chk_fail(iVar3);}
Нуууууу… ничего такого, с чем бы мы не встречались ранее. Тут должен сработать самый простой и топорный хак с подменой возвращаемого значения функции strstr
Запускаем приложение с этим скриптом и убеждаемся, что детектор фриды пал смертью храбрых, а при запуске activity отображается актуальная картинка.
Бонус №2
Стоит сказать, что изначальная формулировка задания была довольно неоднозначной и догадаться о том, что от текста на этой картинке нужно было взять хэш, да еще и в определенном формате написания - было решительно невозможно. После добавления уточнения от организаторов, все встало на свои места:
ПРИМЕЧАНИЕ: некая компания NewBrainIndustrial-E выпустила приложение, представляющее собой галерею цифровых произведений искусства, которые сгенерировал разработанный в этой компании искусственный интеллект. Приложение используется для рекламы компании и содержит также коммерческие предложения по приобретению подписки на ИИ для решения собственных задач. Есть подозрение, что у NewBrainIndustrial-E нет никакой ИИ-разработки, а картины принадлежат настоящему художнику. Необходимо найти доказательства и сообщить имя художника.
Ключ – это NQ2022+SHA256(ИмяФамилия)
Спасибо ребята =) Без этого примечания вы меня заставили вспомнить еще и стеганографию. Но обовсем по порядку.
Получив картинку на экране я предположил, что вся самая ценная информация зашита в самой картинке. Нужно было как-то ее извлечь и препарировать. Собственно в этот момент и возникла необходимость сделать обход фриды, о котором я писал выше. Для того чтобы достать картинку нам нужно перехватить вызов функции Java_com_newbrainindustrial_galery_LogoDecoderAIDL_getData и сохранить полученный массив как картинку. Для этого напишем немного замороченный скрипт на python:
importfrida,sysdefon_message(message,data):ifmessage['type']=='send':print("[*] {0}".format(message['payload']))else:print(message)jscode="""
'use strict';
let imageBuffer = new Uint8Array(1024);
let wastetime = true
rpc.exports = {
getimagebuffer: function() {
return imageBuffer;
},
wastingtime: function() {
return wastetime;
}
}
function bytes2hex(array) {
var result = '';
for(var i = 0; i < array.length; ++i)
result += ('0' + (array[i] & 0xFF).toString(16)).slice(-2);
return result;
}
Interceptor.attach(Module.findExportByName("libc.so", "strstr"), {
onEnter: function(args) {
this.haystack = Memory.readUtf8String(args[0])
this.isFrida = Boolean(0)
if (this.haystack.indexOf("gmain") !== -1 || this.haystack.indexOf("gum-js-loop") !== -1 || this.haystack.indexOf("linjector") !== -1) {
this.frida = Boolean(1);
}
},
onLeave: function(retval) {
if (this.frida) {
retval.replace(0);
}
return retval;
}
});
Interceptor.attach(Module.findExportByName("libgalery.so", "Java_com_newbrainindustrial_galery_LogoDecoderAIDL_getData"), {
onEnter: function(args) {
},
onLeave: function(retval) {
var b = Java.use('[B')
var buffer = Java.cast(retval, b);
var result = Java.array('byte', buffer);
imageBuffer = bytes2hex(result);
wastetime = false
return retval;
}
});
"""process=frida.get_usb_device().attach('NBI-E Gallery')script=process.create_script(jscode)script.on('message',on_message)print('[*] Wasting time...')script.load()jsexports=script.exportswhile(jsexports.wastingtime()):passbuff=jsexports.getimagebuffer()withopen("result.png","wb")asf:f.write(bytearray.fromhex(buff))print("Complete")
Чтобы вся эта добрая магия сработала, нам нужно убедиться, что весь нужный нам код уже находится в памяти. Для этого нужно один раз вызвать метод AIDL интерфейса чтобы “прогреть” всю машинерию. Потом не закрывая приложение запускаем этот скрипт и тригерим AIDL еще раз. В результате получим желанную картинку:
А потом я творил с этой картинкой такое, что я б не позволил печатать на месте цензуры… Естественно все было бесполезно :D Но один приемчик на будущее я вам все же покажу. В реальной жизни пригодится вряд-ли, а вот на CTF-ах вполне. Называется эта штука Steganography Toolkit и работать с ней нужно так: