Tags: linker misc
Rating:
# LakeCTF: So What?
*Misc, 50 Points, (but used to be 500).*
Follow my thoughts on this journey.
The description already taunts us.
> Are you a shellcoding pro? If not, so what? (salt guaranteed once you know the solution)
The author is playing a game against us. They want to see us suffer. For them, the greatest happiness is to scatter their enemy, to drive us before them, to see our cities reduced to ashes and to see those who love us shrouded in tears. But to beat them, all we need to to is play along in their game of gleeful malice, with intent.
**Obviously, the obvious path is wrong**.
## The Dockerfile
We received two files and start with the obviously wrong one to look at. However, it does not seem to be that interesting, apart from needing special options to be built and launched.
```bash
$ DOCKER_BUILDKIT=1 docker build -t lakesw .
$ DOCKER_BUILDKIT=1 docker run --privileged \
--rm -p5000:5000 -it lakesw
```
Importantly, it does not contain the flag.
## The Flag's Residence
So where is it? We find it in the `challenge.py` file. Or rather, a placeholder.
> For the reader new to playing CTF: Oftentimes, challenges have a server that runs the exact same program as we receive to look at, except that it has a different flag. That makes local testing more reliable but keeps the flag securely hidden in its grove.
But actually, it is not ... *really* to be found inside the `challenge.py`. Because the `challenge.py` file itself is not accessible inside the jail in the docker where the challenge server is running at. And the code writes the part with the flag placeholder to a file.
```python
main_source = """
#include <stdio.h>
extern int win();
#ifdef flag
int win() {
printf("Congratulations!\\n");
printf("EPFL{https://youtu.be/FJfFZqTlWrQ}\\n");
}
#endif
int main() {
win();
}
"""
with open("main.c", "w") as f:
f.write(main_source)
print("Stage A", file=sys.stderr)
os.system("gcc main.c -shared -o libflag.so -Dflag")
print("Stage A.1", file=sys.stderr)
#os.system("cat libflag.so")
print("Stage B", file=sys.stderr)
os.system("gcc main.c -L. -lyour_input -o main")
print("Stage C", file=sys.stderr)
os.system("LD_LIBRARY_PATH='.' ./main")
```
This snippet is only the end of the code, but we can already see the obvious solution - which must be a trap, otherwise there would be no salt at the end. We clearly can provide some `your_input` library that the `main.c` is linked against.
> The **`print("Stage")`** and **`os.system("cat libflag.so")`** were added by me, to debug. Since the flag is inside `main.c` it is also in `libflag.so`, and being able to `cat` any of these would give the flag.
## The Start of the File
Oh btw, this is the first part of the file: Some annoying filters but basically we provide input and it first runs it through **`as`**, then **`ld`**, and then as we have already seen through **`gcc`** to create both `libflag.so` and an executable called `main`, which is then run.
There was more code, but it is useless to understanding so I removed it for brevity.
```python
#!/usr/bin/env python3
os.chdir("/tmp")
print("Please input the shellcode to your shared library")
print("This shared library will be assembled and linked against ./main")
print("Try to make ./main print the flag!", flush=True)
last_byte = b""
binary = b""
while True:
byte = sys.stdin.buffer.read(1)
binary += byte
# allow cancer constraints here
# man, I really wish there was a way to avoid all this pain!!!
# lmao
if False:
if b"\x80" <= byte < b"\xff": # 1. printable shellcode
print("Quit 1: Printable")
quit()
if byte in b"/bi/sh": # 2. no shell spawning shenanigans
print("Quit 2: /bi/sh")
quit()
if b"\x30" <= byte <= b"\x35": # 3. XOR is banned
print("Quit 3: XOR")
quit()
if b"\x00" <= byte < b"\x05": # 3. ADD is banned
print("Quit 3: ADD")
quit()
if byte == b"\n" and last_byte == b"\n":
break
last_byte = byte
if len(binary) >= 0x1000:
exit(1)
with open("libyour_input.so", "wb") as f:
f.write(binary)
print("Assembling!")
os.system("as libyour_input.so -o libyour_input.obj && ld libyour_input.obj -shared -o libyour_input.so")
```
## Excurse
We recall that there was some quirky behaviour where invalid files are treated as linker scripts. Since the description clearly stated that the obvious path is not a fun one, let us verify this idea.
```
$ nc 127.0.0.1 5000
> Welcome!
> Please input the shellcode to your shared library
> This shared library will be assembled and linked against ./main
> Try to make ./main print the flag!
> Send the assembly (double newline terminated):
$ a
$
> libyour_input.so: Assembler messages:
> libyour_input.so:1: Error: no such instruction: `a'
> Stage A
> Stage A.1
> Stage B
> /usr/bin/ld:./libyour_input.so: file format not recognized; treating as linker script
> /usr/bin/ld:./libyour_input.so:0: syntax error
> collect2: error: ld returned 1 exit status
> Stage C
> sh: 1: ./main: not found
> Assembling!
```
Indeed. It says "treating as linker script". So we try a few things with that.
* **`input(libflag.so)`** would link `libflag.so` so that running `./main` would actually just print the flag. But this does not work because the filters filter out the **`b"i"`** byte.
We can partially circumvent this with **`INPUT`** but the filename is case-sensitive.
* Sometimes **`gcc`** does weird things and automatically prepends `lib` to the front of a filename and `.so` to the back. Like in the line where they specify the **`gcc`** flag
**`-lyour_input` **and then it gets interpreted as **`libyour_input.so`**. So let's try this too: Submitting **`INPUT(flag)`** ... simply does not do that:
```
$ nc 127.0.0.1 5000
Welcome!
Please input the shellcode to your shared library
This shared library will be assembled and linked against ./main
Try to make ./main print the flag!
Send the assembly (double newline terminated):
INPUT(flag)
libyour_input.so: Assembler messages:
libyour_input.so:1: Error: invalid character '(' in mnemonic
Stage A
Stage A.1
Stage B
/usr/bin/ld: cannot find flag: No such file or directory
collect2: error: ld returned 1 exit status
Stage C
sh: 1: ./main: not found
Assembling!
```
## The First Flag
Let us spend hours reading up, to no avail, about setting a [custom entrypoint](https://stackoverflow.com/questions/27895900/does-gnu-assembler-add-its-own-entry-point), [dynamically including](https://stackoverflow.com/questions/5873722/c-macro-dynamic-include), and skimming through the whole manual of [the linker](https://users.informatik.haw-hamburg.de/~krabat/FH-Labor/gnupro/5_GNUPro_Utilities/c_Using_LD/ldLinker_scripts.html#Symbol_names) and [assembler](https://ftp.gnu.org/old-gnu/Manuals/gas-2.9.1/html_node/as_toc.html).
Somewhere pretty soon along this way, an announcement was made in the LakeCTF Discord:
> We have fixed `so what ?` and will therefore release `so what? revenge` in order to let you play it in the intended way.
After a quick look at the diff and then an support ticket to make sure they did not just forget to actually update their files, I knew: The files for the revenge challenge were the same. Except for the trailing newline, but I could not imagine how that would have any impact...
```diff
$ git diff --no-index sowhat/challenge.py sowhat2/handout.py
int win() {
printf("Congratulations!\\n");
- printf("EPFL{https://youtu.be/FJfFZqTlWrQ}\\n");
+ printf("FLAG_HERE");
}
```
This made no sense so I decided to just solve the revenge first and then get the "easier" challenge for free. I spent another hour reading documentation, then got bored of thinking and decided not to think for a moment. Handing in this youtube link actually congratulated me!
## Revenge Flag
After continuing the mentioned reading for a day, interspersed with looking at other challenges, I finally gave up on the linker idea and instead attempted with low motivation to create a **`win`** symbol [without the `i` character](https://stackoverflow.com/questions/40352929/how-to-create-symbols-with-weird-names-in-assembler) by [escaping hex digits in **`as`**](https://ftp.gnu.org/old-gnu/Manuals/gas-2.9.1/html_chapter/as_3.html), and to use [pwnlib encoders](https://docs.pwntools.com/en/stable/encoders.html#pwnlib.encoders.encoder.encode) to generate an input that would get past the filters, crash the assembler, and then be linked to anyway. Or actually just writing a piece of assembly that can print the files in the working directory.
Eventually, I returned to the linker idea and read the [manual](https://users.informatik.haw-hamburg.de/~krabat/FH-Labor/gnupro/5_GNUPro_Utilities/c_Using_LD/ldLinker_scripts.html#Symbol_names) *again*.
> INCLUDE filename
> Include the linker script filename at this point. The file will be searched for in the current directory,
But this does not support globbing, and I still can not insert the lowercase character **`i`**.
> INPUT (file , file , ...)
> INPUT (file file ...)
> The INPUT command directs the linker to include the named files in the link, as though they were named on the command line.
> [...]
>
> * If you use **`INPUT (-l file )`** , `ld` will transform the name to **`lib file.a`**, as with the command line argument **`-l`**.
I had tried this before. At the very start of my journey. It did not work. Still not.
**`INPUT (-l flag )`** gives
```
-/usr/bin/ld: cannot find l: No such file or directory
/usr/bin/ld: cannot find flag: No such file or directory
```
Huh. So what if I do once more what is obviously wrong and deviate from the manual website by omitting whitespace?
**`INPUT(-lflag)`** gives
```
$ nc chall.polygl0ts.ch 3201
Welcome!
Please input the shellcode to your shared library
This shared library will be assembled and linked against ./main
Try to make ./main print the flag!
Send the assembly (double newline terminated):
INPUT(-lflag)
libyour_input.so: Assembler messages:
libyour_input.so:1: Error: invalid character '(' in mnemonic
Congratulations!
EPFL{This_time_we_did_not_forget_to_remove_it_from_source_:)}
Assembling!
```
## Useless Conclusion
Hopefully, this kind of writeup is entertaining and shows also how one might go about a challenge. Even if I had not known that the linker sometimes does weird stuff, simply submitting garbage was enough to be informed about that in a warning. If you don't know who to be, be a human fuzzer.
I was right, the challenge was fun. The author was right, the challenge solutions (**both!**) made me salty.
And the manual was wrong.
*Lucid, 26.09.2022*