Flare-On 2020 Solutions Write-Ups

This contains write-ups with my solutions for Flare-On 2020 that was hosted by FireEye from September 11 to October 23 2020.

Download the binaries here.

Table of Contents

1 – Fidler

Time spent: around 10 minutes

Tools used: Python

The first challenge of the anual flare-on CTF always starts relatively easy and this year is no exception. We are given a game written in Python, and a message that tells us to beat the game to reveal the flag.

The password screen

Starting up the main python script fidler.py prompts us with the following password screen:

Figure 1

The first question that you should ask yourself here, is which parts of the code is responsible for prompting this dialog, and checking whether the input password is correct or not? Let’s have a look at the python script. Below are the relevant parts:

def password_check(input):
    altered_key = 'hiptu'
    key = ''.join([chr(ord(x) - 1) for x in altered_key])
    return input == key

def password_screen():
    # ...

    while not done:
        # ...

        if input_box.submitted:
            if password_check(input_box.text):
                return True
            else:
                return False

        # ...

# ...

def main():
    if password_screen():
        game_screen()
    else:
        password_fail_screen()
    pg.quit()

if __name__ == '__main__':
    main()

Immediately we can see that the script does very little to protect itself. We can see that main first shows the password screen, and if that succeeds, then the main game screen is opened. Looking into password_screen, we can see that the contents of the input_box is fed into password_check, which returns true if the input text is some calculated value. Although we cannot see the key directly, we can quickly find out what the contents should be, by simply copying the password check code to a new file, and changing the last return with a print.

altered_key = 'hiptu'
key = ''.join([chr(ord(x) - 1) for x in altered_key])
print(key)

This gives us:

ghost

Typing this into the real game results in unlocking the actual game.

Beating the game

The main game is a simple autoclicker game, and it tells use to get to 100 billion coins to win.

Figure 2

Obviously, we don’t want to wait for 100 billion coins, so let’s figure out how we can trick the game into thinking that we already have the required amount of coins.

Looking into the games main screen code, we can see something interesting:

ef game_screen():
    # ...

    while not done:
        target_amount = (2**36) + (2**35)
        if current_coins > (target_amount - 2**20):
            while current_coins >= (target_amount + 2**20):
                current_coins -= 2**20
            victory_screen(int(current_coins / 10**8))
            return
        # ...

In the above, we can see that victory_screen is called when current_coins > (target_amount - 2**20). To trick the game into thinking we already won, we can insert the following line just before the if statement:

current_coins = target_amount

Running the app one final time, automatically reveals the flag:

Figure 3

2 – garbage

Time spent: around 20 minutes

Tools used: CFF Explorer, HxD, UPX

The second challenge of flare-on 2020 can either be very easy, or very difficult if you don’t know what you should be looking for. You are given a file called garbage.exe, along with a message that tells you the executable was recovered using digital forensics, but is incomplete. The task is to repair it and acquire the flag.

I heard a lot of people got stuck on this one, which is very understandable if you are not very familiar with the PE file format. Let’s go through it step-by-step.

Orientation

The first thing to try, is to just run it. As you might have guessed, this does not work.

Opening it up in a tool such as CFF explorer, however, gives us some clues. Looking at the sections of the executable, we can see the names UPX0 and UPX1:

Figure 1

UPX stands for the Ultimate Packer for eXecutables, and is a well-known packer that attempts to compress an executable file to a smaller size. It can be downloaded at https://upx.github.io/.

The cool thing about the UPX project is that it not only comes with a compressor, but also a built-in decompressor, by specifying the -d commandline flag. Unfortunately for us, this does not seem to work that well:

D:\Washi\RE\flareon2020\02>upx.exe -d garbage.exe
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2020
UPX 3.96w       Markus Oberhumer, Laszlo Molnar & John Reiser   Jan 23rd 2020

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
upx: garbage.exe: OverlayException: invalid overlay size; file is possibly corrupt

It seems some of the PE headers are corrupt. It complains about some size being incorrect.

“Fixing” the sections

When a file is incomplete, it usually means it is too short. Looking again at the sections, we can see that the last section .rsrc starts at file offset 0x9E00, and has a size of 0x400. However, if we look in a hex editor, such as HxD, we can quickly see that some of the section’s data got cut off:

Figure 2

We are missing a total of 0x400 – 0x124 = 0x2DC bytes. Let’s append 0x2DC 00 bytes to the end of the file. Let’s run UPX again.

D:\Washi\RE\flareon2020\02>upx.exe -d garbage2.exe
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2020
UPX 3.96w       Markus Oberhumer, Laszlo Molnar & John Reiser   Jan 23rd 2020

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
     79360 <-     41472   52.26%    win32/pe     garbage2.exe

Unpacked 1 file.

This time around, it does not complain. Success!

“Fixing” resources

Except there is a problem. Running the application results in the following error:

Figure 3

It complains about “side-by-side configuration”. This is probably because while we did fill up the resources section up to the right size, we didn’t really fill it up with the right data. There’s two ways of going about this: Either we can fix the resources directory by coming up with the right contents, or just remove it entirely and guess that the program doesn’t really need the resources at all.

I chose the latter one :). This can be done by simply clearing out the directory entry in the optional header of the PE:

Figure 4

Save it, and run it. Should be fine now right?

Fixing imports

Nope, we are still greeted with an error message:

Figure 5

.DLL? That’s a weird file name! Let’s have a look at the libraries that this PE file imports.

Figure 6

No wonder it could not find the imported DLLs. The module names are cleared out! We can easily figure out the original names of these modules by simply doing a quick Google search for one of the imported functions for each and every import. The module names are “kernel32.dll” and “shell32.dll”.

Fixing relocations

The application still does not run as expected, but it does not give an error anymore. Looking again at the sections and the data directories, we can observe one final issue.

Figure 7

The binary contains a section called .reloc, which is used for storing base relocations. However, if a binary contains relocations, it must contain a data directory entry in the optional header for it. But if we look into the optional header, we see it is cleared out:

Figure 8

Copying over the virtual address and the size from the .reloc section to the appropriate data directory fixes the binary, and the app is runnable.

Getting the flag

Turns out, this is all we need to do. The final application drops a vbs script and executes it. This script prompts the flag:

Figure 9

3 – wednesday

Time spent: 2 hours

Tools used: Ghidra, x64dbg, Cyberchef

