A look back at a cybersecurity event that was small in scale but rich in technical depth: Auvergn’Hack. While everyone’s eyes were on the FIC, another event quietly made its mark—far from the hustle and bustle of Lille: Auvergn’Hack. A down-to-earth, tech-focused conference with hands-on workshops and talks in the morning, followed by a CTF in the afternoon. During the CTF, I focused on the reverse engineering challenge. This article is the write-up of the “driverlicence1” challenge.

The method below is a straightforward approach. But before that, I initially tried to make the decompiled code more readable by identifying structures, just like we would in C++. However, facing the daunting task of cleaning up compiled Rust code, I decided to switch to a more efficient method—better suited for this type of challenge.

Challenge file (sha256sum : f5529eb120b6f676f4339514b997457f5752877a7ae0dc47cb8719aa3c2d39cd) : challenge

Basic checks

The first step is to take a quick look around and get familiar with the binary. It’s an ELF format, which is the most common executable format on Linux. It’s compiled for 64-bit systems and uses dynamic library linking :

$ file driverlicence1 
driverlicence1: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 4.4.0, BuildID[sha1]=46add9e05a6e4e71549187740adc2f2ae8cad950, not stripped

After analyzing the strings, the news isn’t great: the binary was written in Rust. That’s going to make things trickier to analyze—especially since I had never reversed any Rust code before this challenge :

$ strings driverlicence1 | grep '.rs$'
alloc/src/str.rsalloc/src/fmt.rs
CLICOLORNO_COLORextern "src/main.rs
[.] Loading circuit...[.] Checking Track Surface...[.] Preparing Flags...[.] Heating Tire...[.] Checking Pit Lane...[.] Verifying pilot abilities...[-] Pilot has no driving licencecould not initialize ThreadRng: std/src/io/buffered/bufwriter.rsstd/src/os/unix/net/ancillary.rsstd/src/sys/sync/rwlock/futex.rs
ACcapacity but is  (bytes , max = /home/owl/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sync/once.rs/home/owl/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/slice.rs/home/owl/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/str/pattern.rs/home/owl/.cargo/registry/src/index.crates.io-6f17d22bba15001f/num_cpus-1.16.0/src/linux.rs/proc/self/cgroup/proc/self/mountinfocpu.cfs_period_uscpu.max/home/owl/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed.rs/home/owl/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/raw_vec.rs
/home/owl/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/io/mod.rsstream did not contain valid UTF-8cannot access a Thread Local Storage value during or after destruction/home/owl/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/thread/local.rs/home/owl/.cargo/registry/src/index.crates.io-6f17d22bba15001f/rand-0.9.0/src/rngs/thread.rs
[...]

When running the program, the following strings are displayed. Trying to pass parameters doesn’t change anything. There’s no interaction or user input expected.

[.] Loading circuit...
[.] Checking Track Surface...
[.] Preparing Flags...
[.] Heating Tire...
[.] Checking Pit Lane...
[.] Verifying pilot abilities...
[-] Pilot has no driving licence

Digging further into the strings, I found one that says: “Pilot is ready to race”. I’m guessing the goal will be to find a way to trigger the correct execution path.

[.] Loading circuit...[.] Checking Track Surface...[.] Preparing Flags...[.] Heating Tire...[.] Checking Pit Lane...[.] Verifying pilot abilities...[-] Pilot has no driving licencecould not initialize ThreadRng: std/src/io/buffered/bufwriter.rsstd/src/os/unix/net/ancillary.rsstd/src/sys/sync/rwlock/futex.rs
[+] Pilot is ready to race

A few additional checks (like strace, memory inspection, etc.) didn’t reveal anything useful. Time to bring out Ghidra for a deeper analysis.

Ghidra

To move quickly, I decided to start from that string and work backwards to find an execution path that would trigger it. To do that, I searched for the string “ready to race” and used XREFs to identify where its address is used in the program:

alt text

Following the XREF, I landed in a section of the program with two overly strict if conditions guarding the path to the desired execution flow, which ends up at address 0x00108d0d :

alt text

One way to find the actual runtime address is to open the program in a debugger and set a breakpoint on main. Then, using GDB, I can find the base address of the program:

$ gdb ./driverlicence1
(gdb) break main    
Breakpoint 1 at 0x9230  
(gdb) run  
Starting program: /home/gebroise/Téléchargements/driverlicence1    
[Thread debugging using libthread_db enabled]  
Using host libthread_db library "/lib64/libthread_db.so.1".  
  
Breakpoint 1, 0x000055555555d230 in main ()  
(gdb) info proc map  
process 6207  
Mapped address spaces:  
  
Start Addr         End Addr           Size               Offset             Perms File    
0x0000555555554000 0x000055555555a000 0x6000             0x0                r--p  /home/gerboise/Téléchargements/driverlicence1
0x000055555555a000 0x00005555555a4000 0x4a000            0x6000             r-xp  /home/gerboise/Téléchargements/driverlicence1
[...]

To align addresses between GDB and Ghidra, we’ll update Ghidra’s memory mapping. This will make navigating and analyzing the binary much easier later on. In this case, the base address is: 0x0000555555554000. The image below shows where to update the base address in Ghidra’s memory map settings :

alt text

We then find that the address of the first comparison instruction is at 0x000055555555cd0d. At this point, all that’s left is to hope the program actually hits this breakpoint. And It’s work !

(gdb) break *0x000055555555cd0d
Breakpoint 2 at 0x55555555cd0d
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/gebroise/Téléchargements/driverlicence1 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

Breakpoint 1, 0x000055555555d230 in main ()
(gdb) c
Continuing.
[.] Loading circuit...
[.] Checking Track Surface...
[.] Preparing Flags...
[.] Heating Tire...
[.] Checking Pit Lane...
[.] Verifying pilot abilities...

Breakpoint 2, 0x000055555555cd0d in driverlicence::main ()
(gdb) 

In Ghidra, it can be a bit tricky to find the exact address when variables are referenced in the format qword ptr [RSP + local_80], especially if you’re trying to determine the variable’s offset on the stack.

alt text

A quick solution is to check the instruction where the breakpoint was set directly in GDB:

(gdb) x/i $pc
=> 0x55555555cd0d <_ZN13driverlicence4main17ha7e5f19755fa0981E+1229>:   cmpq   $0x539,0x78(%rsp)

To enter the function, you need to set the value 0x539 at the address 0x78(%rsp):

(gdb) set *(long *)($rsp + 0x78) = 0x539

Analyzing the second condition, it seems to be a memory allocation check—so there’s little chance we’ll be able to bypass it:

alt text

So I let the process run normally… and that’s a win :

(gdb) c
Continuing.
[+] Pilot is ready to race
ZiTF{ca62f78aa55bbb1922d2255a44c645a3}
[Inferior 1 (process 8036) exited normally]

Conclusion

I really enjoyed this challenge. It wasn’t particularly difficult, but it gave me the opportunity to dig into some reverse engineering on a Rust binary and explore a few tricks to solve it more efficiently. Big thanks to Auvergn’Hack for this fun and well-crafted challenge!

The next post will cover the follow-up to this challenge: driverlicence2.