Do you know what ret2dlresolve is?
nc chal.amt.rs 1344
author: Zek
Check protections:
Check source:
Ok, so this is a very simple binary. Given theres no canary and PIE, lets check for any gadgets:
Hmm… it doesn't look like theres anything that useful here.
The challenge mentioned "ret2dlresolve", so I looked into it.
This article has an excellent description of ret2dlresolve on x64 binaries, but the gist of it is forging three fake sections, JMPREL
, DYNSYM
, and STRTAB
, then calling _dl_runtime_resolve()
with a value (reloc_arg
) on the stack to resolve the libc address of system()
into the GOT. system()
is then called when _dl_runtime_resolve()
returns.
The problem is that we have barley any control over the registers. The article assumes we have some pop gadgets. Some things to note:
gets()
returns, a pointer to a writeable region in libc is left in $rdi (_IO_stdfile_0_lock
), but this address is randomized with ALSRleave
instruction and store a value in $rbp, then jump back to these instructions:
which will allow us to write into the .bss.
system()
call.So, heres the attack:
gets()
again
gets()
call loads in the stack frame/data for steps 4 onwardsgets()
call has space on the stack and won't overwrite our data [might be unneccesary]gets()
from the PLT, not main()
, loading the "/bin/sh" string into $rdi
gets()
modifies the value in _IO_stdfile_0_lock
._dl_runtime_resolve()
with reloc_args
on the stackI decided to use 0x404d48 as the start of the fake sections, mainly to avoid a segfault in _dl_runtime_resolve()
when the process state was saved to the stack.
I stole borrowed a payload from that article I linked before, and with a little bit of tweaking:
Here's my solve script:
Flag: amateursCTF{i_dont_know_how_but_they_found_me}
Not pictured here: hours of fixing stack alignment
🕳️
Elixir.Program.beam enc.txt code.🕳️
author: Emerald Block
A quick search revealed that Erlang/Elixir has a builtin function for disassembling .beam
files, :beam_disasm.file
.
oops
At first I had trouble disassembling the .beam
file without some error about bad data. I then learnt what Elixir.[whatever].beam
files actually are: Compiled libraries that can be accessed e.g. through the iex
Elixir interactive shell. Attempting to access a function on Program
in the shell printed an error that Erlang 24 was too old; upgrading to 26 fixed that!
…after learning how even to write basic Elixir…
This prints the "bytecode" in a format like an abstract syntax tree. Unfortunately I couldn't find any documentation and only found an article explaining a few new instructions. I also compiled a module to see how some things translated, but a lot of my analysis was guesswork. (As I was writing this I found an introduction which also links a list of all instructions, oops!)
The entire code is very long; here's an annotated snippet:
Key observations:
{instruction, ...data, output_register}
.:x
registers are used for parameters, {:x, 0}, {:x, 1}, ...
, and the return value, {:x, 0}
.{:f, label_number}
is a GOTO and usually represents where to jump if a check fails.1..-1//1
, are 0-indexed and inclusive.get_token
is by far the most complicated and also the most critical to analyze correctly, since it's what actually parses the code.🕳
file contents. It uses binary pattern matching applied to the result of String.at([arg0], 0)
to split into multiple cases, each of which asserts its binary representation is of a certain length.
At first I was confused how a character could have different lengths, until I realized that strings are UTF-8 and String.at
returns a grapheme, which has variable length (a code point is 1 to 3 bytes, and I just realized that tacking on a variation selector is what allows for 6-byte graphemes).
I manually translated the operations into a more readable format,
and then translated parts of it into pseudocode until I figured out what was going on:
tokenize(x0) { if x0 == "": x0 = nil return x0 = get_token(x0) { y0, x0 } = x0 x0 = tokenize(x0) # cons cell representation # so this is prepending y0 to x0 x0 = [y0, x0] return x0 }
tokenize(text) { if text == "": return nil info = get_token(text) { token, text } = info tokens = tokenize(text) return [token, tokens] }
So for tokenize
, The net effect is repeatedly performing { next_instruction, remaining_text } = get_token(text)
. (This was by far the easiest nontrivial function to analyze!)
Because get_token
performs string slicing to return remaining_text
, I was worried I'd have to deal with UTF-8 encoding, but thankfully all operations used work at the level of graphemes. By also looking at code.🕳
with a hex editor, I determined that get_token(text)
operates as follows:
char
be the first grapheme of text
.char
is 226, extract the next two bytes as a (big-endian) integer, and…
{:input, text[1:]}
(Python-style half-open interval slicing).{:output, text[2:-1]}
.text
by "*"s to obtain list
.list
(and assign the resulting list to list
) and parse them as decimal integers to obtain a
, b
. (I figured out the order later.)list[:-3]
with "*"s to obtain text
.{{:reverse, a, b},
text}
.Note how text
is consumed from both ends! Anyway, I could now decode the instructions:
['output', ('reverse', 1291, 1321), ('reverse', 1435, 1448), ...much more..., ('reverse', 43, 1090), 'input', 'input', 'input', 'input', 'input', 'input', 'input', 'input', 'input', 'input', 'input', 'input', 'input', 'input', 'input', 'input', 'input']
I now took the opportunity to analyze main(filename)
, which is really short when you ignore the file error handling. It calls tokenize
on the contents of the file, reverses the result, and invokes run
. That explains the reversed order.
run(ops)
calls run(ops, "")
. Analyzing run(ops, str)
was a lot easier since I already know the (very small) instruction set and could guess what it does. It works as follows:
ops
to obtain op
.op
is a tuple, assert it begins with :reverse
, and…
a
, b
be the other elements of op
.[0, a-1]
and [a, b]
.run(ops, [idk])
.op
:
:output
…
str
to standard output.:bs_create_bin
.run(ops, [idk])
.:input
…
"> "
.:bs_create_bin
.run(ops, [idk])
.So the only manipulation is reversing closed intervals, which can be undone by performing the operations in reverse:
!!!EYES ONLY - DESTROY AFTER READING!!! Agent Larry, really boring request extremely useless information "amateursCTF{sp3cial_ops_agen7_larry_1nb0und}" utterly pointless instructions completely meaningless goal unnecessary backup plan wholly insignificant note Farewell. {.;,;.}
Flag: amateursCTF{sp3cial_ops_agen7_larry_1nb0und}
unsolved :(
However… I have a& difficult request to ask& of you.
…
I would like you to walk& to the end of the room by yourself.
Forgive me for this.
- Toriel
author: Zek
Extracting the archive results in 4 files: data.win, f.txt, MiniTale.exe, and options.ini. Like revtale-1, this is a Game Maker Studio game. Opening the exe, we can pretty quickly see its a mod of Undertale.
Opening it in UndertaleModTool (a tool to analyze Game Maker Studio games) gives out an error.
Given the description, we probably have to somehow get to the end of the room. The hallway seems to extend infinitely. However, if you just walk backwards, its clear you get teleported back at a certain point.
We probably have to bypass this wall/teleporter/thing. Thankfully, this is pretty easy with Cheat Engine. Cheat Engine is a debugger of sorts that allows you to attach to a program, and scan for/edit memory addresses (along with other features).
Lets attach RevTale.exe, and create a new scan. From here, I spammed 5 types of scans until I got to ~40 addresses left:
After this, I was left with a bunch of values that all seemed to be the x coordinate of the player, but when I changed some of them, they didn't change the position and were immediately reverted. These are likely related variables, but dont control the position. From here, it was a guess and check of editing each address until…
There we go!
Flag: amateursCTF{d0_y0u_pr3f3r_butt3rsc0tch_0r_c1nnam0n?}
i ran out of ideas so here is a login page challenge
author: goose
We are able to template any String
field into the username and password; however, the username input has a check to ensure that the flag field is not contained in the input, and the password is stored as a hash, not as plaintext.
There is one observation that we can make off the bat: the salts for the bcrypt hashes are static. To me, this indicates that we likely need to mess with the hashed password rather than try to bypass the filter on the username.
After some searching, we found that bcrypt only uses the first 72 bytes of the input password string when generating a hash. We can abuse this fact combined with the fact that salts are static to find the flag character-by-character.
Because we can use any arbitrary-length string as our password, we can simply pad the password with 71 bytes of known junk and template the flag so the password is the the 71 bytes of junk concatenated with the flag. Since the salts are static, the resulting hash will always be equivalent to that junk concatenated with the same character as the start of the flag, so we can then check the equality of that hash against the hash of the junk concatenated with different possible characters until we find a character that results in the same hash. This character is the first character of our flag.
We can use a similar method to find the rest of the flag; however, our junk will now also need to contain the start of the flag that we already found. After running the below script, we get the flag. (thanks for putting a capital A in the flag )
Flag: amateursCTF{1_l0v3_l0gin_pAges}
Uh, I modified the fake onion and now it's hoarding flags with an unreversable cipher?? Please pilfer them for me.
nc chal.amt.rs 1415
author: Emerald Block
This challenge's cipher is shown below, where key
is 16 cryptographically secure random bytes.
When we connect to the server, we can get the encrypted flag and perform arbitrarily many encryptions.
Let and be block
and idk
after rounds. The algorithm is thus
where , the input is , and the output is .
Note that F()
is only ever called with a bytestring of length 1. The extremely direct usage of MD5 probably means the 256 bytestrings of length 15 that F()
outputs are as good as truly random. (Also, I don't know anything about MD5.)
I noticed two key vulnerabilities:
To avoid clutter, we write instead of .
The next simplest thing to encrypt is where we fall into the trap on the second iteration, i.e. . For convenience, let the input start with , so that results from after the first XOR; specifically, .
Let . Since we have no clue what is, let us guess some value for and hope that the resulting has residue , in which case the cipher performs
with the last two repeating until ending with the latter. Thus, if we guessed a right value, the other array entries should have no effect on the last two elements of the encrypted array.
So, to check whether our choice was right, we can encrypt with a few different choices for the other entries and confirm that the last two elements are always the same. (Note that checking whether the last element of the output has residue 15 is basically useless, because the cipher almost always falls into the trap.)
Furthermore, we only need to guess the values through , since the XOR with just permutes the residues modulo , so exactly one of those guesses is correct. Since the check gives no false negatives, if exactly one guess passes, we know it must be correct. (I found that choices for the other entries for each guess is usually enough to fail all wrong guesses.)
Since we know the values of the input and output arrays, we can calculate as .
To recover the rest of , we need to add another step to our attack so that is XOR'd in an even number of times and disappears. (Note can be any value with residue ; we choose for simplicity.) We can try to have and fall into the trap on the third iteration, though this requires guessing . The cipher performs
with the last two repeating until ending with the latter. Notice how the two XORs with are actually offset by one! Thus, the output contains
It also contains , so we can calculate and so on to recover the entirety of .
We just need to verify which guess for is correct. We can just check that the last two values of the output are and . Since this produces no false negatives, if exactly one guess passes, we know it must be correct. (Heuristically, the likelihood of a false positive from each of the wrong guesses is about in .)
Now that we know , notice that our first attack depends only on and , and we know the former. Thus, we can reemploy that attack to calculate for all with residue , no guessing needed!
I choose to fill the remaining entries of the input with so that the XOR with cancels out, leaving behind exactly in the first entries of the output.
For the remaining where does not have residue , we can again employ a variant of the first attack, now knowing . (We again use for simplicity.) Note that since may no longer be , we have to position the trap at in the input.
Let . We guess , which should result in
If we guessed the right value, then we can recover and , and is just our guess, giving us all of .
To check a guess, we can just verify that the last two values of the output are what we expect. If we end up with more than one passing guess, we can't do much other than record both (or all) possibilities for and try every combination of possibilities when recovering the flag.
This is by far the most time-consuming part of recovering the masks, since for each of the values of , we need to check different possibilities for . The chance of false positives is decently important here, since if a value of gets a false positive, the number of combinations doubles. (The possibility of multiple false positives for a single is negligible.)
Again, heuristically, the chance of a guess being a false positive is in , so there are about false positives, meaning , which is well within manageable limits. The exact number of false positives will vary, but since the cipher takes on the order of operations, any number of possibilities below a million should be fine.
Below is my implementation. To avoid network speeds being a bottleneck, we can concatenate and send multiple blocks at a time, since the cipher operates on each block independently.
oops
Initially I directly tested this code against the remote, which was both much slower and meant I couldn't check whether the s were correct without recovering all of them and implementing the flag decryptor. Then I realized I could test locally and print out the real masks for debugging!
As mentioned above, and usually fall into a trap. Going in reverse looks like the following:
Note we do not know how many rounds are spent before the trap, so step 3 could occur at any time. Since undoing the trap either XORs the array with or doesn't, we can just take both possibilities and reverse them according to step 2, on each round trying out step 3 to see if it yields valid ASCII.
Below is the decryptor, which expects the last two lines printed by the earlier code to be in data.txt
. I chose to iterate over all masks for each chunk separately and assume the first valid ASCII decoding is correct.
Fortunately, while there were 786432 possibilities, each chunk tried no more than 50000, with half of them basically instant.
Flag: amateursCTF{i_love_cycles_4nd_cycl3s_anD_cYcl3s_AND_cyCLEs_aNd_cyc135_4319d671}
What is life if not incomprehensible nonsense? Please untangle this for me. I'm hungry.
Flag is valid ascii.
author: Emerald Block
Unzipping out.zip
gives a huge text file out.txt
that begins 8638/12964/g0g0g0g0g0…
. Looking at checker.py
, the code is relatively straightforward and does the following:
player.txt
and assert it matches out.txt
, except for instances of ^0
which must be replaced with either ;3
or $0
. (Hence out.txt
is like a template to be filled in at select cells.)player.txt
as a grid of 2-character cells (12964 rows of 8638 cells, as specified by out.txt
).
$0
, ;3
, and g0
(ignoring ^0
since it's replaced).;3
and exactly one of the nine cells in the 3×3 area centered on the cell is $0
, replace it with $0
.
;3
s are on the edge.$0
, then generate the flag by converting the choices we made for replacing ^0
to a bitstring (;3
is 0 and $0
is 1) and interpreting it as UTF-8 (which will just be ASCII, as per the flavortext).This immediately reminded me of a Minetest challenge LiveOverflow covered that involved parsing and solving a circuit constructed on a 2D grid.
I realized that ;3
was like wire that could be "activated" to become $0
, which propagates. g0
does nothing and so is just a filler cell. To visualize the grid, I rendered it as pixels to form a raster image:
chars | nickname | color |
---|---|---|
g0 |
EMPTY | black |
^0 |
CHOICE | white |
;3 |
WIRE | red |
$0 |
POWER | blue |
Zooming in, we see the choice cells form a row at the top that propagate down, each splitting into two columns that enter a huge matrix of some kind of logic gate:
Similarly, there are slanted rows that start either powered or unpowered and travel through the matrix, with extra power sources along the way that presumably act as NOT gates. We can work out their exact behavior by applying the propagation rules manually. Recall that each cell is updated once in row-major order, and a wire becomes powered if exactly one neighboring cell is powered. The result is:
So it is indeed a NOT gate.
As for the complex gate, it has 3 inputs and 3 outputs, though the bottom 2 outputs are linked and the top 2 inputs come from either the same choice cell or a previous pair of bottom 2 outputs, so we know them to be the same:
It thus performs a XOR on the row value while letting the column value pass through.
Note that there is a slight variation with one wire cell missing at the left top input:
It thus lets both values pass through.
The net effect is that a given row's value is XOR'd with every column value whose XOR gate is not missing a wire cell, and NOT'd once for every power cell on its row. (Order doesn't matter due to commutativity of the operations.)
At the right edge, we find that the row values are merged with a different gate:
The weird connection from the end of the row to the left input of the gate is just flair. The gate itself performs:
where the middle cell simplifies from ¬(¬X)∧¬(¬S). Thus, it is an AND gate. The bottom-right-most cell is the cell the script checks is powered, so we need every row to be powered at the end.
Let denote whether the -th choice cell was replaced with power () or not (). Let denote whether the gate at the -th row and -th column is a XOR gate () rather than a pass-through ().
If row has powered cells (including the initial leftmost cell), the requirement that the row be is powered is given by
since XOR is equivalent to addition modulo 2.
The resulting system of equations can be expressed as a Boolean matrix equation:
It remains to determine and by extracting the positions of the NOT gates and the missing wire cells.
The equation can be solved with e.g. Sage.
Flag: amateursCTF{5hR13kbu1bS_Ar3_Th3_B3s7_b3c4uS3_th3y_a110W_m3_t0_c0n5trUcT_CircU1ts_68bbf319}
oops
When I solved this challenge, I didn't realize that the matrix was singular and just assumed the solution .solve_right()
gave was unique. As a result, the solution I initially found was invalid ASCII. I fiddled with the data and found that inverting the matrix entries rather than the output vector got the flag.
Only as I was writing this did I realize what was going on. That the flag is a solution to the altered system is because for solutions with an odd number of bits, which the flag happens to be, the systems are equivalent. Furthermore, both matrices' kernels have dimension 1, so there is a "line" of only two solutions, and Sage happened to pick the (in)correct one.
We're building the next generation flag sharing social media inside discord! Join us for the fun flag sharing activity, well uhm actually I might need to finish testing my code. Use code "flag" to get instant access to the flag!
https://discord.com/oauth2/authorize?client_id=1223421353907064913&permissions=0&scope=bot
author: goose
While loading the /oauth2/authorize
link provided in the challenge, we see the following response sent while loading the page:
It turns out that the description of the challenge is very important:
We're building the next generation flag sharing social media inside discord! Join us for the fun flag sharing activity, well uhm actually I might need to finish testing my code. Use code "flag" to get instant access to the flag!
The key word here, activity, means a Discord activity. The application.flags
field being 131072 indicates that the application is embedded within the Discord client.
Googling this, it means the bot is associated with an activity (those voice channel games).
If we look at the network tab while we join another activity, we can see that a request to https://[application ID].discordsays.com
is made. According to Reddit, this is the domain where Discord activities are hosted. After going to https://1223421353907064913.discordsays.com, we find a place to input some "launch code." We get the flag after typing "flag" into the input (given to us in desc).
Flag: amateursCTF{p0v_ac3ss_c0ntr0l_bypass_afd6e94d}