The third challenge is another game. In the game, you play as a frog on an obstacle course, where you can either duck under or jump over each obstacle on the track.

Figure 1

Orientation

If you play the game a few times, you will quickly notice you’ll die quite often randomly, even if it looks like you ducked in time or jumped over the obstacle correctly. It seems the game lacks some visual cues on whether to duck or jump for every obstacle.

If we open the binary in Ghidra, we also notice that our binary is most likely not obfuscated. It has a lot of symbols present, and a huge list of strings that are used throughout the game. The problem however is that it is a game, and the source code of a game is substantially larger than a typical crackme / keygenme. Most of the code is probably relevant to drawing and updating the game, and therefore not really relevant to us at all. The challenge of this task is to figure out what is important to us, and what is not.

Naturally, we want to try to beat the game and hope that there is a flag at the end of it all. I started out by looking for code that updates the score. In particular, when it resets the score after colliding with one of the obstacles (visible or invisible).

We know that the game draws the text “Score: #” on our screen. Let’s see if we can find some references to it. If we have a look at the strings that are present, we can see some interesting things:

Figure 2

The last one seems to not only just print “Score:” but also a hardcoded 0. Let’s have a look at references to this string:

Figure 3

This is convenient. Since symbols are not stripped from this executable, we can quickly see that this string is used upon initialization of the game, as well as another function which resets everything. A good guess would be that the latter one is called when the game has decided you lost. Let’s have a look:

void __fastcall @[email protected](int param_1)
{
    /* ... */

    // Reset score.
    _score__h34o6jaI3AO6iOQqLKaqhw = 0;
    __prev_score__55xT1lC51wWU8x2SoheEqg = 0;
    @[email protected](*(param_1 + 0x34),&_TM__V45tF8B8NBcxFcjfe7lhBw_2,0);
    
    // Reset day_index.
    _day_index__HImZp3MMPNE3pGzeJ4pUlA = 0;
    
    /* ... */
    
    // Reset obstacle 1.
    @[email protected]
              (*(param_1 + 0x3c),0,0x40845000,0,0x40668000,
               _obstacles__Xqz7GG9aS72pTPD9ceUjZPNg[_day_index__HImZp3MMPNE3pGzeJ4pUlA + 8]);

    /* ... */

    _day_index__HImZp3MMPNE3pGzeJ4pUlA = _day_index__HImZp3MMPNE3pGzeJ4pUlA + 1;
    
    /* ... */
    
    // Reset obstacle 2.
    @[email protected]
              (*(param_1 + 0x40),0,0x408ef000,0,0x40668000,
               _obstacles__Xqz7GG9aS72pTPD9ceUjZPNg[_day_index__HImZp3MMPNE3pGzeJ4pUlA + 8]);

    /* ... */
}

In this function, a lot of things happen, most of which is not really important to us. The key take away from this function however, is that it tells us something about a lot of important variables that we can start cross-referencing on:

  • _score__h34o6jaI3AO6iOQqLKaqhw: The current score.
  • __prev_score__55xT1lC51wWU8x2SoheEqg: The previous score.
  • _obstacles__Xqz7GG9aS72pTPD9ceUjZPNg: Some data related to obstacles.
  • _day_index__HImZp3MMPNE3pGzeJ4pUlA: An index that is used to access elements inside the obstacles array.

We can also see in the cross references of this resetEverything function, that this function is called in one of the update functions of the game. Let’s have a look at this function:

void __thiscall @[email protected](void *this,double param_1)
{
    /* ... */

    if (*(*(this + 0x28) + 0xf9) == '\x01') {
        @[email protected](this);
    }

    /* ... */
 
    if (-(uVar9 * 0.50000000) < *(*(this + 0x40) + 0x48)) goto LAB_00433e79;

    // Update day index.
    uVar8 = _day_index__HImZp3MMPNE3pGzeJ4pUlA + 1;
    /* ... */
    _day_index__HImZp3MMPNE3pGzeJ4pUlA = uVar8;

    /* ... */

    // Load next obstacle.
    @[email protected]
              (*(this + 0x3c),0,0x40845000,0,0x40668000,
               _obstacles__Xqz7GG9aS72pTPD9ceUjZPNg[_day_index__HImZp3MMPNE3pGzeJ4pUlA + 8]);

    /* ... */

    uVar8 = _day_index__HImZp3MMPNE3pGzeJ4pUlA + 1;
    /* ... */
    _day_index__HImZp3MMPNE3pGzeJ4pUlA = uVar8;
    
    /* ... */

    // Load second next obstacle.
    @[email protected]
              (*(this + 0x40),0,0x408ef000,0,0x40668000,
               _obstacles__Xqz7GG9aS72pTPD9ceUjZPNg[_day_index__HImZp3MMPNE3pGzeJ4pUlA + 8]);
    
    /* ... */

    // Check if score needs to be updated.
LAB_00433e79:
    if (__prev_score__55xT1lC51wWU8x2SoheEqg != _score__h34o6jaI3AO6iOQqLKaqhw) {
        __prev_score__55xT1lC51wWU8x2SoheEqg = _score__h34o6jaI3AO6iOQqLKaqhw;

        // Update score label.
        puVar4 = @[email protected](_score__h34o6jaI3AO6iOQqLKaqhw);
        if (puVar4 == 0x0) {
            /* ... */
        }
        else {
            puVar5 = @[email protected](*puVar4 + 7);
            uVar8 = *puVar5;
            *(puVar5 + uVar8 + 8) = 0x726f6353; // "Scor"
            *(puVar5 + uVar8 + 0xc) = 0x203a65; // "e: "
            *puVar5 = uVar8 + 7;
            _memcpy(puVar5 + uVar8 + 0xf,puVar4 + 2,*puVar4 + 1);
            *puVar5 = *puVar5 + *puVar4;
        }
        @[email protected](*(this + 0x34),puVar5,0);

        // Update high score and high score label if necessary:
        if (__high_score__zZRxWe9cBeocEphfWmZaLtA < _score__h34o6jaI3AO6iOQqLKaqhw) {
            __high_score__zZRxWe9cBeocEphfWmZaLtA = _score__h34o6jaI3AO6iOQqLKaqhw;
            puVar4 = @[email protected](_score__h34o6jaI3AO6iOQqLKaqhw);
            if (puVar4 == 0x0) {
                /* ... */
            }
            else {
                puVar5 = @[email protected](*puVar4 + 0xc);
                puVar1 = puVar5 + *puVar5 + 8;
                *puVar1 = 0x68676948;   // "High"
                puVar1[1] = 0x6f635320; // " Sco"
                puVar1[2] = 0x203a6572; // "re: "
                *(puVar1 + 3) = 0;
                uVar8 = *puVar5;
                *puVar5 = uVar8 + 0xc;
                _memcpy(puVar5 + uVar8 + 0x14,puVar4 + 2,*puVar4 + 1);
                *puVar5 = *puVar5 + *puVar4;
            }
            @[email protected](*(this + 0x38),puVar5,0);
        }
    }

    // Win condition:
    piVar2 = *(*(this + 0x28) + 0xfc);
    if ((piVar2 != 0x0) && (*piVar2 == 0x128)) {
        // Switch to win scene.
        @[email protected](_game__7aozTrKmb7lwLeRmW9a9cs9cQ,_winScene__eVaCVkG1QBiYVChMxpMGBQ);
    }
    return;
}

