NSEC25 - Hit the Jackpot
NorthSec 2025's theme was a heist on a luxury cruise ship. As one can expect from a luxury cruise ship, there was a casino on board. Who were we not to try our luck?
Hit the Jackpot was a 34 points track spread across 9 challenges and split in 4 sub-tracks. Unfortunately, we only managed to solve 5 challenges during the event, but I took the liberty to finish the 6th challenge afterward and include it in the writeup too.
I wrote this right after NorthSec last year, but forgot to post it. Better late than never :)
1/9 [reels] 1/1 (1 point)
The track begins at the slot machine, on the casino floor. Standing alight, the machine beckons patrons of the cruise ship to insert their access card and try their luck.

Once inserted, the player may select the stakes at which they want to play.

After playing for a few rounds, you notice that one of the displayed symbols on the reels seems to be letters, starting with FLAG; this is probably the first flag.

There we go, maybe we haven't won big, but we aren't leaving empty handed.
2/9 [rng] 1/4 (2 points)
Play a couple games on the white stakes and you'll notice something strange: it seems like they are all the same. We can exploit that by playing once to find which rounds are winning, then betting the minimum on a losing round and the maximum on a winning round.
In our experience, it went like this:
- Bet $1 on round 1, 2 and 3;
- Bet $47 on round 4, you'll win $141 with three cherries;
- Bet $1 on round 5, 6 and 7;
- Bet $138 on round 8, you'll win $414 with three cherries;
- Bet $1 on round 9 and 10; and finally
- Bet $412 on round 11, you'll win $2060 with three bars and reach the maximum wins for the white stakes.
The flag is then revealed:

3/9 [web] 1/2 (1 point)
This one was pretty straightforward. Upon closer inspection of the slot machine, we noticed a sheet taped to the side with a big nice QR code at the bottom. The QR code reads: WL-ef2582640681ad8a1ac57583dcf98691ad. Beneath the QR code was the URL: http://wonderlight.ctf.

Upon visiting the website, we are greeted with what looks like a support website for slot machine owners. Entering the model number, we can see some technical specifications and the flag:

4/9 [web] 2/2 (1 point)
On the same page where we found the previous flag, we can see that a service password is required to download the firmware. Using the power of right-click > view source code, we can reveal the service password written in cleartext and validated client-side:

