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:
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 :
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 :
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.
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:
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
.