Again, a very large function for which most of the code is not really relevant to us. The key take away again is how the previously mentioned variables are used and updated. Similar to the resetEverything function, we can see that _day_index is increased twice. We can also see that a reset function is called twice. Towards the end, we see a big if statement that handles updating the score labels, and finally we see a very interesting if statement, which is the deciding factor on whether the game should switch to the win scene or not.

Letting the game think we have won

My first attempt to win the game was making two small changes in the program. The first change is to patch out the if statement that decides on whether to reset the game (0x00433d5c), and patching out the condition where we check whether we won the game or not (0x00433fa7-0x00433fb3).

This did let me “win” the game immediately, but unfortunately this didn’t seem to be enough. We are greeted with the following screen:

Figure 4

Let’s be a bit more smart about this

Maybe the program requires you to actually win the game by successfully avoiding all obstacles (who would have thought?!). I decided to look at how obstacles are actually generated. For this, this reset function that we have seen a few times already seemed interesting. Let’s have a look:

void __thiscall
@[email protected]
          (void *this,undefined4 param_1,undefined4 param_2,undefined4 param_3,undefined4 param_4,char obstacleData)

{
    undefined8 uVar1;
    
    *(this + 0x18) = 0;
    *(*(this + 0xf8) + 0x18) = 0;
    @[email protected](this, obstacleData);
    *(this + 0x48) = param_1;
    *(this + 0x4c) = param_2;
    *(this + 0x50) = param_3;
    *(this + 0x54) = param_4;
    *(*(this + 0xf8) + 0xf8) = obstacleData;
    uVar1 = @[email protected](*(this + 0x1c));
    *(*(this + 0xf8) + 0x48) = uVar1 * 0.50000000 + CONCAT44(param_2,param_1);
    *(*(this + 0xf8) + 0x50) = 0;
    return;
}

A lot of noise in this function that is related to objects. What we can see though, is that the obstacle data is passed onto a call to assignDay. Let’s follow the trail:

void __fastcall @[email protected](void *param_1,char obstacleData)
{
    /* ... */
    
    if (obstacleData == '\x01') {
        ppiVar5 = @[email protected](&TABLE_1,3);
    }
    else {
        ppiVar5 = @[email protected](&TABLE_2,3);
    }
    puVar3 = @[email protected](_gfxData__TftuyNzrt9cB3jrOs59bhBFw,ppiVar5);
  
    /* ... */

    *(param_1 + 0x1c) = puVar1;
    uVar6 = @[email protected](puVar1);
    @[email protected](param_1,uVar6,uVar6 >> 0x20,0,0,0,0);
    
    /* ... */
}

From this function, we can finally see how the obstacle data is used. We see that the obstacle data is nothing more than a single char, and depending on the contents of this char, it calls the sample function on either TABLE_1 or TABLE_2. In particular, it looks like the obstacle data is either 1 or not 1 (0? is this binary?). After that, it ends up in a call to initSprite. Looking at these two tables in Ghidra, we see it is a table of strings…

Figure 5

… which match very conveniently some of the file names in the gfx directory of the game:

Figure 6

Tying it together

We now know that our obstacles array is nothing more than an array of characters, and that the actual obstacles are generated based on this array. Let’s have a look at its raw contents:

Figure 7

That data looks awfully lot like a binary string, prepended by a length. Let’s copy the bytes as a hex string, throw it in cyberchef, filter out the leading zero, and convert from binary:

Figure 8

.. revealing the flag.

4 – report

Time spent: 2 hours

Tools used: Office Excel 2010, Python

For the fourth challenge in the series, you are given nothing more than an Excel sheet called report.xls. The message tells us that the file is infected, and that we should have a look at it.

Orientation

Opening the report, we are greeted with a message that the document was made using an “older version of Office”, and that we should click on “Enable content” to enable the totally-not-a-virus macros.

Figure 1

Newer versions of Office already notice that something is wrong with this document, and warn us to not enable any of its progrmamable content. Of course, we are stubborn and we do it anyway.

We immediately notice that the script fails to run properly, complaining about some invalid procedure call:

Figure 2

And we are transferred to the VBA editor:

Figure 3

Nothing seems to be out of the ordinary in terms of syntax errors. Odd! If we look at the VBA project structure, we see it consists of a couple of VBA script files called ThisWorkbook, and Sheet1 (a copy can be found here). Furthermore, we also see it contains a form with a weird label and textbox:

Figure 3

Recreating the script in Python

Since the script doesn’t run, let’s try to recreate it. In ThisWorkbook1 we see that folderol of Sheet1 is our main procedure of the script. We immediately see the script is obfuscated, but not heavily. In particular, a lot of the strings seem to be stored in the onzo variable, and are decoded using the rigmarole function. Looking at line 38, onzo is nothing more than the contents of the L label in our form, splitted by ..

onzo = Split(F.L, ".")

Let’s reimplement the decoder using Python and build up a strings table:

def rigmarole(d): 
    result = ""
    for i in range(0, len(d), 4):
        c1 = int(d[i:i+2], 16)
        c2 = int(d[i+2:i+4], 16)
        c = c1 - c2 
        result += chr(c)
    return result 

with open("F.L.txt", "r") as f:
    data = f.read().split('.')
    
for j in range(len(data)):
    print(j, rigmarole(data[j]))

