Flare-On 2021: antioch
antioch was a challenge based on the old movie, Monty Python and the Holy Grail. I’m given a Tar archive, which is a Docker image, the output of a command like docker save
. It has a lot of layer data, but most the layers are not referenced in the manifest. The image does have a single ELF executable in it. Though reversing this binary, I’ll see how it expects input matching the various authors from the metadata in the unused layers, and how each author has an id associated with it. I’ll use the order of those IDs to reconstruct the Docker image to include the files in the right order, and then the new image will give the flag.
Challenge
To solve this challenge, you’ll need to …AAARGH
The archive (password “flare”) contains a TAR archive:
$ file antioch.tar
antioch.tar: POSIX tar archive (GNU)
Extract
Binary
The manifest.json
file shows a single layer as the latest:
[
{
"Config": "a13ffcf46cf41480e7f15c7f3c6b862b799bbe61e7d5909150d8a43bd3b6c039.json",
"RepoTags": [
"antioch:latest"
],
"Layers": [
"7016b68f19aed3bb67ac4bf310defd3f7e0f7dd3ce544177c506d795f0b2acf3/layer.tar"
]
}
]
In that layer, the .tar
holds a single file which is an ELF:
$ file AntiochOS
AntiochOS: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped
It’s a bit weird that only one layer shows up in the manifest.json
. When I look at the tar, there are lots of layers present:
$ tar tf antioch.tar
09e6fff53d6496d170aaa9bc88bd39e17c8e5c13ee9066935b089ab0312635ef/
09e6fff53d6496d170aaa9bc88bd39e17c8e5c13ee9066935b089ab0312635ef/VERSION
09e6fff53d6496d170aaa9bc88bd39e17c8e5c13ee9066935b089ab0312635ef/layer.tar
09e6fff53d6496d170aaa9bc88bd39e17c8e5c13ee9066935b089ab0312635ef/json
1c5d28d6564aed0316526e8bb2d79a436b45530d2493967c8083fea2b2e518ce/
1c5d28d6564aed0316526e8bb2d79a436b45530d2493967c8083fea2b2e518ce/VERSION
1c5d28d6564aed0316526e8bb2d79a436b45530d2493967c8083fea2b2e518ce/layer.tar
1c5d28d6564aed0316526e8bb2d79a436b45530d2493967c8083fea2b2e518ce/json
25e171d6ac47c26159b26cd192a90d5d37e733eb16e68d3579df364908db30f2/
25e171d6ac47c26159b26cd192a90d5d37e733eb16e68d3579df364908db30f2/VERSION
25e171d6ac47c26159b26cd192a90d5d37e733eb16e68d3579df364908db30f2/layer.tar
25e171d6ac47c26159b26cd192a90d5d37e733eb16e68d3579df364908db30f2/json
2b363180ec5d5862b2a348db3069b51d79d4e7a277d5cf5e4afe2a54fc04730e/
...[snip]...
In fact, there are 31 layers:
$ tar tf antioch.tar | grep '/json' | wc -l
31
Only the binary is included in the manifest
. This will prove important.
Authors (or Knights)
As I noted above, each later has three files, json
, layer.tar
, and VERSION
. Each VERSION
is just “1.0”. Each json
looks like:
{
"architecture": "amd64",
"author": "Dragon of Angnor",
"config": {
"AttachStderr": false,
"AttachStdin": false,
"AttachStdout": false,
"Cmd": null,
"Domainname": "",
"Entrypoint": null,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Hostname": "",
"Image": "",
"Labels": null,
"OnBuild": null,
"OpenStdin": false,
"StdinOnce": false,
"Tty": false,
"User": "",
"Volumes": null,
"WorkingDir": ""
},
"container_config": {
"AttachStderr": false,
"AttachStdin": false,
"AttachStdout": false,
"Cmd": [
"/bin/sh",
"-c",
"#(nop) ADD multi:cac4629faae36e1a69040f3ca0fb3377ddd7eb285ac22bc701f064de1bf22f46 in / "
],
"Domainname": "",
"Entrypoint": null,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Hostname": "",
"Image": "",
"Labels": null,
"OnBuild": null,
"OpenStdin": false,
"StdinOnce": false,
"Tty": false,
"User": "",
"Volumes": null,
"WorkingDir": ""
},
"created": "1975-04-03T12:00:00.000000000Z",
"docker_version": "20.10.2",
"id": "09e6fff53d6496d170aaa9bc88bd39e17c8e5c13ee9066935b089ab0312635ef",
"os": "linux"
}
The important part of each of these is the Author
, which contains characters from Monty Python and the Holy Grail. I can use find
and jq
to get a list of the characters and their layers:
$ find . -name json -exec jq -c '[.author, .id]' {} \;
["Sir Gallahad","a2de31788db95838a986271665b958ac888d78559aa07e55d2a98fc3baecf6e6"]
["Roger the Shrubber","1c5d28d6564aed0316526e8bb2d79a436b45530d2493967c8083fea2b2e518ce"]
["Sir Bors","ea12384be264c32ec1db0986247a8d4b2231bf017742313c01b05a7e431d9c26"]
["Sir Gawain","58da659c7d1c5a0c3447cb97cd6ffb12027c734bfba32de8b9b362475fe92fae"]
["Black Knight","cfd7ddb31ce44bb24b373645876ac7ea372da1f3f31758f2321cc8f5b29884fb"]
["King Arthur","e6c2557dc0ff4173baee856cbc5641d5b19706ddb4368556fcdb046f36efd2e2"]
["Squire Concorde","81f28623cca429f9914e21790722d0351737f8ad3e823619a4f7019be72e2195"]
["Legendary Black Beast of Argh","9a31bad171ad7e8009fba41193d339271fc51f992b8d574c501cae1bfa6c3fe2"]
["A Famous Historian","49fb821d2bf6d6841ac7cf5005a6f18c4c76f417ac8a53d9e6b48154b5aa1e76"]
["Trojan Rabbit","6b4e128697aa0459a6caba2088f6f77efaaf29d407ec6b58939c9bc7814688ad"]
["Green Knight","76531a907cdecf03c8ac404d91cbcabd438a226161e621fab103a920600372a8"]
["Prince Herbert","e1a9333f9eccfeae42acec6ac459b9025fe6097c065ffeefe5210867e1e2317d"]
["Lady of the Lake","a435765bcd8745561460979b270878a3e7c729fae46d9e878f4c2d42e5096a44"]
["Rabbit of Caerbannog","cd27ad9a438a7eef05f5b5d99e2454225693e63aba29ce8553800fed23575040"]
["Dragon of Angnor","09e6fff53d6496d170aaa9bc88bd39e17c8e5c13ee9066935b089ab0312635ef"]
["Inspector End Of Film","fadf53f0ae11908b89dffc3123e662d31176b0bb047182bfec51845d1e81beb9"]
["Dennis the Peasant","2b363180ec5d5862b2a348db3069b51d79d4e7a277d5cf5e4afe2a54fc04730e"]
["Chicken of Bristol","bfefc1bdf8b980a525f58da1550b56daa67bae66b56e49b993fff139faa1472c"]
["Squire Patsy","f2ebdc667cbafc2725421d3c02babc957da2370fbd019a9e1993d8b0409f86dd"]
["Sir Ector","303dfd1f7447a80322cc8a8677941da7116fbf0cea56e7d36a4f563c6f22e867"]
["Sir Lancelot","fd8bf3c084c5dd42159f9654475f5861add943905d0ad1d3672f39e014757470"]
["Miss Islington","b75ea3e81881c5d36261f64d467c7eb87cd694c85dd15df946601330f36763a4"]
["Sir Bedevere","7d643931f34d73776e9169551798e1c4ca3b4c37b730143e88171292dbe99264"]
[null,"7016b68f19aed3bb67ac4bf310defd3f7e0f7dd3ce544177c506d795f0b2acf3"]
["Bridge Keeper","f9621328166de01de73b4044edb9030b3ad3d5dbc61c0b79e26f177e9123d184"]
["Tim the Enchanter","4c33f90f25ea2ab1352efb77794ecc424883181cf8e6644946255738ac9f5dbd"]
["Sir Not-Appearing-in-this-Film","8e11477e79016a17e5cde00abc06523856a7db9104c0234803d30a81c50d2b71"]
["Zoot","b5f502d32c018d6b2ee6a61f30306f9b46dad823ba503eea5b403951209fd59b"]
["Brother Maynard","e5254dec4c7d10c15e16b41994ca3cf0c5e2b2a56c9d4dc2ef053eeff24333ff"]
["Dinky","25e171d6ac47c26159b26cd192a90d5d37e733eb16e68d3579df364908db30f2"]
["Sir Robin","754ee87063ee108c1f939cd3a28980a03b700f3c3967df8058831edad2743fd7"]
Running It
Docker
This archive format is the output of the docker save
command. I looked at this before in the Twelve Steps of Christmas challenge in Hackvent 2020.
I can load this image and run it as is using docker
:
$ docker load --input antioch.tar
Loaded image: antioch:latest
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
antioch latest a13ffcf46cf4 7 weeks ago 13kB
If I try to run bash
or sh
, it fails:
$ docker run -it antioch bash
docker: Error response from daemon: OCI runtime create failed: container_linux.go:370: starting container process caused: exec: "bash": executable file not found in $PATH: unknown.
ERRO[0000] error waiting for container: context canceled
$ docker run -it antioch sh
docker: Error response from daemon: OCI runtime create failed: container_linux.go:370: starting container process caused: exec: "sh": executable file not found in $PATH: unknown.
ERRO[0000] error waiting for container: context canceled
$ docker run -it antioch /bin/sh
docker: Error response from daemon: OCI runtime create failed: container_linux.go:370: starting container process caused: exec: "/bin/sh": stat /bin/sh: no such file or directory: unknown.
ERRO[0000] error waiting for container: context canceled
I can run AntiochOS
:
$ docker run -it antioch /AntiochOS
AntiochOS, version 1.32 (build 1975)
Type help for help
>
ELF
This works the same as running the ELF:
$ ./AntiochOS
AntiochOS, version 1.32 (build 1975)
Type help for help
>
Running help
isn’t too helpful:
> help
Available commands:
help: print this help
...AAARGH
>
RE
Helper Functions
There were three types of functions that were quick to label once I understood the formats. I’ll call these load
, loadxl
, and syscall
functions.
load
There were nine functions that simply took a pointer to a buffer and set a string value into it one byte at a time, like this:
By retyping param_1
from undefined *
to char *
, it will quickly show the value, and I’ll rename the function to describe it:
loadxl
There were five more functions that had a format like this:
In each case, it made sure that the first byte in some buffer was 0, and then xored each byte in the buffer up to a hard-coded stop point with a value (each was different, but 0xbe in this example). The buffer looked like:
After the xor, it returned the address of the second byte in the original buffer (0x00404001 in the example above). I can select that buffer and right click, “Copy Special…”, and copy the buffer as a Python Byte String. Then I can paste it into my Python terminal and quickly decode it:
>>> x = b'\xec\xd7\xd9\xd6\xca\x90\x9e\xf1\xd8\xd8\x9e\xc7\xd1\xcb\x9e\xd9\xd1\x90\x9e\x9d'
>>> ''.join([chr(c ^ 0xbe) for c in x])
'Right. Off you go. #'
syscall
The other group of functions each just made a call to FUN_00401980
. That function just makes a syscall
:
undefined [16]
syscall_execute(undefined8 syscall_number,undefined8 arg1,undefined8 arg2,undefined8 arg3)
{
syscall();
return CONCAT88(arg3,syscall_number);
}
The decompilation return is a bit weird, but otherwise it’s straight forward. Knowing that, I can label these eight functions as syscall_[call]
, for example, syscall_open
:
undefined [16] syscall_open(undefined8 filename)
{
undefined auVar1 [16];
auVar1 = syscall_execute(2,filename,0,0);
return auVar1;
}
The first arg to syscall_execute
is the syscall number, and in this case, 2 is open.
There were eight of these functions, providing close
, execute
, exit
, open
, read
, select
, stat
, and write
.
entry
The entry function is quite simple once the helpers are defined. It prints the welcome message, then goes into a loop where it prints the prompt, reads input, and compares it against X strings and takes action based on that (function names are given by me):
Command | Action |
---|---|
quit |
Breaks loop and exits |
help |
Calls print_help() , starts of help menu but then “…AAARGH” |
consult |
Calls do_consult |
approach |
Calls do_approach |
approach
Entering the approach
command prompts for my name (just like in the movie):
> approach
Approach the Gorge of Eternal Peril!
What is your name?
Entering a name just replies “…AAARGH”. If I enter one of the authors from the list above, it asks my quest (which the answer doesn’t matter), and then for a favorite color:
> approach
Approach the Gorge of Eternal Peril!
What is your name? Sir Ector
What is your quest? find the grail
What is your favorite color? Green
...AAARGH
There is an array of data which I’ve labeled knights
at 0x00402000. There are 30 blocks, each consisting of a CRC32 of the knight name, a CRC32 of the knights favorite color, and a four byte int ranging from 1-30.
The code starts by printing and getting the name:
res = loadxl_approach_the_gorge_str();
syscall_write(1,res,0x25);
syscall_select(1);
load_what_is_your_name_str(message_strs);
syscall_write(1,message_strs,0x13);
res = syscall_read(0,read_buffer,0x80);
name_crc = crc32(read_buffer,res);
knight_ptr = knight_ARRAY_00402000;
crc = -0x4a6c6a57;
while (knight_ptr = knight_ptr + 1, crc != name_crc) {
i2 = (int)i + 1;
i = (ulong)i2;
if (i2 == 0x1e) goto LAB_00401800;
crc = knight_ptr->name_crc;
}
It takes the input name, finds the CRC32, and then steps through the data looking for a CRC32 that matches. I’ve actually defined a 12-byte struct in Ghidra for each knight, which is why it only increments the pointer by 1.
If it finds the matching CRC32, it exits the while loop, of if it reaches the end, it jumps to the end where it prints “…AAARGH” and exits.
The next code does a similar check against the favorite color:
load_what_is_your_quest_str(message_strs);
syscall_write(1,message_strs,0x14);
name_len = syscall_read(0,read_buffer,0x80);
if (1 < name_len) {
load_what_is_your_favorite_color_str(message_strs);
syscall_write(1,message_strs,0x1d);
res = syscall_read(0,read_buffer,0x80);
crc = crc32(read_buffer,res);
if ((knight_ARRAY_00402000[i].color_crc == crc) && (0 < *(char *)&knight_ARRAY_00402000[i].id)) {
If the color matches the corresponding CRC32 for that knight, it continues, otherwise it prints “…AAARGH” and exits.
On answering the questions correctly, it just prints “Right. Off you go.” and a number, which is the third value from that knight struct:
> approach
Approach the Gorge of Eternal Peril!
What is your name? Sir Lancelot
What is your quest? grail
What is your favorite color? Blue
Right. Off you go. #18
I’ll write a Python script to find each character, their color, and their number:
#!/usr/bin/env python3
import struct
from zlib import crc32
knights = {}
knights_data = b'\xa9\x95\x93\xb5\x29\xab\xb5\x1b\x0e\x00\x00\x00\x4b\xd0\xfd\x5e\xc8\x68\x84\x3f\x12\x00\x00\x00\xd0\x85\xed\xec\x48\x3d\xd2\x82\x02\x00\x00\x00\x14\x92\x54\xd8\xe5\x2e\x47\x00\x1d\x00\x00\x00\x4d\x02\x2f\x2c\xaa\x60\xa0\xc9\x0c\x00\x00\x00\x32\x52\x8a\x01\x35\xd2\x24\x00\x0d\x00\x00\x00\x33\x8a\xb8\x72\x13\x66\x57\x81\x14\x00\x00\x00\xe2\x04\x44\x67\x29\xe1\x69\x51\x0b\x00\x00\x00\xb5\x73\x7a\x30\x3e\xe1\x60\xe5\x1c\x00\x00\x00\x04\x87\x46\x13\xa9\xe4\x58\x23\x15\x00\x00\x00\x1b\x47\xf6\x94\x53\x1a\x34\xd6\x05\x00\x00\x00\x75\xcf\xa1\xed\xe5\x91\xfa\xba\x18\x00\x00\x00\x4d\x12\xac\xbb\x1d\x64\x97\xa6\x19\x00\x00\x00\xc3\xe4\x07\xf7\x43\x56\x18\xef\x07\x00\x00\x00\x6f\x59\x02\xd7\x15\x89\xc2\x79\x0a\x00\x00\x00\x48\x08\xa1\x86\xdc\x8f\x10\x59\x01\x00\x00\x00\x1c\x53\x40\xd6\xe8\xe1\x3d\xef\x13\x00\x00\x00\xb3\x5d\x66\x7b\xb0\x03\xa9\xa3\x03\x00\x00\x00\xcc\x21\x13\xab\xd7\xea\xed\xee\x04\x00\x00\x00\xd8\x66\x60\x4f\x07\x3d\x8a\x9c\x11\x00\x00\x00\xca\x47\x60\x25\x9e\xbe\x85\x40\x09\x00\x00\x00\xd3\x1e\xc9\x3f\xc9\x49\x95\x37\x08\x00\x00\x00\xe4\xaf\x24\xa4\x47\x13\x87\xef\x1b\x00\x00\x00\xda\x01\x09\x55\x6b\xec\xfc\x01\x10\x00\x00\x00\x2d\x9e\xa2\x10\xaa\x56\x60\xe7\x16\x00\x00\x00\x5f\xc8\xcb\x56\x68\x1a\x6f\x35\x0f\x00\x00\x00\xa6\xe3\xdf\x80\x36\xb5\x0a\x9d\x1e\x00\x00\x00\xe1\xd4\x57\xe6\x30\xfd\xe9\xb4\x17\x00\x00\x00\xd4\xe1\xa1\x2b\x18\xd9\x66\xbe\x1a\x00\x00\x00\x9b\x08\x33\x7d\x85\xf5\xc1\x67\x06\x00\x00\x00'
names = ['Sir Gallahad', 'Roger the Shrubber', 'Sir Bors', 'Sir Gawain', 'Black Knight', 'King Arthur', 'Squire Concorde', 'Legendary Black Beast of Argh', 'A Famous Historian', 'Trojan Rabbit', 'Green Knight', 'Prince Herbert', 'Lady of the Lake', 'Rabbit of Caerbannog', 'Dragon of Angnor', 'Inspector End Of Film', 'Dennis the Peasant', 'Chicken of Bristol', 'Squire Patsy', 'Sir Ector', 'Sir Lancelot', 'Miss Islington', 'Sir Bedevere', 'Bridge Keeper', 'Tim the Enchanter', 'Sir Not-Appearing-in-this-Film', 'Zoot', 'Brother Maynard', 'Dinky', 'Sir Robin']
with open('colors.txt', 'r') as f:
colors = f.readlines()
crc_names = {crc32(f"{kn}\n".encode()): kn for kn in names}
crc_colors = {crc32(c.encode()): c for c in colors}
knights = {}
for i in range(0, len(knights_data), 12):
name_crc, color_crc, num = struct.unpack("<III", knights_data[i:i+12])
name = crc_names[name_crc]
try:
color = crc_colors[color_crc].strip()
except KeyError:
color = "Not Identified"
print(f"No color for {name}")
knights[num] = {"name_crc": name_crc, "color_crc": color_crc, "name": name, "color": color}
for i in sorted(knights):
print(f"[{i:2d}] {knights[i]['name']:35} {knights[i]['color']}")
colors.txt
comes from this list of CSS colors. It finds colors for all but one:
$ python3 list_knights.py
No color for Sir Not-Appearing-in-this-Film
[ 1] Miss Islington Brown
[ 2] Sir Bors Coral
[ 3] Tim the Enchanter Orange
[ 4] Dragon of Angnor Khaki
[ 5] Brother Maynard Crimson
[ 6] Sir Bedevere Teal
[ 7] Sir Robin Red
[ 8] Zoot Tan
[ 9] Squire Concorde Periwinkle
[10] Green Knight Green
[11] Trojan Rabbit Beige
[12] Chicken of Bristol Mint
[13] Roger the Shrubber Tomato
[14] Bridge Keeper Indigo
[15] Sir Gawain Azure
[16] Legendary Black Beast of Argh Silver
[17] A Famous Historian Pink
[18] Sir Lancelot Blue
[19] Lady of the Lake Gold
[20] Rabbit of Caerbannog Salmon
[21] Sir Not-Appearing-in-this-Film Not Identified
[22] Prince Herbert Wheat
[23] King Arthur Purple
[24] Inspector End Of Film Gray
[25] Sir Ector Bisque
[26] Squire Patsy Chartreuse
[27] Dennis the Peasant Orchid
[28] Dinky Turquoise
[29] Black Knight Black
[30] Sir Gallahad Yellow
consult
Running consult
prints a message, hangs for a second, and then prints a ton of lines of Vs:
> consult
Consult the Book of Armaments!
VVVVVVVVVVVVVVV
VVVVVVVVVVVVVVV
...[snip]...
The function do_consult
starts by zeroing out a buffer and printing a message:
ptr_decoded_buf = decoded_buf;
for (i = 0x200; i != 0; i = i + -1) {
*ptr_decoded_buf = 0;
ptr_decoded_buf = ptr_decoded_buf + 1;
}
puVar1 = loadxl_consult_the_book_string();
syscall_write(1,puVar1,0x1f);
syscall_select(2);
There are two variables defined at the top:
c = 0x61;
fn = 0x7461642e2e;
c
is “a”, and fn
is “..dat”.
The code then loops on c
until it reaches 0x7b (“z”):
do {
while( true ) {
fn = fn & 0xffffffffffffff00 | (ulong)c;
res = syscall_open(&fn);
if (res < 0) break;
syscall_read(res,file_buf,0x1000);
syscall_close(res);
ptr_decoded_buf = decoded_buf;
ptr_file_buf = file_buf;
do {
*(byte *)ptr_decoded_buf = *(byte *)ptr_decoded_buf ^ *ptr_file_buf;
ptr_decoded_buf = (undefined8 *)((long)ptr_decoded_buf + 1);
ptr_file_buf = ptr_file_buf + 1;
} while (ptr_decoded_buf != (undefined8 *)&stack0xffffffffffffffd8);
c = c + 1;
if (c == 0x7b) goto LAB_00401544;
}
c = c + 1;
} while (c != 0x7b);
On each loop, it replaces the first .
in ..dat
with c
, and opens that file, reads it, and then loops over the buffer xoring each byte by the input. So effectively it’s looking for the files a-z.dat and xoring them together.
Then it loads a string of characters used to make ascii art into 0x00404100 (the rest is filled with “.”):
V\',`)(//\\\\\\||||||||||||_______________.
Now it loops through the buffer from above, using the value for a key into that string above to get one of those characters except for every 16th character, which it leaves as a newline:
i = 0;
do {
value = '\n';
if (((uint)i & 0xf) != 0xf) {
value = (&DAT_00404100)[*(byte *)((long)decoded_buf + i)];
}
*(undefined *)((long)decoded_buf + i) = value;
i = i + 1;
} while (i != 0x1000);
syscall_write(1,decoded_buf,0x1000);
When none of the files could be read, the buffer was just all null, so it returned all V.
Solve
Find dats
The only place I can see where a flag could be would be in the ASCII art built from the .dat
files. There are no dat files yet, but I do have all these layers that aren’t included. Checking a couple, I find them:
$ tar tf 09e6fff53d6496d170aaa9bc88bd39e17c8e5c13ee9066935b089ab0312635ef/layer.tar
a.dat
c.dat
d.dat
e.dat
f.dat
i.dat
j.dat
k.dat
l.dat
q.dat
t.dat
u.dat
v.dat
x.dat
y.dat
z.dat
$ tar tf 1c5d28d6564aed0316526e8bb2d79a436b45530d2493967c8083fea2b2e518ce/layer.tar
a.dat
e.dat
g.dat
h.dat
i.dat
k.dat
l.dat
m.dat
n.dat
o.dat
p.dat
q.dat
s.dat
t.dat
w.dat
x.dat
y.dat
z.dat
This is good because I have the files, but bad because each of the layers has many dat files and there are repeats. I can get a histogram of all the files in the different layers:
$ find . -name layer.tar -exec tar tf {} \; | sort | uniq -c
23 a.dat
1 AntiochOS
22 b.dat
15 c.dat
18 d.dat
23 e.dat
22 f.dat
20 g.dat
24 h.dat
23 i.dat
24 j.dat
22 k.dat
21 l.dat
22 m.dat
23 n.dat
23 o.dat
18 p.dat
24 q.dat
13 r.dat
22 s.dat
20 t.dat
22 u.dat
21 v.dat
23 w.dat
24 x.dat
20 y.dat
27 z.dat
This means the ordering of the layers is important. The last one added for each letter will be the one that ends up in the image.
Order Layers
The one thing I did manage to pull from the approach
command is each character and their number. And each layer has a character as an author. So I can try ordering the layers in the order of the character numbers.
There are two files that need to be fixed in the antioch.tar
file in order to add the layers back in. First, the manifest.json
will need to include the layers in order. The default one is:
[{"Config":"a13ffcf46cf41480e7f15c7f3c6b862b799bbe61e7d5909150d8a43bd3b6c039.json","RepoTags":["antioch:latest"],"Layers":["7016b68f19aed3bb67ac4bf310defd3f7e0f7dd3ce544177c506d795f0b2acf3/layer.tar"]}]
The other 30 layers will need to be added to the Layes
array. The manifest.json
also references a Config
file. In that file, it has a hash for each of the layers at the end in the rootfs.diff_ids
list.
{"architecture":"amd64","config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/AntiochOS"],"Image":"sha256:72081c09b8504bda08787ba6ea0c5059e74464398cb92685b3c86a26230b8a1f","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"5a7d890eaf80df63166dedb6c0f0afaa26894ba10dd647671da887cfe2ce4349","container_config":{"Hostname":"5a7d890eaf80","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\"/AntiochOS\"]"],"Image":"sha256:72081c09b8504bda08787ba6ea0c5059e74464398cb92685b3c86a26230b8a1f","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{}},"created":"2021-07-23T03:21:55.959771124Z","docker_version":"20.10.2","history":[{"created":"2021-07-23T03:21:55.793483339Z","created_by":"/bin/sh -c #(nop) ADD file:fae1674275a5cc9b0c04ef177df65aebeaf796b0ba7c94ac2bd35120306411d4 in / "},{"created":"2021-07-23T03:21:55.959771124Z","created_by":"/bin/sh -c #(nop) CMD [\"/AntiochOS\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:d26c760acd6e75540d4ab7a33245a75a5506daa7998819f97918a39632a15497"]}}
The original version only shows one, which is the SHA256 of the layer.tar
containing AntiochOS
:
$ sha256sum 7016b68f19aed3bb67ac4bf310defd3f7e0f7dd3ce544177c506d795f0b2acf3/layer.tar
d26c760acd6e75540d4ab7a33245a75a5506daa7998819f97918a39632a15497 7016b68f19aed3bb67ac4bf310defd3f7e0f7dd3ce544177c506d795f0b2acf3/layer.tar
$ tar tf 7016b68f19aed3bb67ac4bf310defd3f7e0f7dd3ce544177c506d795f0b2acf3/layer.tar
AntiochOS
The order of the hashes must be in the same order as the layers.
Script
This script will take the original .tar
file, open it, and create antioch-new.tar
with a modified manifest and config such that the layers are added back in the order of the characters/knights:
#!/usr/bin/env python3
import hashlib
import json
import os
import struct
import tarfile
from io import StringIO, BytesIO
from zlib import crc32
knights = {}
knights_data = b'\xa9\x95\x93\xb5\x29\xab\xb5\x1b\x0e\x00\x00\x00\x4b\xd0\xfd\x5e\xc8\x68\x84\x3f\x12\x00\x00\x00\xd0\x85\xed\xec\x48\x3d\xd2\x82\x02\x00\x00\x00\x14\x92\x54\xd8\xe5\x2e\x47\x00\x1d\x00\x00\x00\x4d\x02\x2f\x2c\xaa\x60\xa0\xc9\x0c\x00\x00\x00\x32\x52\x8a\x01\x35\xd2\x24\x00\x0d\x00\x00\x00\x33\x8a\xb8\x72\x13\x66\x57\x81\x14\x00\x00\x00\xe2\x04\x44\x67\x29\xe1\x69\x51\x0b\x00\x00\x00\xb5\x73\x7a\x30\x3e\xe1\x60\xe5\x1c\x00\x00\x00\x04\x87\x46\x13\xa9\xe4\x58\x23\x15\x00\x00\x00\x1b\x47\xf6\x94\x53\x1a\x34\xd6\x05\x00\x00\x00\x75\xcf\xa1\xed\xe5\x91\xfa\xba\x18\x00\x00\x00\x4d\x12\xac\xbb\x1d\x64\x97\xa6\x19\x00\x00\x00\xc3\xe4\x07\xf7\x43\x56\x18\xef\x07\x00\x00\x00\x6f\x59\x02\xd7\x15\x89\xc2\x79\x0a\x00\x00\x00\x48\x08\xa1\x86\xdc\x8f\x10\x59\x01\x00\x00\x00\x1c\x53\x40\xd6\xe8\xe1\x3d\xef\x13\x00\x00\x00\xb3\x5d\x66\x7b\xb0\x03\xa9\xa3\x03\x00\x00\x00\xcc\x21\x13\xab\xd7\xea\xed\xee\x04\x00\x00\x00\xd8\x66\x60\x4f\x07\x3d\x8a\x9c\x11\x00\x00\x00\xca\x47\x60\x25\x9e\xbe\x85\x40\x09\x00\x00\x00\xd3\x1e\xc9\x3f\xc9\x49\x95\x37\x08\x00\x00\x00\xe4\xaf\x24\xa4\x47\x13\x87\xef\x1b\x00\x00\x00\xda\x01\x09\x55\x6b\xec\xfc\x01\x10\x00\x00\x00\x2d\x9e\xa2\x10\xaa\x56\x60\xe7\x16\x00\x00\x00\x5f\xc8\xcb\x56\x68\x1a\x6f\x35\x0f\x00\x00\x00\xa6\xe3\xdf\x80\x36\xb5\x0a\x9d\x1e\x00\x00\x00\xe1\xd4\x57\xe6\x30\xfd\xe9\xb4\x17\x00\x00\x00\xd4\xe1\xa1\x2b\x18\xd9\x66\xbe\x1a\x00\x00\x00\x9b\x08\x33\x7d\x85\xf5\xc1\x67\x06\x00\x00\x00'
manifest_fn = 'manifest.json'
config_fn = 'a13ffcf46cf41480e7f15c7f3c6b862b799bbe61e7d5909150d8a43bd3b6c039.json'
origtar = tarfile.open('./antioch.tar', 'r')
newtar = tarfile.open('./antioch-new.tar', 'w')
layers_to_hashes = {}
knights_layers = {}
# loop through tar creating new copy and getting info
for f in origtar.getmembers():
# skip manifest and config - will modified and add later
if f.name in [manifest_fn, config_fn]:
continue
newtar.addfile(f, origtar.extractfile(f.name))
# get map of layer to sha256
if 'layer.tar' in f.name:
layers_to_hashes[f.name.split('/')[0]] = hashlib.sha256(origtar.extractfile(f.name).read()).hexdigest()
# get map of authors/knights to layer
elif f.name.endswith('/json'):
layer = json.loads(origtar.extractfile(f.name).read())
if 'author' in layer:
knights_layers[layer['author']] = f.name.split('/')[0]
crc_names = {crc32(f"{kn}\n".encode()): kn for kn in knights_layers}
for i in range(0, len(knights_data), 12):
name_crc, color_crc, num = struct.unpack("<III", knights_data[i:i+12])
name = crc_names[name_crc]
layer = knights_layers[name]
sha256 = layers_to_hashes[layer]
knights[num] = {"name_crc": name_crc, "color_crc": color_crc, "name": name, "layer": layer, "sha256": sha256}
# Create new manifest.json
manifest = json.loads(origtar.extractfile(manifest_fn).read())
manifest[0]['Layers'] = ["7016b68f19aed3bb67ac4bf310defd3f7e0f7dd3ce544177c506d795f0b2acf3/layer.tar"] + [f"{knights[i]['layer']}/layer.tar" for i in sorted(knights)]
manifest_str = json.dumps(manifest)
manifest_fobj = BytesIO(manifest_str.encode())
info = tarfile.TarInfo(name=manifest_fn)
info.size = len(manifest_str)
newtar.addfile(tarinfo=info, fileobj=manifest_fobj)
# Create new config
config = json.loads(origtar.extractfile(config_fn).read())
config['rootfs']['diff_ids'] = ['sha256:d26c760acd6e75540d4ab7a33245a75a5506daa7998819f97918a39632a15497'] + [f"sha256:{knights[i]['sha256']}" for i in sorted(knights)]
config_str = json.dumps(config)
config_fobj = BytesIO(config_str.encode())
info = tarfile.TarInfo(name=config_fn)
info.size = len(config_str)
newtar.addfile(tarinfo=info, fileobj=config_fobj)
origtar.close()
newtar.close()
On running this, it will create the new tar, which I can import:
$ python3 knights.py
$ docker image rm -f antioch:latest
Untagged: antioch:latest
Deleted: sha256:f9a1a435538a8f2fb4c0f15b85a5f46f90e3dde228506fabc70989cb4ea60a09
$ docker image load --input antioch-new.tar
Loaded image: antioch:latest
$ docker run -it antioch
AntiochOS, version 1.32 (build 1975)
Type help for help
>
From here, I’ll just run consult
, and it prints it out as ASCII art:
Flag: Five-Is-Right-Out@flare-on.com