We can also copy the download link and retrieve the firmware. When the download finishes, the flag can be found in the README.md file inside the archive.
5/9 [reverse] 1/2 (3 points)
Now we get into the juicy part (and the last challenge we managed to solve during the CTF).
The firmware we downloaded previously is actually an AppImage binary, a portable executable for Linux. They are usually shipped with all the required dependencies for the program to run properly on any system.
Thankfully, there is a command to extract the files from an AppImage:
./slot-machine.AppImage --appimage-extract
This command will drop an AppDir directory and a squashfs-root symlink that points to that directory. We can ignore the symlink and dive into AppDir/.
Within AppDir/, a few things stand out:
- binaries named
AppRunandsharun .envfile- and a couple directories.
After some searching, we learned that sharun is a tool that can "run dynamically linked ELF binaries everywhere", according to its GitHub repo description. It works by generating a binary that will have all of its dependencies in a shared directory and does some magic to make it all work. I think that removing layers of abstraction on top of the binary makes the reversing process easier, so that's what I did.
I managed to run the actual binary using the following command:
LD_LIBRARY_PATH=./shared/lib/ ./shared/bin/slot-machine
After playing with the binary to see how it worked, we ran strings and found multiple instances of the string tauri throughout the file. We investigated that path and tried to find tools to extract assets from Tauri binaries, but with no success.
To look for interesting functions related to Tauri, we loaded GDB and ran:
info functions tauri
A lot of functions were displayed, but reading through them revealed an interesting one:
fn tauri_utils::assets::EmbeddedAssets::get(*mut tauri_utils::assets::EmbeddedAssets, *mut tauri_utils::assets::AssetKey)
Disassembling the function gives us the following:
gef➤ disas tauri_utils::assets::EmbeddedAssets::get
Dump of assembler code for function _ZN11tauri_utils6assets14EmbeddedAssets3get17h1046c86bf30ab87eE:
0x0000000100ea8260 <+0>: sub rsp,0x68
0x0000000100ea8264 <+4>: mov QWORD PTR [rsp+0x8],rdx
0x0000000100ea8269 <+9>: mov QWORD PTR [rsp+0x10],rsi
0x0000000100ea826e <+14>: mov rax,rdi
0x0000000100ea8271 <+17>: mov rdi,QWORD PTR [rsp+0x8]
0x0000000100ea8276 <+22>: mov QWORD PTR [rsp+0x18],rax
0x0000000100ea827b <+27>: mov QWORD PTR [rsp+0x20],rax
0x0000000100ea8280 <+32>: mov QWORD PTR [rsp+0x58],rsi
0x0000000100ea8285 <+37>: mov QWORD PTR [rsp+0x60],rdi
0x0000000100ea828a <+42>: call QWORD PTR [rip+0xbf7408] # 0x101a9f698
0x0000000100ea8290 <+48>: mov rdi,QWORD PTR [rsp+0x10]
0x0000000100ea8295 <+53>: mov rsi,rax
0x0000000100ea8298 <+56>: call QWORD PTR [rip+0xbf3142] # 0x101a9b3e0
0x0000000100ea829e <+62>: mov rsi,rax
0x0000000100ea82a1 <+65>: lea rdi,[rsp+0x40]
0x0000000100ea82a6 <+70>: call QWORD PTR [rip+0xbe2524] # 0x101a8a7d0
0x0000000100ea82ac <+76>: lea rdi,[rsp+0x28]
0x0000000100ea82b1 <+81>: lea rsi,[rsp+0x40]
0x0000000100ea82b6 <+86>: call QWORD PTR [rip+0xbf4e3c] # 0x101a9d0f8
0x0000000100ea82bc <+92>: mov rdi,QWORD PTR [rsp+0x18]
0x0000000100ea82c1 <+97>: lea rsi,[rsp+0x28]
0x0000000100ea82c6 <+102>: call QWORD PTR [rip+0xbe87cc] # 0x101a90a98
0x0000000100ea82cc <+108>: mov rax,QWORD PTR [rsp+0x20]
0x0000000100ea82d1 <+113>: add rsp,0x68
0x0000000100ea82d5 <+117>: ret
End of assembler dump.
This function seems to be used to retrieve an asset by name/id, so we can probably break near the end and dump the extracted asset to the disk. Let's try it out:
gef➤ b *tauri_utils::assets::EmbeddedAssets::get+113
Breakpoint 1 at 0x100ea82d1: file src/assets.rs, line 148.
gef➤ c
[ ... snip ... ]
gef➤ dereference $rsp
0x00007fffffff5e30│+0x0000: 0x000000000000000a ("\n"?) ← $rsp
0x00007fffffff5e38│+0x0008: 0x00007fffffff6058 → 0x000000000000000b ("
"?)
0x00007fffffff5e40│+0x0010: 0x00007fffe4001050 → 0x00000001010881ac → add BYTE PTR [rax], al
0x00007fffffff5e48│+0x0018: 0x00007fffffff6118 → 0x00000000000004e6
0x00007fffffff5e50│+0x0020: 0x00007fffffff6118 → 0x00000000000004e6
0x00007fffffff5e58│+0x0028: 0x00000000000004e6
0x00007fffffff5e60│+0x0030: 0x0000000101fd8700 → "<!doctype html>\n<html lang="en">\n <head>\n [...]"
0x00007fffffff5e68│+0x0038: 0x00000000000004e6
0x00007fffffff5e70│+0x0040: 0x00000000000004e6
0x00007fffffff5e78│+0x0048: 0x0000000101fd8700 → "<!doctype html>\n<html lang="en">\n <head>\n [...]"
Strings in Rust are a pointer to a struct with a length and a pointer to the data. We can see what looks like a pointer to a length at $rsp+0x8 and another at $rsp+0x18. Dereferencing those pointers, we get the following strings, the first one being the name of the assets being requested and the other being the content:
gef➤ x/s *((*(($rsp+0x8) as *mut u64)+0x8) as *mut u64)
0x101fa1810: "/index.html"
gef➤ x/s *((*(($rsp+0x18) as *mut u64)+0x8) as *mut u64)
0x101fd8700: "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <!--link rel=\"icon\" href=\"./favicon.png\" /-->\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n \n\t\t<link rel=\"modulepreload\" href=\"./_app/immutable/entry/start.Cx1_lLtp.js\">\n\t\t<link rel=\"modulepreload\" href=\"./_app/immutable/chunks/FwgTYjBD.js\">\n\t\t<link rel=\"modulepreload\" href=\"./_app/immutable/chunks/DGK3jQbY.js\">\n\t\t<link rel=\"modulepreload\" href=\"./_app/immutable/entry/app.CvAHx8w0.js\">\n\t\t<link rel=\"modulepreload\" href=\"./_app/immutable/chunks/Dfxzfz8y.js\">\n\t\t<link rel=\"modulepreload\" href=\"./_app/immutable/chunks/Y8tDg5xf.js\">\n\t\t<link rel=\"modulepreload\" href=\"./_app/immutable/chunks/C_s4oqxE.js\">\n </head>\n <body data-sveltekit-preload-data=\"hover\">\n <div style=\"display: contents\">\n\t\t\t<script>\n\t\t\t\t{\n\t\t\t\t\t__sveltekit_1oigkoo = {\n\t\t\t\t\t\tbase: new URL(\".\", location).pathname.slice(0, -1)\n\t\t\t\t\t};\n\n\t\t\t\t\tconst element = document.currentScript.parentElement;\n\n\t\t\t\t\tPromise.all([\n\t\t\t\t\t\timport(\"./_app/immutable/entry/start.Cx1_lLtp.js\"),\n\t\t\t\t\t\timport(\"./_app/immutable/entry/app.CvAHx8w0.js\")\n\t\t\t\t\t]).then(([kit, app]) => {\n\t\t\t\t\t\tkit.start(app, element);\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t</script>\n\t\t</div>\n </body>\n</html>\n"
The casting of Rust variables was a pain to figure out, thankfully we found this stackoverflow post where someone had a similar issue and it helped a lot. We can automate part of the process by adding the following to our .gdbinit:
b *tauri_utils::assets::EmbeddedAssets::get+113
command
x/s *((*(($rsp+0x8) as *mut u64)+0x8) as *mut u64)
x/s *((*(($rsp+0x18) as *mut u64)+0x8) as *mut u64)
end
It is pretty verbose, but at some point you end up dumping some JS with the flag. It's in the file "/_app/immutable/nodes/0.B3J0M_qK.js":

6/9 [reverse] 2/2 (6 points)
Being lazy is fine, but it came back to bite me in the end. This part of the challenge was not solved during the CTF due to lack of time, but here's how I solved it when I came back home.
First thing first, we have to dump the files for real this time and attempt to understand how it all works out. The forum post refers to the backend, so we need to identify how one talks to the backend in Tauri.
This gdb script worked great to extract the files in an orderly fashion:
# dump assets
shell mkdir -p dump
b *tauri_utils::assets::EmbeddedAssets::get+113
commands
set $filename = *((*(($rsp+0x8) as *mut u64)+0x8) as *mut u64)
set $content_start = *((*(($rsp+0x18) as *mut u64)+0x8) as *mut u64)
set $content_end = $content_start+*((*(($rsp+0x18) as *mut u64)) as *mut u64)
eval "dump binary memory ./dump/%p-%p.bin %ld %ld", $content_start, $content_end, $content_start, $content_end
eval "shell echo ./dump/%p-%p.bin:%s >> dump.txt", $content_start, $content_end, $filename
end
I tried to add a continue statement at the end, but for some reason it kept hanging after the first file. I have no idea why, so if you do, please let me know :)
The dump.txt file contains all of the filenames associated with the key used in the source code so that we can make sense of how they work together. Navigating through the app is necessary to download all of the files because the function we break on is only called when a file is retrieved.
A good grep -ri tauri ./dump/ helps me find interesting bits to look at, only one file matches: /_app/immutable/chunks/BEFjKjka.js. I beautified it a bit and skipped to the interesting parts:
// ... snip ... //
async function T(t, e = {}, s) {
return window.__TAURI_INTERNALS__.invoke(t, e, s)
}
// ... snip ... //
async function L(t) {
p(E, t, !0), p(w, new v, !0), p(g, await T("start_level", {
level: t,
timerEvents: d(w)
}), !0)
}
// ... snip ... //
export {
v as C, Y as a, P as b, F as c, i as d, j as e, B as f, R as g, W as h, T as i, O as j, y as l, c as m, A as r, L as s
};
Basically, there is a function related to Tauri called invoke that takes a string and a dictionnary as arguments. This seems like a nice way to call function in Rust from Javascript.
To confirm our hypothesis, we can go back in GDB and look for a function called start_level:
gef➤ info functions start_level
All functions matching regular expression "start_level":
[ ... snip ...]
File src/lib.rs:
195: static fn slot_machine_lib::start_level(usize, tauri::ipc::channel::Channel<alloc::string::String>, tauri::state::State<alloc::sync::Arc<tokio::sync::mutex::Mutex<slot_machine_lib::AppState>, alloc::alloc::Global>>) -> slot_machine_lib::start_level::{async_fn_env#0};
199: static fn slot_machine_lib::start_level::{async_fn#0}(*mut core::task::wake::Context) -> core::task::poll::Poll<core::result::Result<slot_machine_lib::models::StartResponse, alloc::string::String>>;
This function exists in slot_machine_lib, we can search for other functions in the same namespace using info functions slot_machine_lib::. In the massive list that was returned, we have the following functions that stand out:
slot_machine_lib::j9htd1slot_machine_lib::AJBCKQOISJS::{closure#0}slot_machine_lib::get_flagslot_machine_lib::get_secret_flag
I opened up Ghidra and attempted to reverse the first two functions, but they were massive and I ended up giving up. Instead, I tried to see if I could call get_secret_flag from the front-end since I can see it being called by the other two functions at some point during initialization.
The function invoke was exported as i, let's try to find it being used in other files:
grep -ar 'i as.*from"../chunks/BEFjKjka\.js' ./dump/
A couple of files contain a call to invoke, but one stands out: /_app/immutable/nodes/4.D8UeKdei.js. It calls get_flag, so it must be used when a game is won to retrieve the flag and display it, exactly what we want to do. Just to be sure, we need to keep the file length the same as before our edit to not overwrite some important memory. Thankfully, there are some spaces in the file used for the CSS that we can delete to keep the same file size.
We can modify our gdb script to modify the returned value for that file to swap out get_flag for get_secret_flag:
# Replace `get_flag` with `get_secret_flag` in `/_app/immutable/nodes/4.D8UeKdei.js`
break *tauri_utils::assets::EmbeddedAssets::get+113 if *((*(($rsp+0x18) as *mut u64)) as *mut u64) == 0xeaae0
commands
set $content_start = *((*(($rsp+0x18) as *mut u64)+0x8) as *mut u64)
restore ./patch_D8UeKdei.bin binary $content_start
end
# break on get_secret_flag to confirm the right function is called
break slot_machine_lib::get_secret_flag::{async_fn#0}
start
However, when we run this, we get an interesting error from the program, something about a panic:
thread 'tokio-runtime-worker' panicked at src/lib.rs:281:5:
called `Result::unwrap()` on an `Err` value: Utf8Error { valid_up_to: 1, error_len: Some(1) }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Let's rerun with the the environment variable set. It gives the following error:
thread 'tokio-runtime-worker' panicked at src/lib.rs:281:5:
called `Result::unwrap()` on an `Err` value: Utf8Error { valid_up_to: 1, error_len: Some(1) }
stack backtrace:
0: rust_begin_unwind
at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/std/src/panicking.rs:695:5
1: core::panicking::panic_fmt
at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/core/src/panicking.rs:75:14
2: core::result::unwrap_failed
at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/core/src/result.rs:1704:5
3: core::result::Result<T,E>::unwrap
at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/core/src/result.rs:1109:23
4: slot_machine_lib::get_secret_flag::{{closure}}
at /tmp/slot-machine/src/lib.rs:281:5
5: <futures_util::future::future::map::Map<Fut,F> as core::future::future::Future>::poll
at /root/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.31/src/future/future/map.rs:55:37
6: <futures_util::future::future::Map<Fut,F> as core::future::future::Future>::poll
at /root/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.31/src/lib.rs:86:13
7: slot_machine_lib::run::{{closure}}::{{closure}}::{{closure}}
at /tmp/slot-machine/src/lib.rs:89:25
8: tauri::ipc::InvokeResolver<R>::respond_async_serialized::{{closure}}
at /root/.cargo/git/checkouts/tauri-69fbbe4d0942e697/72211be/crates/tauri/src/ipc/mod.rs:343:33
9: tokio::runtime::task::core::Core<T,S>::poll::{{closure}}
at /root/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.45.0/src/runtime/task/core.rs:331:17
10: tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut
at /root/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.45.0/src/loom/std/unsafe_cell.rs:16:9
11: tokio::runtime::task::core::Core<T,S>::poll
at /root/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.45.0/src/runtime/task/core.rs:320:13
12: tokio::runtime::task::harness::poll_future::{{closure}}
at /root/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.45.0/src/runtime/task/harness.rs:532:19
13: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/core/src/panic/unwind_safe.rs:272:9
14: std::panicking::try::do_call
at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/std/src/panicking.rs:587:40
[ ... snip ... ]
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
It isn't very clear why it panicked, but it seems to be about some invalid UTF8 that was unwrap-ed in get_secret_flag. Let's investigate further and see where the crash happens:
break slot_machine_lib::get_secret_flag::{{closure}}
start
continue
After winning the slots, our breakpoint is triggered. By running disas we can see that going instruction by instruction would take a while because the function itself is about 20000 instructions. I picked a location after the big blob of movaps/movups that seemed to be copying stuff around a lot and inserted a breakpoint. The address I picked was slot_machine_lib::get_secret_flag::{{closure}}+20911 because it was loading the address of a from_utf8 function. I had to use the address directly because the function name refused to resolve.
gef➤ b *0x000000010066452f
Breakpoint 4 at 0x10066452f: file src/lib.rs, line 281.
gef➤ c
Continuing.
Thread 42 "tokio-runtime-w" hit Breakpoint 4, 0x000000010066452f in slot_machine_lib::get_secret_flag::{async_fn#0} () at src/lib.rs:281
281 in src/lib.rs
gef➤ context code
───────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x100664523 <slot_machine_lib::get_secret_flag::{{closure}}+51a3> jmp 0x1006644e5 <_ZN16slot_machine_lib15get_secret_flag28_$u7b$$u7b$closure$u7d$$u7d$17h5505b858b551aa8bE+20837>
0x100664525 <slot_machine_lib::get_secret_flag::{{closure}}+51a5> mov rdx, QWORD PTR [rsp+0x20]
0x10066452a <slot_machine_lib::get_secret_flag::{{closure}}+51aa> mov rsi, QWORD PTR [rsp+0x28]
●→ 0x10066452f <slot_machine_lib::get_secret_flag::{{closure}}+51af> lea rax, [rip+0xa0eada] # 0x101073010 <_ZN4core3str8converts9from_utf817hd4f1e51ae1294830E>
0x100664536 <slot_machine_lib::get_secret_flag::{{closure}}+51b6> lea rdi, [rsp+0x2ae8]
0x10066453e <slot_machine_lib::get_secret_flag::{{closure}}+51be> call rax
0x100664540 <slot_machine_lib::get_secret_flag::{{closure}}+51c0> jmp 0x100664542 <_ZN16slot_machine_lib15get_secret_flag28_$u7b$$u7b$closure$u7d$$u7d$17h5505b858b551aa8bE+20930>
0x100664542 <slot_machine_lib::get_secret_flag::{{closure}}+51c2> cmp QWORD PTR [rsp+0x2ae8], 0x0
0x10066454b <slot_machine_lib::get_secret_flag::{{closure}}+51cb> je 0x1006645bd <_ZN16slot_machine_lib15get_secret_flag28_$u7b$$u7b$closure$u7d$$u7d$17h5505b858b551aa8bE+21053>
──────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ si
0x0000000100664536 281 in src/lib.rs
gef➤
[Thread 0x7fff783f96c0 (LWP 51482) exited]
0x000000010066453e 281 in src/lib.rs
gef➤ so
Temporary breakpoint 5 at 0x100664540: file src/lib.rs, line 281.
gef➤ si
core::result::Result<&str, core::str::error::Utf8Error>::unwrap<&str, core::str::error::Utf8Error> (self=...) at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/core/src/result.rs:1107
warning: 1107 /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/core/src/result.rs: No such file or directory
gef➤ context trace
─────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x100664542 → core::result::Result<&str, core::str::error::Utf8Error>::unwrap<&str, core::str::error::Utf8Error>(self=core::result::Result<&str, core::str::error::Utf8Error>::Err(core::str::error::Utf8Error {
valid_up_to: 0x1,
error_len: core::option::Option<u8>::Some(0x1)
}))
We can see that we are now unwrapping an error, so it confirmed our hypothesis that a UTF8 decode failed earlier. This could mean that there is something that we miss that prevents us from successfully decrypting and decoding the flag.
This is as far as we got during the CTF on this challenge. However, after some discussions we had after the CTF, it seems that there was a sneaky anti-debugging feature that prevents successfully dumping the flag when a debugger is running.
Defeating the anti-debugging
I haven't been confronted with anti-debugging until now, so this is very new to me. I read a bit about it and here's what I found:
- a program can hunt for the byte
0xCCwhich indicate a software breakpoint (reference) - a program can check
/proc/pid/statusfor a TracerPID or attach ptrace to itself and do nothing (reference)
The first one seems impractical to implement since the check needs to call a function to verify, so let's attempt to see if the program is doing some ptrace magic.
My first idea is to launch the process, then read /proc/pid/status and see if there's a TracerPID. Using cat /proc/$(pidof slot-machine)/status | grep -r trace returns nothing.
Next, I tried looking for the symbol ptrace in the binary:
gef➤ info functions ptrace
All functions matching regular expression "ptrace":
Non-debugging symbols:
0x00007fffef530a70 ptrace
That seems like good news, let's put a breakpoint on ptrace and continue the execution until we hit it:
gef➤ b ptrace
Breakpoint 2 at 0x7fffef530a70
gef➤ c
Continuing.
[ ... snip ... ]
Thread 31 "tokio-runtime-w" hit Breakpoint 2, 0x00007fffef530a70 in ptrace ()
from ./lib/libc.so.6
Alright, we do have a call to ptrace, let's see who called it:
gef➤ context trace
─────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7fffef530a70 → ptrace()
[#1] 0x1005f59c9 → slot_machine_lib::j9htd1::{async_fn#0}()
[#2] 0x10065cac2 → slot_machine_lib::gpio_subscribe::{async_fn#0}()
[ ... snip ... ]
Looks like we found the culprit. A function within gpio_subscribe seems to call ptrace. Let's add a breakpoint to our caller and inspect what value is returned:
gef➤ b *0x1005f59c9
Breakpoint 3 at 0x1005f59c9: file src/lib.rs, line 130.
gef➤ c
Continuing.
[Thread 0x7fffe2ffe6c0 (LWP 53334) exited]
Thread 42 "tokio-runtime-w" hit Breakpoint 3, 0x00000001005f59c9 in slot_machine_lib::j9htd1::{async_fn#0} () at src/lib.rs:130
warning: 130 src/lib.rs: No such file or directory
gef➤ p $rax
$1 = 0xffffffffffffffff
According to its manpage, all errors return -1. We could probably patch ptrace to always return a static value, but we need to figure out what value it requires. I'm a bit lazy, so 0 looks like a pretty good success value to me, let's do that.
We can add this snippet to our .gdbinit to patch ptrace:
break ptrace
commands
set *($rip as *u8) = 0x48
set *(($rip+1) as *u8) = 0x31
set *(($rip+2) as *u8) = 0xC0
set *(($rip+3) as *u8) = 0xC3
end
That should force ptrace to always return 0, which is not -1, so we should probably take a different path than before and hopefully that's enough to bypass the anti-debugging.
Running the app again, winning at the slots again, we successfully get the flag!

End notes
That's about it for now. This track was pretty interesting and made me learn a bit more about GDB, AppImages, Tauri and Rust binaries reversing with Ghidra.