This gives us:

0 AppData
1 \Microsoft\stomp.mp3
2 play
3 FLARE-ON
4 Sorry, this machine is not supported.
5 FLARE-ON
6 Error
7 winmgmts:\\.\root\CIMV2
8 SELECT Name FROM Win32_Process
9 vbox
10 WScript.Network
11 \Microsoft\v.png

Now we can replace all occurrences of rigmarole with the strings. Here is the first part:

If GetInternetConnectedState = False Then
    MsgBox "Cannot establish Internet connection.", vbCritical, "Error"
    End
End If

Set fudgel = GetObject("winmgmts:\\.\root\CIMV2")
Set twattling = fudgel.ExecQuery("SELECT Name FROM Win32_Process", , 48)
For Each p In twattling
    Dim pos As Integer
    pos = InStr(LCase(p.Name), "vmw") + InStr(LCase(p.Name), "vmt") + InStr(LCase(p.Name), "vbox"))
    If pos > 0 Then
        MsgBox "Sorry, this machine is not supported.", vbCritical, rigmarole(onzo(6))
        End
    End If
Next

The code above checks for internet connection (which a lot of automatic malware analysis tools disable), and secondly it looks for a list of known processes that are running when using a virtual machine. If any of these checks are met, the program exits. This can therefore be classified as an anti analysis technique, and can be ignored.

Then we get to our second part:

xertz = Array(&H11, &H22, &H33, &H44, &H55, &H66, &H77, &H88, &H99, &HAA, &HBB, &HCC, &HDD, &HEE)

wabbit = canoodle(F.T.Text, 0, 168667, xertz)
mf = Environ("AppData") & "\Microsoft\stomp.mp3"
Open mf For Binary Lock Read Write As #fn
    Put #fn, , wabbit
Close #fn

mucolerd = mciSendString("play " & mf, 0&, 0, 0)

We see that it grabs the contents of the textbox on our form, feeds it into the canoodle function together with some hardcoded array, and then writes it to a file in AppData called stomp.mp3. Looking at canoodle reveals it is not much more than interpreting every 4th and 5th character in the input string as a hexadecimal number, and XOR’ing it with the second parameter, i.e. a typical xor encryption/decryption routine.

def decrypt(data, start, length):
    result = bytearray([0]*length)
    quean = 0
    for cattywampus in range(start, len(data), 4):
        result[quean] = int(data[cattywampus:cattywampus+2], 16) ^ key[quean % len(key)]
        quean += 1
        if quean == len(result):
            break
    return result

with open("F.T.txt", "r") as f:
    data = f.read()

key = bytes([0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE])
result = decrypt(data, 0, 168667)

with open("stomp.mp3", "wb") as f:
    f.write(result)

However, it outputs a sound file (stomp.mp3) with the title “This is not what you should be looking at…”, and the audio itself is nothing more than some drums playing a rhythm.

Figure 4

What did we miss?

Introduction to VBA stomping

The clue is in the file name of output mp3 file.

It so happens that documents containing VBA code do not directly interpret the VBA code as specified in the script files. Instead, the VBA is compiled into what is known as P-Code, a stack-based assembly-like language. This P-Code is stored next to the original source code, and evaluated instead. My guess for Microsoft doing this, is that evaluating P-Code is easier and potentially faster than trying to parse the VBA code every time the macro is invoked.

The problem with this is, when Excel notices that some P-Code exists in the file, it will execute this code, without compiling the original source first. This happens even if the P-Code does not match the original code. An attacker could therefore construct a malicious document, by compiling a VBA script to P-Code, and removing or replacing the original VBA code in the document with something completely different. This effectively hides the true program that is executed from the GUI editor that is built into Office products. This technique is also called VBA stomping, which was referred to in the name of the mp3 file.

Finding the real program

Luckily, there are various tools to deal with stomped VBA documents and sheets. One of them is oletools, which can be found here. Running the olevba script on report.xls, confirms that the file is indeed stomped:

$ olevba report.xls
...
+----------+--------------------+---------------------------------------------+
|Type      |Keyword             |Description                                  |
+----------+--------------------+---------------------------------------------+
|AutoExec  |Auto_Open           |Runs when the Excel Workbook is opened       |
|AutoExec  |Workbook_Open       |Runs when the Excel Workbook is opened       |
|Suspicious|CreateObject        |May create an OLE object                     |
|Suspicious|Environ             |May read system environment variables        |
|Suspicious|Write               |May write to a file (if combined with Open)  |
|Suspicious|Put                 |May write to a file (if combined with Open)  |
|Suspicious|Open                |May open a file                              |
|Suspicious|Lib                 |May run code from a DLL                      |
|Suspicious|Chr                 |May attempt to obfuscate specific strings    |
|          |                    |(use option --deobf to deobfuscate)          |
|Suspicious|Xor                 |May attempt to obfuscate specific strings    |
|          |                    |(use option --deobf to deobfuscate)          |
|Suspicious|Binary              |May read or write a binary file (if combined |
|          |                    |with Open)                                   |
|Suspicious|Hex Strings         |Hex-encoded strings were detected, may be    |
|          |                    |used to obfuscate strings (option --decode to|
|          |                    |see all)                                     |
|IOC       |wininet.dll         |Executable file name                         |
|IOC       |winmm.dll           |Executable file name                         |
|Suspicious|VBA Stomping        |VBA Stomping was detected: the VBA source    |
|          |                    |code and P-code are different, this may have |
|          |                    |been used to hide malicious code             |
+----------+--------------------+---------------------------------------------+

While olevba is able to dump the actual P-Code that is run, P-Code is harder to read than normal VBA. Fortunately, there is another tool called pcode2code, which is able to decompile it back to normal VBA code. Feeding report.xls to pcode2code gives us an output similar to the one provided in ActualCode.vb.

The final part of the original main procedure is replaced with the following code:

Set groke = CreateObject(rigmarole(onzo(10)))   ' "WScript.Network"
firkin = groke.UserDomain
If firkin <> rigmarole(onzo(3)) Then   ' "FLARE-ON"
    MsgBox rigmarole(onzo(4)), vbCritical, rigmarole(onzo(6)) ' "Sorry, this machine is not supported", "Error"
    End
End If

n = Len(firkin)
For i = 1 To n
    buff(n - i) = Asc(Mid$(firkin, i, 1))
