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.

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.

This folder contains the signature of the APK file together with the MANIFEST.MF file which contains metadata about the application.

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.

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.

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.

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.

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.

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.


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.

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.

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 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.

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.

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.

Using APKTool (which is described in the set-up article), the APK can be decoded. 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
│   │   └──
│   ├── armeabi
│   │   └──
│   ├── armeabi-v7a
│   │   └──
│   ├── mips
│   │   └──
│   ├── mips64
│   │   └──
│   ├── x86
│   │   └──
│   └── x86_64
│       └──
├── 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 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.


Using the same approach as before, it is best to first summarise all facts to decide what the best approach is.

  1. Determine the entry point of the application
  2. Find out where the shared object binary is used
  3. 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="" package="">
    <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="">
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>

As one can see, the activity 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) {
    setContentView((int) R.layout.activity_main);
    ((TextView) findViewById("Reverse the apk!");

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
The shared object is used in combination with the Java Native Interface (JNI). In the 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 {

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 ./

Listing all functions shows the JNI functions, which all contain the package and function name of the Java function. This provides two results:


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] ; ; [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.

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
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 @
/ (fcn) 3
| ();
|           0x00007350      b05f           mov al, 0x5f                ; '_'
\           0x00007352      c3             ret
[0x000073a0]> pdf @
/ (fcn) 3
| ();
|           0x000073d0      b06c           mov al, 0x6c                ; 'l'
\           0x000073d2      c3             ret
[0x000073a0]> pdf @
/ (fcn) 3
| ();
|           0x00007370      b06e           mov al, 0x6e                ; 'n'
\           0x00007372      c3             ret
[0x000073a0]> pdf @
/ (fcn) 3
| ();
|           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 @LibraAnalysis.