I would create a new memory definition for just the vector table, and move the beginning of FLASH past the reserved flash sectors.
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 320K
FLASH_VECTOR (rx) : ORIGIN = 0x8000000, LENGTH = 32K
FLASH (rx) : ORIGIN = 0x8018000, LENGTH = 928K /* 1024 - 3*32 */
}
/* Sections */
SECTIONS
{
/* The startup code into "FLASH" Rom type memory */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector)) /* Startup code */
. = ALIGN(4);
} >FLASH_VECTOR
.after_vector :
{
. = ALIGN(4);
KEEP(*(.after_vector))
. = ALIGN(4);
} >FLASH_VECTOR
and keep everything else unchanged.
Now the memory between 0x08008000 and 0x08018000, i.e. flash sectors 1 and 2 are no longer managed by the linker, so you can have your own functions that read and write that area as described in the flash chapter of the reference manual.
There is about 31 kbytes of flash space wasted after the vector table, you can move some read-only data in there if memory is still tight. Look through the .map file, look for read-only data structures, arrays whose size add up to roughly 31 k. CubeIDE has a memory details table in the Build Analyzer view which contains more or less the same information as the .map file, open the .rodata section under flash, order it by size, and pick some objects from there. Put the following line before their definition in the .c source:
__attribute__((section(".after_vector")))
so that this 31 kbyte of flash does not go wasted.