Next

wabbit = canoodle(F.T.Text, 2, 285729, buff)
mf = Environ(rigmarole(onzo(0))) & rigmarole(onzo(11))
Open mf For Binary Lock Read Write As #fn
wabbit = canoodle(F.T.Text, 2, 285729, buff)
mf = Environ(rigmarole(onzo(0))) & rigmarole(onzo(11)) '"AppData", "\Microsoft\v.png"

Instead of writing output.mp3, it writes v.png, and the parameters of the decryption function are different. The key is the reversed bytes of “FLARE-ON”, and we start at index 2 instead of 0. Let’s adjust our python script:

key = bytes(reversed(b"FLARE-ON"))
result = decrypt(data, 2, 285729)

with open("v.png", "wb") as f:
    f.write(result)

This results in the answer:

Figure 4

5 – TKApp

Time spent: 2 hours (one hour wasted on setting up and troubleshooting an emulator 🙁 )

Tools used: dnSpy, C#, and NOT an emulator

The fifth challenge of this years is an interesting concept. You are given an app that runs on Android Wearables, and a note that tells you that you can play flare-on now on your smart watch as well, so long you have a good debugger for it. The app apparently is some sort of a gallery app, with some cool tiger pictures in it.

Figure 1

However, the conclusion of this write-up might be somewhat anticlimactic. I probably did not solve this the intended way. In fact, I ended up solving it without the need to run the program at all.

Orientation

The first thing I did, was start downloading and installing an emulator to run the app on (I used Visual Studio’s Android Emulator that comes with the Xamarin SDK).

While that is going on, we can already do some preparation work. We know it is an Android app. The cool thing about APK and TPK files, is that they are simply zip files with a certain structure to them. We can therefore just use any archiving tool that supports the zip archive format, and extract all relevant files from there. Here’s the folder structure of the app:

  • /
    • bin/ (compiled binaries)
      • TKapp.dll (main code)
      • ExifLib.Standard.dll
      • other dependencies…
    • lib/ (libraries used, empty)
    • res/ (resource files, images etc.)
    • shared/ (more images)
    • author-signature.xml
    • signature1.xml
    • tizen-manifest.xml (application’s manifest)
    • TKApp.deps.json

From the tizen-manifest.xml we can find the application’s entrypoint dll (TKApp.dll). Dragging this in a tool like CFF explorer reveals that this dll is written in a .NET language.

Decompilers targeting .NET applications (such as dnSpy or ILSpy) are quite good nowadays, especially when the application in question is not obfuscated heavily. This app is no exception. Hardly any obfuscation is applied:

Figure 2

Let’s start the detective work!

UnlockPage

Since I was still waiting for the emulator to be downloaded and installed (sheesh), I decided to just click around a bit. If an application is not obfsucated, then we can already get a lot of information just by looking at the source code.

The app consists of a couple of pages. One that immediately stuck out to me was UnlockPage. This page defines a method called IsPasswordCorrect, which is called by a login button click handler. Here’s the contents of these two methods:

private async void OnLoginButtonClicked(object sender, EventArgs e)
{
    if (this.IsPasswordCorrect(this.passwordEntry.Text))
    {
        App.IsLoggedIn = true;
        App.Password = this.passwordEntry.Text;
        base.Navigation.InsertPageBefore(new MainPage(), this);
        await base.Navigation.PopAsync();
    }
    else
    {
        Toast.DisplayText("Unlock failed!", 2000);
        this.passwordEntry.Text = string.Empty;
    }
}

private bool IsPasswordCorrect(string password)
{
    return password == Util.Decode(TKData.Password);
}

Password is a hardcoded byte array in the TKData class:

public static byte[] Password = new byte[]
{
    62, 38, 63, 63, 54, 39, 59, 50, 39
};

And the Decode method is a simple xor decryption routine:

public static string Decode(byte[] e)
{
    string text = "";
    foreach (byte b in e)
    {
        text += Convert.ToChar((int)(b ^ 83)).ToString();
    }
    return text;
}

Plugging this decode function into any C# editor and running the code will give us the password:

mullethat

Great, but this is not a flag, as flags end with @flare-on.com. Obviously it couldn’t be that easy!

A rant about Visual Studio and Android emulators…

At this point the android emulator finally finished installing, and I started wasting lots of time on trying to get the app running. And at this moment, I was once again reminded of why I never use Visual Studio anymore. Besides the fact that it is absolutely the sluggiest IDE I probably have ever used, for the love of god, I couldn’t get the emulator to work for at least 30 minutes. When it finally ran, I tried running a blank watch app project myself, which I eventually got running on my virtual machine, but could not figure out how to upload a custom TPK file for another 15 minutes. Then as a last resort I tried to fully decompile the TKApp, recompile it using Visual Studio, and see if I can run it like that, but apparently the App uses very outdated dependencies of Tizen, which stopped me from compiling the project properly, even though ILSpy’s generated source code had no syntax errors whatsoever. I decided at this point to call it quits and just try this challenge completely statically.

You’d expect from Visual Studio, a toolsuite that is gigabytes in size on the disk, developed by Microsoft, a reputable company, that it would be easy to upload a custom tpk file to the watch emulator and run it. I am probably missing something obvious but boy, they could have made it a lot easier. Once again, a downvote from me for you Visual Studio.

MainPage

Let’s do a step back: In the login click handler, we can see that after a correct password validation, we can see that the program opens MainPage. This page has a couple of very interesting, odd-looking methods that suspiciously look like some verification or decryption routine. One is called PedDataUpdate, and the other is GetImage. Here is the code:

private void PedDataUpdate(object sender, PedometerDataUpdatedEventArgs e)
{
    if (e.StepCount > 50U && string.IsNullOrEmpty(App.Step))
    {
        App.Step = Application.Current.ApplicationInfo.Metadata["its"];
    }
    if (!string.IsNullOrEmpty(App.Password) && !string.IsNullOrEmpty(App.Note) && !string.IsNullOrEmpty(App.Step) && !string.IsNullOrEmpty(App.Desc))
    {
        HashAlgorithm hashAlgorithm = SHA256.Create();
        byte[] bytes = Encoding.ASCII.GetBytes(App.Password + App.Note + App.Step + App.Desc);
        byte[] first = hashAlgorithm.ComputeHash(bytes);
        byte[] second = new byte[]
        {
            50, 148, 76, 233, 110, 199, 228, 72, 114, 227, 78, 138, 93, 189, 189, 147, 
            159, 70, 66, 223, 123, 137, 44, 73, 101, 235, 129, 16, 181, 139, 104, 56
        };
        if (first.SequenceEqual(second))
        {
            this.btn.Source = "img/tiger2.png";
            this.btn.Clicked += this.Clicked;
            return;
        }
        this.btn.Source = "img/tiger1.png";
        this.btn.Clicked -= this.Clicked;
    }
}

