This article was published on the 22nd of October 2018. This article was updated on the 30th of April 2020, as well as on the 11th of May 2020.
Platforms have different file formats for executable files, such as the PE file format for Windows, the Mach-O format for MacOS and the ELF format for Linux distributions. Android uses its own format: the Android PacKage which uses the APK extension. In this article, the lay-out of the Android Package is examined and information about the runtime environment is given. Additionally, a practical case is analysed and the thought process behind the taken steps is explained.
Table of contents
The Android PacKage
The APK file file is a compressed ZIP file, although not completely. Google altered the APK format in Android 7.0 and above, as is explained here. Within the APK file, there are multiple files and folders. The content of an APK is given below.
Meta-inf
This folder contains the signature of the APK file together with the MANIFEST.MF file which contains metadata about the application.
Lib
In here, the native libraries are stored. The names of the sub folders in this directory indicate the architecture of the given library. Note that the library is build from the same sources with a different compiler to function on a different architecture. These different architectures are provided in order to support a broad variety of mobile devices, some of which use ARM and some of which use a different architecture such as Intel x86 or Intel x86_64.
Res
All external resources that are used within the application are saved in this directory. As an example, one can think of logos or background images. In the sub directories, one can find different versions of the resources. Those versions differ in resolution for different phone (or tablet) screens.
Assets
Anything that does not belong in the above mentioned folders, is placed in the assets folder. Additional classes.dex files are often stored in this folder as well, regardless if they are encrypted or not. Another example of an asset is a database with information in it. Do note that this folder is read only within the application, which makes the database only usable to extract information from, as one can not write any data to it.
Classes.dex
This file contains all compiled classes (which contain the compiled byte code for the Dalvik Virtual Machine). This file can be decompiled to Java source code using tools such as JADX, or another decompiler.
AndroidManifest.xml
The manifest contains all the information regarding the required permissions, the version, and the referenced libraries. This file is in binary XML format, which is unreadable for humans unless it is decoded. In here, the starting class of the application is found together with the registered services and intent filters.
Resources.arsc
The project’s resources, such as views, are saved within this file during the compilation. Generally, this file contains only smaller resources since a programmer will likely put a big file in the asset folder to avoid always loading big chunks of data when only a small part of the loaded data is used.
Permissions
Unlike most desktop platforms, an Android application does not have much permissions by default. Additional permissions are defined in the AndroidManifest.xml file. Depending on the Android version, the permissions are either requested during the installation or during the run time of the application. To contact internet, the application requires the following permission:
<uses-permission android:name="android.permission.INTERNET" />
Since all of these permissions are saved within the manifest, one can review these before investigating an application. This gives an indication of the capabilities of the application. Most permissions are self explanatory whilst others require a quick search on this page.
The codebase
The code base of an Android application consists mainly of Java code. The Java code is compiled to SMALI code, which is byte code (similar to the Common Intermediate Language in the Dot Net Framework). The entry point of the code is within the MainActivity class, in the onCreate function. Note that the main activity of the application can have any name, but the function can not.
Additionally, one can add native code (C or C++) as a library using the Java Native Interface (JNI). Functions which are linked with the JNI have a specific naming convention: the name of all packages, the class and the function name, where dots are replaced with underscores. The function testFunction in the package com.maxkersten.test would have the following JNI name: com_maxkersten_test_testFunction.
Activities
An activity can be somewhat compared to an exported function of a library, since it invokes the application but not via the onCreate function of the main activity, which is the equivalent of the main function in C or Java. The main activity is used when the user opens the application from the home screen or application list. Upon sharing a link from a mobile browser, the user selects the application where (s)he would like to share it, in this example the chosen application is a chat application. This automatically opens the chat application on the screen where the user can select contacts to receive the link. This differs from simply opening the chat application and viewing the most recent conversations. Each of these elements (the share part and the default start part) is an activity, in which the default start part is most likely the main activity of the whole application, whereas the share part is most likely another activity.
Activity lifecycle
Applications are not used in a linear way, like most desktop applications: the application does not always open on the same screen after which the user can interact with the interface. Therefore, it is important for an application to keep on functioning when another application is opened or when the user switches between applications. Depending on the action, a different function is called. To start the activity, the onCreate function is called. After that, the onStart function is called. The onRestart, onResume, onPause, onStop, and onDestroy functions are called when required, with the purpose clearly stated in the function name.
One can (partially) compare this with threading in Java, in which the states of a thread are also caught within predefined functions. When a thread is interrupted, it waits. If the thread is allowed to resume, the function continues. In an activity, the onResume function is called before the activity continues.
Asynctask
The asynchronous task is used to perform actions that take a short period of time in a different thread, which avoids blocking the UI thread. The asynchronous task has, similar to the activities, multiple functions. These functions are, however, executed in a set order. Firstly, the onPreExecute is executed, which prepares the execution of the task. Secondly, the doInBackground function is automatically called after the first function. This is the main function of the task. Thirdly, the onProgressUpdate can be called during the execution to obtain information about the progress of the task. Lastly, the onPostExecute function is called. This function is automatically executed after the main function of the thread is finished.
Intents
Intents are used to address another part of the system or to use another application for something. If a user opens a link using a chat application which does have an activity to open webpages, an intent can be send out. There are two types of intents, both of which are listed below.
Implicit intents
An implicit intent requests the system to use a suited application to handle the action. If there is more than one application present, the user has to select the preferred application. This choice can be remembered by the system, to avoid repeatedly displaying the same dialog to the user. The choice can be altered in the settings.
Explicit intents
An explicit intent includes the application which should handle the action, leaving the user no choice. Often, explicit intents are used within the same application since all the required information is already known and the programmer does not want an another application to respond to an internal action.
Intent filters
Since implicit intents are broadcasted system wide, the system needs to know if an application can respond to an intent and with which activity. Setting intent filters in the AndroidManifest.xml of an application during the development solves this problem. It is therefore an important aspect to look at when analysing an Android application.
Shared Preferences
The shared preferences are persisted preferences with a key and a value. It is up to the programmer how to use these key-value pairs and what is stored in them. Generally, static code analysis provides insight in the stored data since the key is usually a string which indicates the purpose of a specific setting.
Services
A service is used to offer functionality to other applications or to use internally for longer tasks which require no user interaction. A service has no user interface by default. One can start a service together with the application, or when an intent is received.
Toast
A toast is a message which is displayed to the user in the bottom middle part of the screen. The display length can be set to either short or long, both of which can be defined by the programmer. Often, messages such as You are now connected to [SSIDName] are displayed using toast.
Practical case
The practical case is based on the Capture The Flag which was organised by HackerOne June 2018, as can be read in the announcement here. This CTF had one web challenges and multiple mobile challenges, which were all based on the Android platform. In this practical case, the first mobile challenge will be analysed step-by-step. The file can be downloaded here.
The APK can be decoded using APKTool. One can do this with command that is given below.
cd brut.apktool/apktool-cli/build/libs/ java -jar apktool-cli-all.jar d path/to/the/file.apk
This should give the following output:
libra@laptop:~/Downloads/h1-702/mobile/challenge1/Apktool/brut.apktool/apktool-cli/build/libs$ java -jar ./apktool-cli-all.jar d ../../../../../challenge1_release.apk I: Using Apktool 2.4.0-9fe399-SNAPSHOT on challenge1_release.apk I: Loading resource table... I: Decoding AndroidManifest.xml with resources... S: WARNING: Could not write to (/home/libra/.local/share/apktool/framework), using /tmp instead... S: Please be aware this is a volatile directory and frameworks could go missing, please utilize --frame-path if the default storage directory is unavailable I: Loading resource table from file: /tmp/1.apk I: Regular manifest package... I: Decoding file-resources... I: Decoding values */* XMLs... I: Baksmaling classes.dex... I: Copying assets and libs... I: Copying unknown files... I: Copying original files...
The output of APKTool is, by default, saved in the same directory as directory where the JAR is executed from. The name of the folder in which the output is saved equals the name of the file that was decoded, excluding the file extension.
The layout of the folder, excluding files, looks similar to the structure that is given below.
. ├── AndroidManifest.xml ├── apktool.yml ├── lib │ ├── arm64-v8a │ │ └── libnative-lib.so │ ├── armeabi │ │ └── libnative-lib.so │ ├── armeabi-v7a │ │ └── libnative-lib.so │ ├── mips │ │ └── libnative-lib.so │ ├── mips64 │ │ └── libnative-lib.so │ ├── x86 │ │ └── libnative-lib.so │ └── x86_64 │ └── libnative-lib.so ├── res │ ├── [omitted] └── smali ├── android │ ├── [omitted] └── com └── hackerone └── mobile └── challenge1 ├── BuildConfig.smali ├── FourthPart.smali ├── MainActivity.smali └── [omitted]
In the structure above, multiple folders and files are omitted for brevity’s sake. The AndroidManifest.xml contains the entry point of the application. Additionally, one can see that the lib folder contains multiple versions of the library named libnative-lib.so. The extension, so, stands for shared object, which is similar to the dynamic link library (dll) on Windows. A shared object file is only present if it is also used within the binary. One does not need to start with the analysis of the binary unless they are called from the Java (or smali) code.
Additionally, one can see that the smali files are split between two folders: the android and the com folders. These folders, and their subsequent children, are equal to the structure of the packages within the application. The android package can be ignored, since it contains the required base code that is used in the com package. Knowing this, the amount of files that need to be examined drastically decreases, leaving the analyst with only three smali files that require attention. One of these files has a name, FourthPart, which implies the existence of at least three other parts. What the parts contain, are unknown for now, but one can assume it is the flag. This assumption looks plausible because there is not much else within the application, other than the MainActivity and FourthPart classes.
Summary
Using the same approach as before, it is best to first summarise all facts to decide what the best approach is.
- Determine the entry point of the application
- Find out where the shared object binary is used
- Obtaining the fourth part
Finding the entry point
The starting point of the application can be found with the help of the manifest file. The content of the AndroidManifest.xml file is given below.
<?xml version="1.0" encoding="utf-8" standalone="no"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.hackerone.mobile.challenge1"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name="com.hackerone.mobile.challenge1.MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> </manifest>
As one can see, the activity com.hackerone.mobile.challenge1.MainActivity is defined as the android.intent.action.MAIN or, in other words, the main activity. The function which is first called in this class is named the onCreate function, which is also given below.
protected void onCreate(Bundle bundle) { super.onCreate(bundle); setContentView((int) R.layout.activity_main); ((TextView) findViewById(R.id.sample_text)).setText("Reverse the apk!"); doSomething(); }
Firstly, the parent class is called using super. The parent class is the class which is extended in this class, which can be seen below.
public class MainActivity extends AppCompatActivity {
Then the content view is set together with the layout of the main activity. The class R contains all resources that are embedded within the project. The text in the text view provides some insight and confirms this is the right track. Lastly, the function doSomething is called. The function is given below.
void doSomething() { Log.d("Part 1", "The first part of your flag is: \"flag{so_much\""); }
This function prints the first part of the flag to the log and proves the assumption that the parts make up the flag.
Finding the shared object
MainActivity class, the function native String stringFromJNI resides together with the public native void oneLastThing function. The call below that, loads the shared object, as can be seen in the code below.public native void oneLastThing(); public native String stringFromJNI(); static { System.loadLibrary("native-lib"); }
Using the GNU Strings binary, one can obtain all readable strings within a file. This provides the result for the second part: This is the second part: “_static_”. The flag so far is flag{so_much_static_.
Getting lucky with the GNU Strings binary does aid in quickly retrieving the required information. To fully understand what is happening, one has to look at the disassembly of the library. In this example, radare2 together with the r2dec plug-in will be used.
The binary is opened and automatically analysed with the following command:
r2 -A ./libnative-lib.so
Listing all functions shows the JNI functions, which all contain the package and function name of the Java function. This provides two results:
sym.Java_com_hackerone_mobile_challenge1_MainActivity_oneLastThing sym.Java_com_hackerone_mobile_challenge1_MainActivity_stringFromJNI
The disassembly of the oneLastThing function is nothing useful, as can be seen below.
/ (fcn) sym.Java_com_hackerone_mobile_challenge1_MainActivity_oneLastThing 10 | sym.Java_com_hackerone_mobile_challenge1_MainActivity_oneLastThing (); | 0x000044f0 55 push ebp | 0x000044f1 89e5 mov ebp, esp | 0x000044f3 83e4fc and esp, 0xfffffffc | 0x000044f6 89ec mov esp, ebp | 0x000044f8 5d pop ebp \ 0x000044f9 c3 ret
The first two lines are the function prologue, after which the stack is aligned. Then the old values of the registers are restored and the function returns. Using pdd this is also seen.
/* r2dec pseudo C output */ #include <stdint.h> void Java_com_hackerone_mobile_challenge1_MainActivity_oneLastThing (void) { }
This is even more clear if one decides the analyse the x86_64 library. There is no change in the stack frame here, since the first few arguments for a function are passed through registers in the x86_64 architecture. Therefore, there is no function prologue or stack alignment necessary, only a ret instruction to return from the function and set the instruction pointer to the next instruction.
/ (fcn) sym.Java_com_hackerone_mobile_challenge1_MainActivity_oneLastThing 1 | sym.Java_com_hackerone_mobile_challenge1_MainActivity_oneLastThing (); \ 0x000073f0 c3 ret
The output of r2dec is the same here, as it should be since all the libraries are compiled using the same source.
#include <stdint.h> void Java_com_hackerone_mobile_challenge1_MainActivity_oneLastThing (void) { }
Moving on to the other JNI function within the x86_64 library: sym.Java_com_hackerone_mobile_challenge1_MainActivity_stringFromJNI. This function contains a lot more information.
/ (fcn) sym.Java_com_hackerone_mobile_challenge1_MainActivity_stringFromJNI 163 | sym.Java_com_hackerone_mobile_challenge1_MainActivity_stringFromJNI (int arg1); | ; var int local_8h @ rsp+0x8 | ; var int local_10h @ rsp+0x10 | ; var unsigned int local_18h @ rsp+0x18 | ; arg int arg1 @ rdi | 0x00007250 53 push rbx | 0x00007251 4883ec20 sub rsp, 0x20 | 0x00007255 4889fb mov rbx, rdi ; arg1 | 0x00007258 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=0x2a328 ; '(' | 0x00007261 4889442418 mov qword [local_18h], rax | 0x00007266 488d35b38e01. lea rsi, str.This_is_the_second_part:___static ; section..rodata ; 0x20120 ; "This is the second part: \"_static_\"" | 0x0000726d 488d7c2408 lea rdi, [local_8h] | 0x00007272 488d542410 lea rdx, [local_10h] ; 0x10 | 0x00007277 e894150000 call sub.strlen_810 | 0x0000727c 488b742408 mov rsi, qword [local_8h] ; [0x8:8]=0 | 0x00007281 488b03 mov rax, qword [rbx] | 0x00007284 4889df mov rdi, rbx | 0x00007287 ff9038050000 call qword [rax + 0x538] | 0x0000728d 4889c3 mov rbx, rax | 0x00007290 488b442408 mov rax, qword [local_8h] ; [0x8:8]=0 | 0x00007295 488d78e8 lea rdi, [rax - 0x18] | 0x00007299 483b3d483802. cmp rdi, qword [0x0002aae8] ; section..got ; [0x2aae8:8]=0x2b180 section..bss | ,=< 0x000072a0 7519 jne 0x72bb | | ; CODE XREFS from sym.Java_com_hackerone_mobile_challenge1_MainActivity_stringFromJNI (0x72d1, 0x72e0, 0x72ec) | ...--> 0x000072a2 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=0x2a328 ; '(' | :::| 0x000072ab 483b442418 cmp rax, qword [local_18h] ; [0x18:8]=0 | ,=====< 0x000072b0 753c jne 0x72ee | |:::| 0x000072b2 4889d8 mov rax, rbx | |:::| 0x000072b5 4883c420 add rsp, 0x20 | |:::| 0x000072b9 5b pop rbx | |:::| 0x000072ba c3 ret | |:::| ; CODE XREF from sym.Java_com_hackerone_mobile_challenge1_MainActivity_stringFromJNI (0x72a0) | |:::`-> 0x000072bb 48833d2d3802. cmp qword reloc.pthread_create, 0 | |:::,=< 0x000072c3 7410 je 0x72d5 | |:::| 0x000072c5 b9ffffffff mov ecx, 0xffffffff ; -1 | |:::| 0x000072ca f00fc148f8 lock xadd dword [rax - 8], ecx | |:::| 0x000072cf 85c9 test ecx, ecx | |`====< 0x000072d1 7fcf jg 0x72a2 | |,====< 0x000072d3 eb0d jmp 0x72e2 | ||::| ; CODE XREF from sym.Java_com_hackerone_mobile_challenge1_MainActivity_stringFromJNI (0x72c3) | ||::`-> 0x000072d5 8b48f8 mov ecx, dword [rax - 8] | ||:: 0x000072d8 8d51ff lea edx, [rcx - 1] | ||:: 0x000072db 8950f8 mov dword [rax - 8], edx | ||:: 0x000072de 85c9 test ecx, ecx | ||`===< 0x000072e0 7fc0 jg 0x72a2 | || : ; CODE XREF from sym.Java_com_hackerone_mobile_challenge1_MainActivity_stringFromJNI (0x72d3) | |`----> 0x000072e2 488d742410 lea rsi, [local_10h] ; 0x10 | | : 0x000072e7 e8e4470000 call sub._ZdlPv_120_ad0 | | `==< 0x000072ec ebb4 jmp 0x72a2 | | ; CODE XREF from sym.Java_com_hackerone_mobile_challenge1_MainActivity_stringFromJNI (0x72b0) \ `-----> 0x000072ee e80df8ffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
For the flag, one has to look no further than the first call instruction, which is higlighted below.
/ (fcn) sym.Java_com_hackerone_mobile_challenge1_MainActivity_stringFromJNI 163 | sym.Java_com_hackerone_mobile_challenge1_MainActivity_stringFromJNI (int arg1); | ; var int local_8h @ rsp+0x8 | ; var int local_10h @ rsp+0x10 | ; var unsigned int local_18h @ rsp+0x18 | ; arg int arg1 @ rdi | 0x00007250 53 push rbx | 0x00007251 4883ec20 sub rsp, 0x20 | 0x00007255 4889fb mov rbx, rdi ; arg1 | 0x00007258 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=0x2a328 ; '(' | 0x00007261 4889442418 mov qword [local_18h], rax | 0x00007266 488d35b38e01. lea rsi, str.This_is_the_second_part:___static ; section..rodata ; 0x20120 ; "This is the second part: \"_static_\"" | 0x0000726d 488d7c2408 lea rdi, [local_8h] | 0x00007272 488d542410 lea rdx, [local_10h] ; 0x10 | 0x00007277 e894150000 call sub.strlen_810
Before the call, the registers rsi, rdi and rdx are used to store the arguments for the call to the function sub.strlen_810. A sub does not have a return value, so it is only important if it were to contain a key piece of information. There is no need to examine this function, as the flag is already given in the register rsi: This is the second part: “_static”.
Determining the fourth part
Inspecting the class FourthPart class, the part is not directly given. Instead, the part is mangled into multiple pieces which are each stored in a different function within this class. The decompiled class is given below.
package com.hackerone.mobile.challenge1; public class FourthPart { String eight() { return "w"; } String five() { return "_"; } String four() { return "h"; } String one() { return "m"; } String seven() { return "o"; } String six() { return "w"; } String three() { return "c"; } String two() { return "u"; } }
One can either assemble the characters manually or use an IDE to print all characters in the correct order: much_wow.
Summarise again
Although this might sound tedious, it is important to summarise the facts before one continues. Make a habit out of it to routinely summarise your progress every once in a while. This prevents getting stuck on something you actually already knew but missed in the chaos.
- The flag consists of minimally five parts, since part three is missing and part four does not end with a }
- Look in the strings.xml file
- Maybe the library contains more information
Look in the strings.xml file
The strings.xml file is located in resources/values folder. Until now, all parts of the flag contained the word part. Using GNU Cat and GNU Grep, the third part is quickly found in the strings.xml file.
libra@laptop:~/h1-702/challenge1_release/res/values$ strings ./strings.xml | grep "part" <string name="part_3">part 3: analysis_</string>
The flag, parts one through four, equals flag{so_much_static_analysis_much_wow.
Revisiting the library
A binary can contain much more than the JNI functions that were observed earlier. There was no reason to analyse the binary further since the sole load that was given, was the JNI. Since all leads are no effectively over, besides this one, it is time to see what other functions are exported from the library. Within radare2, the command iE is used to do so. Note that the other exported functions are omitted for brevity.
[0x000073e0]> iE [Exports] Num Paddr Vaddr Bind Type Size Name 003 0x000073f0 0x000073f0 GLOBAL FUNC 1 Java_com_hackerone_mobile_challenge1_MainActivity_oneLastThing 004 0x00007250 0x00007250 GLOBAL FUNC 252 Java_com_hackerone_mobile_challenge1_MainActivity_stringFromJNI 005 0x000073e0 0x000073e0 GLOBAL FUNC 3 b() 006 0x000073a0 0x000073a0 GLOBAL FUNC 3 l() 007 0x00007390 0x00007390 GLOBAL FUNC 3 m() 008 0x00007380 0x00007380 GLOBAL FUNC 3 q() 009 0x000073b0 0x000073b0 GLOBAL FUNC 3 t() 010 0x00007360 0x00007360 GLOBAL FUNC 3 z() 011 0x00007350 0x00007350 GLOBAL FUNC 3 cr() 012 0x000073d0 0x000073d0 GLOBAL FUNC 3 ir() 013 0x00007370 0x00007370 GLOBAL FUNC 3 pr() 014 0x000073c0 0x000073c0 GLOBAL FUNC 3 sr() [...]
Whereas the Java_com_hackerone_mobile_challenge1_MainActivity_stringFromJNI function had a size of 252 instructions, the other semi readable functions all have the same size: 3 instructions. Due to the length (or rather the lack thereof) of the function, it is worth to take a look at them without having much clues as to why, other than the fact that it is an exported function.
The function b corresponds with the function name sym.b. The disassembly of b provides enough information to know this is the right track.
/ (fcn) sym.b 3 | sym.b (); | 0x000073e0 b07d mov al, 0x7d ; '}' \ 0x000073e2 c3 ret
The character } is returned in the lower 8 bits of the accumulating register, which is the final character of the flag. This also indicates that the fifth part is the final part.
The disassembly of all functions is given below.
[0x000073a0]> pdf @ sym.b / (fcn) sym.b 3 | sym.b (); | 0x000073e0 b07d mov al, 0x7d ; '}' \ 0x000073e2 c3 ret [0x000073a0]> pdf @ sym.l / (fcn) sym.l 3 | sym.l (); | 0x000073a0 b063 mov al, 0x63 ; 'c' \ 0x000073a2 c3 ret [0x000073a0]> pdf @ sym.m / (fcn) sym.m 3 | sym.m (); | 0x00007390 b05f mov al, 0x5f ; '_' \ 0x00007392 c3 ret [0x000073a0]> pdf @ sym.q / (fcn) sym.q 3 | sym.q (); | 0x00007380 b064 mov al, 0x64 ; 'd' \ 0x00007382 c3 ret [0x000073a0]> pdf @ sym.t / (fcn) sym.t 3 | sym.t (); | 0x000073b0 b06f mov al, 0x6f ; 'o' \ 0x000073b2 c3 ret [0x000073a0]> pdf @ sym.z / (fcn) sym.z 3 | sym.z (); | 0x00007360 b061 mov al, 0x61 ; 'a' \ 0x00007362 c3 ret [0x000073a0]> pdf @ sym.cr / (fcn) sym.cr 3 | sym.cr (); | 0x00007350 b05f mov al, 0x5f ; '_' \ 0x00007352 c3 ret [0x000073a0]> pdf @ sym.ir / (fcn) sym.ir 3 | sym.ir (); | 0x000073d0 b06c mov al, 0x6c ; 'l' \ 0x000073d2 c3 ret [0x000073a0]> pdf @ sym.pr / (fcn) sym.pr 3 | sym.pr (); | 0x00007370 b06e mov al, 0x6e ; 'n' \ 0x00007372 c3 ret [0x000073a0]> pdf @ sym.sr / (fcn) sym.sr 3 | sym.sr (); | 0x000073c0 b06f mov al, 0x6f ; 'o' \ 0x000073c2 c3 ret
The obtained characters are listed below.
} c _ d o a _ l n o
It is known that the flag ends with a bracket (}). Additionally, words are seperated with an underscore (_) instead of a space. Since there are two underscores, there are two words in this part of the flag. The rest of the flag is written in the style of the Doge meme. Words such as many and wow are often used in here. Simple English words which are common and not too old. Additionally, one can assume that (given that the last words is wow) a coordinating conjunction is used to connect two words. The first word one can make is and, which is a coordinating conjunction. The remaining letters then form another word: cool. In total, the last part of the flag equals _and_cool}.
The final flag
The complete flag equals flag{so_much_static_analysis_much_wow_and_cool}.
To contact me, you can e-mail me at [info][at][maxkersten][dot][nl], send me a PM on Reddit, or DM me on Twitter @Libranalysis.