Bait and Switch
Writeup for Bait and Switch (Rev) - K17 CTF (2025) 💜
Description
This store promised me a free gift if I downloaded their app, but it's not working >:(
Solution
We'll follow the usual reversing methodology; basic file checks, static analysis, dynamic analysis.
Basic File Checks
It's 64-bit linux binary, not stripped (easier to reverse).
file bait-and-switch
bait-and-switch: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=38f8c17f0aa216ee86c7f2d76c5f6737250e7cae, for GNU/Linux 3.2.0, with debug_info, not stripped
I run the program, expecting the usual terminal UI but what's this? 😲
OK, this looks like a cool reversing challenge! 🍜
Testing the basic functionality, I add an item to the cart.
Clicking the ✔ button on the navbar takes us to a "redeem a gift" screen, but since the 1337
value is outside the permitted range, we can't verify we are human 🤖
Static Analysis
Opening the binary in ghidra, reviews a lot of functions and references to golang. There's so many that it took ghidra forever to do the auto analysis.
Luckily we did some basic file checks and we already know what functionality to investigate first. I tried searching for strings "human", "verif", "1337" etc, but no luck. I also see mentions of crypto - maybe the flag is encrypted, but there's no network element (I already ran wireshark).
I throw the main()
function at ChatGPT and ask if there's a better way to reverse go binaries (I suspect there is).
go tool nm ./bait-and-switch | egrep "make(HomePage|ProductsPage|GiftPage|CartPage|Toolbar)" -n
25542: b43b00 t main.makeCartPage
25543: b43e40 t main.makeCartPage.func1
25544: b43f00 t main.makeCartPage.func2
25545: b43dc0 t main.makeCartPage.func3
25546: b43260 t main.makeGiftPage
25547: b43a60 t main.makeGiftPage.NewAllStrings.func4
25548: b44020 t main.makeGiftPage.func1
25549: b440a0 t main.makeGiftPage.func2
25550: b43740 t main.makeGiftPage.func3
25551: b422a0 t main.makeHomePage
25552: b42ac0 t main.makeHomePage.func1
25553: b429e0 t main.makeHomePage.func2
25554: b42900 t main.makeHomePage.func3
25555: b42ba0 t main.makeProductsPage
25556: b43120 t main.makeProductsPage.func1
25557: b418e0 t main.makeToolbar
25558: b421a0 t main.makeToolbar.func1
25559: b420a0 t main.makeToolbar.func2
25560: b41fa0 t main.makeToolbar.func3
25561: b44120 t main.makeToolbar.func4
25562: b41ea0 t main.makeToolbar.func5
OK, now we have some functions to search for (I guess we could have searched "main" 😆). I also notice that PIE is disabled, maybe we can break at these functions in GDB?
checksec --file bait-and-switch
[*] '/home/crystal/Desktop/bug/bait-and-switch'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
Returning to ghidra, I notice a function that sounds interesting; main.reveal
👀 Checking the references to that function, it is called by main.makeGiftPage.func3
but we probably can't jump directly to it, as there's some setup (main.genBytes()
).
Dynamic Analysis
Let's open pwndbg
and set a breakpoint at main.reveal
break *0xb439fa
I thought/hoped the code might be executed during setup, but not actually revealed/presented on screen. However, setting a breakpoint at the reveal
call did not trigger anything. Trying to work backwards, I spot an interesting snippet in makeGiftPage.func3
if ((*(long *)(lVar4 + 0x78) != 4) || (**(int **)(lVar4 + 0x70) != 0x37333331)) {
*(undefined8 *)((long)register0x00000020 + -0xc0) = 0xb4387d;
fmt.Errorf(0,0,uVar3,0);
*(undefined8 *)((long)register0x00000020 + -0xc0) = 0xb4388c;
fyne.io/fyne/v2/dialog.ShowError(*(undefined8 *)((long)register0x00000020 + -0x50));
return;
It checks if a value equals 4
, and another value equal 0x37333331
. What's 0x37333331
converted to ASCII? It's 7331
, aka our target verification code (endianess revered). So this is the condition responsible for checking that our code is 4 digits long and equals 1337
! We want to break at each of these conditions (CMP instructions):
00b4376b 48 89 MOV qword ptr [RSP + local_50],RDX
54 24 68
00b43770 49 83 CMP qword ptr [R8 + 0x78],0x4
78 78 04
00b43775 0f 85 JNZ LAB_00b43865
ea 00
00 00
00b4377b 4d 8b MOV R10,qword ptr [R8 + 0x70]
50 70
00b4377f 41 81 CMP dword ptr [R10],0x37333331
3a 31
33 33 37
00b43786 0f 85 JNZ LAB_00b43865
d9 00
00 00
Set those breakpoints in GDB and run the application.
break *0xb43770
break *0xb4377f
We hit the first breakpoint at 0xb43770
, let's check the condition.
0x0000000000b43770 in main.makeGiftPage.func3 ()
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]────────────────────────────────────────────────────
RAX 0xb43740 (main.makeGiftPage.func3) ◂— lea r12, [rsp - 0x38]
RBX 0
RCX 0xc001ca6480 ◂— 0x4210000042b3c000
RDX 0xc000402000 —▸ 0xc0019d8750 —▸ 0x1c09090 ◂— 0
RDI 0xc002412398 —▸ 0xc00040ba40 —▸ 0xc000aa82d0 ◂— 0
RSI 0
R8 0xc000a7d188 ◂— 0x420c500043634400
R9 0xe68ba0 (go:itab.*fyne.io/fyne/v2/internal/driver/glfw.window,fyne.io/fyne/v2[Window]) —▸ 0xd0d180 (type:*+1302880) ◂— 0x10
R10 0x7fffb01e2f20 ◂— 0
R11 8
R12 0xc0000e38f8 —▸ 0x70efd5 (fyne.io/fyne/v2.(*Animation).Start+53) ◂— add rsp, 0x10
R13 0xc002412398 —▸ 0xc00040ba40 —▸ 0xc000aa82d0 ◂— 0
R14 0xc000002380 —▸ 0xc0000e0000 ◂— 0
R15 0xffffffffffffffff
RBP 0xc0000e3928 —▸ 0xc0000e3940 —▸ 0xc0000e3970 —▸ 0xc0000e3a20 —▸ 0xc0000e3a50 ◂— ...
RSP 0xc0000e3878 ◂— 0
*RIP 0xb43770 (main.makeGiftPage.func3+48) ◂— cmp qword ptr [r8 + 0x78], 4
It's comparing [r8 + 0x78]
to 4
- what does that address currently hold?
x $r8 + 0x78
0xc000d75c80: 0x0000000000000003
It's 3
(the max allowed digits), let's change it to 4
set {unsigned long}($r8 + 0x78) = 0x4
Verify it's looking good ✅
x $r8 + 0x78
0xc000d75c80: 0x0000000000000004
We also need the code to actually be 4 digits (1337
) so let's we'll continue to our next breakpoint at 0xb4377f
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]────────────────────────────────────────────────────
RAX 0xb43740 (main.makeGiftPage.func3) ◂— lea r12, [rsp - 0x38]
RBX 0
RCX 0xc001ca6480 ◂— 0x4210000042b3c000
RDX 0xc000402000 —▸ 0xc0019d8750 —▸ 0x1c09090 ◂— 0
RDI 0xc002412398 —▸ 0xc00040ba40 —▸ 0xc000aa82d0 ◂— 0
RSI 0
R8 0xc000a7d188 ◂— 0x420c500043634400
R9 0xe68ba0 (go:itab.*fyne.io/fyne/v2/internal/driver/glfw.window,fyne.io/fyne/v2[Window]) —▸ 0xd0d180 (type:*+1302880) ◂— 0x10
*R10 0xc000d6b4f0 ◂— 0x333331 /* '133' */
R11 8
R12 0xc0000e38f8 —▸ 0x70efd5 (fyne.io/fyne/v2.(*Animation).Start+53) ◂— add rsp, 0x10
R13 0xc002412398 —▸ 0xc00040ba40 —▸ 0xc000aa82d0 ◂— 0
R14 0xc000002380 —▸ 0xc0000e0000 ◂— 0
R15 0xffffffffffffffff
RBP 0xc0000e3928 —▸ 0xc0000e3940 —▸ 0xc0000e3970 —▸ 0xc0000e3a20 —▸ 0xc0000e3a50 ◂— ...
RSP 0xc0000e3878 ◂— 0
*RIP 0xb4377f (main.makeGiftPage.func3+63) ◂— cmp dword ptr [r10], 0x37333331
It's comparing [r10]
to 0x37333331
- what does that address currently hold?
x $r10
0xc0014067a0: 0x0000000000333331
It's 0x333331
, because we are one digit short. Let's do another live modification.
set {unsigned int}($r10) = 0x37333331
Verify it's also looking good ✅
x $r10
0xc0014067a0: 0x0000000037333331
Hit continue and boom, we did it! 🥳
It's not possible to copy and paste the flag, so here's a shortcut.
search K17
Searching for byte: b'K17'
bait-and-switch 0xea84bc 0x38314b460037314b /* 'K17' */
bait-and-switch 0x15cd409 0x130a67501537314b
[anon_c000000] 0xc0002da000 "K17{i_d0n't_th1nk_fi$h_lik3_w@sabi}"
TLDR; we used some static analysis and live debugging to bypass the 3 digit limit and submit the correct 4 digit code, receiving the flag. I'm not sure if this is the most efficient, or intended path to solution - let me know if you solved it differently!
Flag: K17{i_d0n't_th1nk_fi$h_lik3_w@sabi}
Last updated