private bool GetImage(object sender, EventArgs e)
{
    if (string.IsNullOrEmpty(App.Password) || string.IsNullOrEmpty(App.Note) || string.IsNullOrEmpty(App.Step) || string.IsNullOrEmpty(App.Desc))
    {
        this.btn.Source = "img/tiger1.png";
        this.btn.Clicked -= this.Clicked;
        return false;
    }
    string text = new string(new char[]
    {
        App.Desc[2],  App.Password[6],  App.Password[4], App.Note[4], App.Note[0],
        App.Note[17], App.Note[18], App.Note[16], App.Note[11], App.Note[13],
        App.Note[12], App.Note[15], App.Step[4], App.Password[6], App.Desc[1],
        App.Password[2], App.Password[2], App.Password[4], App.Note[18], App.Step[2],
        App.Password[4], App.Note[5], App.Note[4], App.Desc[0], App.Desc[3],
        App.Note[15], App.Note[8], App.Desc[4], App.Desc[3], App.Note[4],
        App.Step[2], App.Note[13], App.Note[18], App.Note[18], App.Note[8],
        App.Note[4], App.Password[0], App.Password[7], App.Note[0],
        App.Password[4], App.Note[11], App.Password[6], App.Password[4],
        App.Desc[4], App.Desc[3]
    });
    byte[] key = SHA256.Create().ComputeHash(Encoding.ASCII.GetBytes(text));
    byte[] bytes = Encoding.ASCII.GetBytes("NoSaltOfTheEarth");
    try
    {
        App.ImgData = Convert.FromBase64String(Util.GetString(Runtime.Runtime_dll, key, bytes));
        return true;
    }
    catch (Exception ex)
    {
        Toast.DisplayText("Failed: " + ex.Message, 1000);
    }
    return false;
}

We can see that GetImage seems to be building up a string based on a bunch of variables in the application, and then feeding it into Util.GetString, together with a hardcoded resource byte array called Runtime.dll. This method does nothing more than implement an AES decryption routine. One of these strings is our previously found password. Let’s find out the values of the other bad boys.

App.Note

Looking into App.Note in the analyzer of dnSpy, we can see that its value is set at the very end of the SetupList() method in the TodoPage class.

private void SetupList()
{
	List<TodoPage.Todo> list = new List<TodoPage.Todo>();
	if (!this.isHome)
	{
		list.Add(new TodoPage.Todo("go home", "and enable GPS", false));
	}
	else
	{
		TodoPage.Todo[] collection = new TodoPage.Todo[]
		{
			new TodoPage.Todo("hang out in tiger cage", "and survive", true),
			new TodoPage.Todo("unload Walmart truck", "keep steaks for dinner", false),
			new TodoPage.Todo("yell at staff", "maybe fire someone", false),
			new TodoPage.Todo("say no to drugs", "unless it's a drinking day", false),
			new TodoPage.Todo("listen to some tunes", "https://youtu.be/kTmZnQOfAF8", true)
		};
		list.AddRange(collection);
	}
	List<TodoPage.Todo> list2 = new List<TodoPage.Todo>();
	foreach (TodoPage.Todo todo in list)
	{
		if (!todo.Done)
		{
			list2.Add(todo);
		}
	}
	this.mylist.ItemsSource = list2;
	App.Note = list2[0].Note;
}

From this we can deduce that the note will contain the following string:

keep steaks for dinner

App.Step

We already have seen the place where App.Step is assigned, namely in PedDataUpdate. The data comes from the application’s metadata with the key its. Metadata like this is stored in the application’s manifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.flare-on.TKApp" version="1.0.0" api-version="5.5" xmlns="http://tizen.org/ns/packages">
    <author href="http://www.flare-on.com" />
    <profile name="wearable" />
    <ui-application appid="com.flare-on.TKApp" exec="TKApp.dll" multiple="false" nodisplay="false" taskmanage="true" api-version="6" type="dotnet" launch_mode="single">
        <label>TKApp</label>
        <icon>TKApp.png</icon>
        <metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true" />
        <metadata key="its" value="magic" />
        <splash-screens />
    </ui-application>
    <shortcut-list />
    <privileges>
        <privilege>http://tizen.org/privilege/location</privilege>
        <privilege>http://tizen.org/privilege/healthinfo</privilege>
    </privileges>
    <dependencies />
    <provides-appdefined-privileges />
</manifest>

Here we can see the string is set to

magic

App.Desc

Looking at the places where this property is set in the analyzer, we can find one reference to it in IndexPage_CurrentPageChanged of the GalleryPage class:

private void IndexPage_CurrentPageChanged(object sender, EventArgs e)
{
	if (base.Children.IndexOf(base.CurrentPage) == 4)
	{
		using (ExifReader exifReader = new ExifReader(Path.Combine(Application.Current.DirectoryInfo.Resource, "gallery", "05.jpg")))
		{
			string desc;
			if (exifReader.GetTagValue<string>(ExifTags.ImageDescription, out desc))
			{
				App.Desc = desc;
			}
			return;
		}
	}
	App.Desc = "";
}

We can see it obtains the description of an image. We can simply copy the iamge and this code (don’t forget to include the reference ExifLib.Standard.dll) to obtain this value:

water

Conclusion

We now have everything to decrypt Runtime.dll. Copy & paste the decryption routine, together with all the values we found, we find that the resulting byte array is an image, revealing the flag:

Figure 3

6 – report

Time spent: 3 hours

Tools used: Detect It Easy, exe2aut, Python

The sixth challenge is called codeit, a simple application that asks you for some input which is transformed into a QR code and displayed. The note tells us that if a special string is given, the QR code of the flag would be generated.

Figure 1

Orientation

If we drag this application into a program like Detect It Easy, we can quickly see that this program is UPX packed, and underneath it, an AutoIt program is conceiled.

Figure 2

There’s tons of tools around on the internet to extract the original script out of this program. I used Exe2Aut:

Exe2Aut.exe codeit.exe

This extracts three files:

Although it is probably more readable than x86 code, the script is obfuscated by renaming all variables and functions to random strings, and replacing all constants with some string decoder function call or a random global variable.

Deobfuscating all constants

The first thing to notice is that the program declares a lot of global variables that are assigned exactly once (lines 134-158).

Global $flavekolca = Number(" 0 "), $flerqqjbmh = Number(" 1 "), $flowfrckmw = Number(" 0 "), $flmxugfnde = Number(" 0 "), $flvjxcqxyn = Number(" 2 "), $flddxnmrkh = Number(" 0 "), $flroseeflv = Number(" 1 "), $flpgrglpzm = Number(" 0 "), $flvzrkqwyg = Number(" 0 "), $flyvormnqr = Number(" 0 "), $flvthbrbxy = Number(" 1 "), $flxttxkikw = Number(" 0 "), $flgjmycrvw = Number(" 1 "), $flceujxgse = Number(" 0 "), $flhptoijin = Number(" 0 "), $flrzplgfoe = Number(" 0 "), $fliboupial = Number(" 0 "), $flidavtpzc = Number(" 1 "), $floeysmnkq = Number(" 1 "), $flaibuhicd = Number(" 0 "), $flekmapulu = Number(" 1 ")
...

Furthermore, we also see a global variable os declared at line 133 that is used a lot throughout the entire program as an argument for the arehdidxrgk function. Looking at where this variable is assigned a value (line 557), we can see it is an array of encoded strings, and arehdidxrgk is the decoder for it:

Func arehdidxrgk($flqlnxgxbp)
	Local $flqlnxgxbp_
	For $flrctqryub = 1 To StringLen($flqlnxgxbp) Step 2
		$flqlnxgxbp_ &= Chr(Dec(StringMid($flqlnxgxbp, $flrctqryub, 2)))
	Next
	Return $flqlnxgxbp_
EndFunc

With this in mind, and some regex selection, we can make a simple python script that does a find and replace in the original source code. The script can be found here. After we ran it, we can remove the remnants of the global variable declarations, and we end up with a script like here.

The detective work

Now the real fun starts. If you use an editor like Visual Studio Code, you can easily collapse all functions using Ctrl+K+0. Doing this gives us a good overview of the complete program.

Figure 3

Furthermore, a lot of functions are small enough to quickly figure out what is going on, just by looking at the strings that are used in these functions. Renaming them will help a lot in seeing the bigger picture of the program, and so therefore I did.

  • areuznaqfmn -> kernel32.dll!GetComputerNameA
  • arerujpvsfp -> kernel32.dll!CreateFile
  • aremyfdtfqp -> kernel32.dll!CreateFile
  • aremfkxlayv -> kernel32.dll!SetFilePointer followed by kernel32!WriteFile
  • aremlfozynu -> kernel32.dll!ReadFile
  • arevtgkxjhu -> kernel32.dll!CloseHandle
  • arebbytwcoj -> kernel32.dll!DeleteFileA

Now that those are out of the way, let’s have a look at the main function:

Func MessageLoop()
    ;- ...
    
	While 1
		Switch GUIGetMsg()
			Case $confirm_button
				;- If confirm button clicked...

				Local $inputText = GUICtrlRead($input_textbox)
				If $inputText Then
					;~ Set up QR encoder parameters.
					Local $qrencoderPath = get_file_path(26)
					Local $qrparameters = DllStructCreate("struct;dword;dword;byte[3918];endstruct")
					Local $result = DllCall($qrencoderPath, "int:cdecl", "justGenerateQRSymbol", "struct*", $qrparameters, "str", $inputText)

					If $result[0] <> 0 Then
						;~ Some magic???
						MAGICFUNCTION($qrparameters)

						;~ Create bitmap
						Local $flbvokdxkg = CreateBitmapStruct((DllStructGetData($qrparameters, 1) * DllStructGetData($qrparameters, 2)), (DllStructGetData($qrparameters, 1) * DllStructGetData($qrparameters, 2)), 1024)
						$result = DllCall($qrencoderPath, "int:cdecl", "justConvertQRSymbolToBitmapPixels", "struct*", $qrparameters, "struct*", $flbvokdxkg[1])
						If $result[0] <> 0 Then
							;~ Write image.
							$sprite_bmp_file = random_string(25, 30) & ".bmp"
							arelassehha($flbvokdxkg, $sprite_bmp_file)
						EndIf
					EndIf
					kernel32DeleteFileA($qrencoderPath)
				Else
					$sprite_bmp_file = get_file_path(11)
				EndIf
				
				;~ Update image in window.
				GUICtrlSetImage($picturebox, $sprite_bmp_file)
				kernel32DeleteFileA($sprite_bmp_file)
    ;- ...

We can see a typical win32 message loop, where we wait for a button click event. If it is triggered, we grab the text of the input text box, set up the parameters for the call to qr_encoder.dll, do the call to justConvertQRSymbolToBitmapPixels, and then eventually display the generated image. But before we transfer control to the external dll, we call a mysterious function, which I called MAGICFUNCTION, that seems to take the qr code parameters, including the input text. Let’s have a look:

Func MAGICFUNCTION(ByRef $inputOutput)
	Local $computerName = kernel32GetComputerName()
	
	If $computerName <> -1 Then
		;~ Preprocess computer name bytes .
		$computerName = Binary(StringLower(BinaryToString($computerName)))
		Local $computerNameraw = DllStructCreate("struct;byte[" & BinaryLen($computerName) & "];endstruct")
		DllStructSetData($computerNameraw, 1, $computerName)
		DecodeFile($computerNameraw)

		Local $phPov = DllStructCreate("struct;ptr;ptr;dword;byte[32];endstruct")
		DllStructSetData($phPov, 3, 32)
		Local $result = DllCall("advapi32.dll", "int", "CryptAcquireContextA", ...)
		If $result[0] <> 0 Then
			$result = DllCall("advapi32.dll", "int", "CryptCreateHash", ...)
			If $result[0] <> 0 Then
				$result = DllCall("advapi32.dll", "int", "CryptHashData", ...)
				If $result[0] <> 0 Then
					$result = DllCall("advapi32.dll", "int", "CryptGetHashParam", ...)
					If $result[0] <> 0 Then
						Local $keydata = Binary("0x" & "08020" & "00010" & "66000" & "02000" & "0000") & DllStructGetData($phPov, 4)
						;~ Keydata now contains the sha256 of the transformed computer name, plus a prefix.

						Local $ciphertext = Binary(...)

						;~ Set up decryption key based off of key data.
						Local $buffer = DllStructCreate("struct;ptr;ptr;dword;byte[8192];byte[" & BinaryLen($keydata) & "];dword;endstruct")
						DllStructSetData($buffer, 3, BinaryLen($ciphertext))
						DllStructSetData($buffer, 4, $ciphertext)
						DllStructSetData($buffer, 5, $keydata)
						DllStructSetData($buffer, 6, BinaryLen($keydata))

						Local $result = DllCall("advapi32.dll", "int", "CryptAcquireContextA",...)
						If $result[0] <> 0 Then
							$result = DllCall("advapi32.dll", "int", "CryptImportKey", ...)
							If $result[0] <> 0 Then
								$result = DllCall("advapi32.dll", "int", "CryptDecrypt", ...)
								If $result[0] <> 0 Then
									;~ If decryption successful...
									Local $flsekbkmru = BinaryMid(DllStructGetData($buffer, 4), 1, DllStructGetData($buffer, 3))

									$FLARE = Binary("FLARE")
									$ERALF = Binary("ERALF")
									$flgggftges = BinaryMid($flsekbkmru, 1, BinaryLen($FLARE))
									$flnmiatrft = BinaryMid($flsekbkmru, BinaryLen($flsekbkmru) - BinaryLen($ERALF) + 1, BinaryLen($ERALF))
									If $FLARE = $flgggftges AND $ERALF = $flnmiatrft Then
										;~ Update the qr parameters to display the decrypted data.
										DllStructSetData($inputOutput, 1, BinaryMid($flsekbkmru, 6, 4))
										DllStructSetData($inputOutput, 2, BinaryMid($flsekbkmru, 10, 4))
										DllStructSetData($inputOutput, 3, BinaryMid($flsekbkmru, 14, BinaryLen($flsekbkmru) - 18))
                                        
    ;- ...

That’s a big function! If we go over it step by step, we can see the computer name is acquired, then some preprocessing is done, and after that, it is used as part of a decryption key for a whole bunch of complicated cryptography (SHA256 and RSA) that we most likely cannot break. If, however, the decryption succeeds, the QR parameters are updated. So we know that this is the place to look.

It seems though, that the decryption routine does not really rely on the input text that was given in the input box, but rather on the computer name. Let’s have a look at the preprocessing of the computer name, see if we can find some clues there:

Func DecodeFile(ByRef $cipherTextBuffer)	
	;- Open sprite.bmp
	Local $spritebmppath = get_file_path(14)
	Local $handle = kernel32CreateFile($spritebmppath)
	If $handle <> -1 Then
		Local $fileSize = kernel32GetFileSize($handle)
		If $fileSize <> -1 AND DllStructGetSize($cipherTextBuffer) < $fileSize - 54 Then
			;- Read file contents.
			Local $buffer = DllStructCreate("struct;byte[" & $fileSize & "];endstruct")
			Local $flskuanqbg = kernel32ReadFile($handle, $buffer)
			If $flskuanqbg <> -1 Then
				;- Skip the first 54 bytes (bmp header).
				Local $flxmdchrqd = DllStructCreate("struct;byte[54];byte[" & $fileSize - 54 & "];endstruct", DllStructGetPtr($buffer))
				Local $counter = 1
				Local $result = ""

				;- Build up a result string based on the input computer name and pixel data in sprite.bmp.
				For $i = 1 To DllStructGetSize($cipherTextBuffer)
					Local $currentChar = Number(DllStructGetData($cipherTextBuffer, 1, $i))
					For $j = 6 To 0 Step -1
						$currentChar += BitShift(BitAND(Number(DllStructGetData($flxmdchrqd, 2, $counter)), 1), -1 * $j)
						$counter += 1
					Next
					$result &= Chr(BitShift($currentChar, 1) + BitShift(BitAND($currentChar, 1), -7))
				Next
				DllStructSetData($cipherTextBuffer, 1, $result)
			EndIf
		EndIf
		kernel32CloseHandle($handle)
	EndIf
	kernel32DeleteFileA($spritebmppath)
EndFunc

Interesting! We are reading some hidden data from sprite.bmp, 7 pixels per character. ASCII is a 7-bit character encoding, so that seems like a good guess. Let’s write a python script that simulates it:

with open("sprite.bmp", "rb") as f:
    data = f.read()

result = []
current = ""
for j in range(0x36, 0x100):
    bit = data[j] & 1
    current += str(bit)
    if len(current) == 7:
        result.append(current)
        current = ""

print(result)
print("".join([chr(int(x, 2)) for x in result]))

Resulting in :

$ python lsb.py
['1100001', '1110101', '1110100', '0110000', '0110001', '1110100', '1100110', '1100001', '1101110', '0110001', '0111001', '0111001', '0111001', '1111111', '1111111', '1111111', '1111111', '1111111', '1111111', '1111111', '1111111', '1111111', '1111111', '1111111', '1111111', '1111111', '1111111', '1111111']
aut01tfan1999⌂⌂⌂⌂⌂⌂⌂⌂⌂⌂⌂⌂⌂⌂⌂

If we replace the computer name with the string aut01tfan1999, we can observe that the decryption succeeds, and a different QR is displayed:

Figure 4

Scanning the resulting QR code with a phone…

Figure 5

… reveals the flag:

[email protected]

To be continued…

Cracking .NET Components

You may wonder why I have chosen this topic, why write a tutor on .net components?

Technically a .NET component is not different from an executable assembly, I mean that both are compiled to MSIL and you can usually view the source in Reflector and other tools, but when it comes to commercial components you have to understand that more and more complicated protection schemes are being implemented to protect them, and after analyzing many products I found so many points that all these components share to protect themselves.

The second reason that pushed me to write this tutor is that I couldn’t find any papers on this topic and that’s because many crackers are still not interested in this platform yet and I really don’t know why!

Today I have chosen a real target and remember that this is only the first tutor on this topic so don’t expect it to be very hard, so this is just an introduction, and later I will demonstrate harder targets.

Download it here