CONfidence Teaser

An online teaser CTF with a chance to win conference passes for the main event at CONFidence in Cracow, Poland in June.
@p4.team
March 16 - March 17 2019

CONfidence Teaser 2019

Well, this one was some excellent mental exercise. I had started to feel like the Easy-rated challenges on CTFs were actually becoming…easy. This one disabused me of that notion. I tackled only two of the “warmup” challenges on this one, and made it to a solution on neither of them. It was an opportunity to learn new things, however, and I got quite deep into both of them. Indeed, it was a frustrating end of the day to feel so close to solutions, yet have no flag to show for it.

It was a great chance, though, to gain some more experience with reverse engineering and to try out Ghidra. I can highly recommend the Twitter and Youtube channels of @ghidraninja to get started with this powerful free tool. Ghidraninja also has some scripts to make Ghidra even more useful and human readable at https://github.com/ghidraninja/ghidra_scripts. I think some time on https://crackmes.one and a refresher on C are both in the cards if I want to start getting solutions on these RE challenges.

My admin panel (web)

I think I've found something interesting, but I'm not really a PHP expert. Do you think it's exploitable?

https://gameserver.zajebistyc.tf/admin/

At the site, we have a listable directory that holds login.php and login.php.bak, and the parent directory is a login/character creation page for the game server we’re trying to hack

Clicking on login.php, we get a “Not authenticated.” message, and downloading login.php just gets us the html of that message. Clicking on the .bak file, though, we get a download of that file, which is presumably the same as the login.php file itself. Time to have a rummage around.

So, here’s the code…let’s see what it’s doing (## my comments)
<?php
## getting some functionality from func.php and config.php in the directory above
include '../func.php';
include '../config.php';

## checking to see that there is a cookie for the site called “otadmin”
if (!$_COOKIE['otadmin']) {
  exit("Not authenticated.\n");
}

## checking to see if the regular expression “/^{hash: [0-9A-Z\"]+}$/”
##so – {“hash”: some string containing at least one digit, capital, and/or quote }
## is the true for the value for the otadmin cookie
if (!preg_match('/^{"hash": [0-9A-Z\"]+}$/', $_COOKIE['otadmin'])) {
  echo "COOKIE TAMPERING xD IM A SECURITY EXPERT\n";
  exit();
}

## set the session_data variable to the cookie contents
## json_decode() sets a JSON string to a PHP array variable
$session_data = json_decode($_COOKIE['otadmin'], true);

## checks to see if the $session_data variable is empty
if ($session_data === NULL) { echo "COOKIE TAMPERING xD IM A SECURITY EXPERT\n";
exit(); }

## checks to see if the session_data variable’s hash component is the same as the
## MD5 hash of the variable $cfg_pass (presumably found in config.php)
if ($session_data['hash'] != strtoupper(MD5($cfg_pass))) {
  echo("I CAN EVEN GIVE YOU A HINT XD \n");

## Giving us a hint…
## running through all of the numbers from 0 to the length of the MD5 hash of
## xDdddddd (32), echo the byte of the hash of cfg_pass in each position 0-31
## masked (logically ANDed) against the 0xC0 bitmask.
  for ($i = 0; i < strlen(MD5('xDdddddd')); i++) {
    echo(ord(MD5($cfg_pass)[$i]) & 0xC0);
  }

  exit("\n");
}

## And if everything is in order, runs the display_admin function
display_admin();

So, a quick test, setting a cookie called otadmin to {“hash”: 1}, we get the “COOKIE TAMPERING xD IM A SECURITY EXPERT” message instead of “Not authenticated.”, which means we’re either not matching the regex, or our session_data variable is empty. After some mucking about, I realised that the quotation in the regex, is so that we can properly format a JSON entry. Setting the otadmin cookie to {“hash”: “SOMESTRING12”} we get the hint:

I CAN EVEN GIVE YOU A HINT XD
0006464640640064000646464640006400640640646400

Parsing this out in a PHP sandbox (http://sandbox.onlinephpfunctions.com), the length of an MD5 hash is 32. So, it’s looking at positions 0 through 31.

The hash for a sample password “aaa” is 47bce5c74f589f4867dbd57e9ca9f808
The value at position [0] is 4
The ord of 4 is 52, which is the ASCII value for the text character 4
The AND of 52 and 0xC0 is 0…the AND of a great many things and 0xC0 is 0.
Trying out echo(ord(MD5("abcdef123")[i]) & 0xC0); we get
6464064064006464000000646406464646400000064640

"password" gets us:
0640646464064064640006400640000646464000646400
So, we can brute-force offline until we get a match to the hint!

“PASSWORD” gets us:
0006406400640640006406464000646406400640000
So, the password would be case-sensitive, but we’re just looking for the hash

Testing the hash of “aaa”:
47bce5c74f589f4867dbd57e9ca9f808 gets us:
006464640640064000640000646464006406464064000

47BCE5C74F589F4867DBD57E9CA9F808 also gets us:
006464640640064000640000646464006406464064000

So, we’re left running a loop looking for some combination of 0-f in 32 positions that ANDs to the hint…shit

Wait…testing "abcdefghijklmnopqrstuvwxyz1234567890" we get:
646464646464646464646464646464646464646464646464 646400000000

So, letters (really just a to f) and 1 and 2 (let’s call this group A) are a 64 and numbers 3 to 0 (let’s call this group B) are a 0.

From our hint,
0006464640640064000646464640006400640640646400

We have the pattern:
BBBAAABABBABBBAAAABBBABBABABAABB

That would greatly reduce our keyspace, but now we’re not brute-forcing offline…

And that’s where I left that one…

Update: 26 March 2019 - Noraj (Alexandre Zanni) has a good write-up here. What I missed was the variable type that is decided by the content of the hash. Since the hash starts with a number, the json_decode command wants to create an int variable for PHP. That int variable will only care about those first three number positions, so it’s just a matter of running through three digit numbers to brute-force the hash’s PHP variable equivalent.

And, of course, the flag was p4{wtf_php_comparisons_how_do_they_work}. Well, now I know.

Elementary (reverse engineering)

Elementary, my dear Watson.

The elementary file in the provided archive is a 64-bit ELF and strings shows us that it’s likely going to validate a password. Time to test drive Ghidra…

There are over 830 functions in here, so let’s start in main (## my comments):

undefined8 main(void)

{
undefined8 uVar1; ## set an undefined variable uVar1
long in_FS_OFFSET; ## set a long int variable in_FS_OFFSET
char local_98 [136]; ## set a character variable local_98 with length 136
long local_10; ## set a long int variable local_10


## set local_10 to the long int of in_FS_OFFSET + 28
local_10 = *(long *)(in_FS_OFFSET + 0x28);

## print the Password prompt
printf("Password: ");

## set the user input and save it to 001d3831f as local_98 ?
__isoc99_scanf(&DAT_001d831f,local_98);

## run local_98 through the checkFlag function
uVar1 = checkFlag(local_98);

## if the final result of checkFlag is 0, then it’s the wrong password
if ((int)uVar1 == 0) {
  printf("Wrong!");
}

## otherwise it’s the right password and we get some congratulations else {
  printf("Good job!");
}

## if local_10 is not the same as it was before the password check (or in the same place?) then
## the program fails. This is presumably to mitigate against buffer overflows – our password is
## limited to 136 characters
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
      /* WARNING: Subroutine does not return */
      __stack_chk_fail();
}
return 0;
}

So, our next stop is the very long checkFlag() function…
It’s only a couple of variables, but a long series of nested if statements

undefined8 checkFlag(char *param_1)

{
ulong uVar1;
undefined8 uVar2;

## the value of uVar1 is the result of function0 processing the integer param_1[0x40] ANDed
## with 1
uVar1 = function0((int)param_1[0x40] & 1);

## If the result is 0, then it moves on to the next step
if ((int)uVar1 == 0) {

## the value of uVar1 is the result of function1 processing the integer param_1[0x40] bit-shifted
## two positions to the right ANDed with 1
  uVar1 = function1((int)param_1[0x40] >> 2 & 1);

## and so on, 830 more times…
if ((int)uVar1 == 0) {
  uVar1 = function2((int)param_1[0x40] >> 5 & 1);
  if ((int)uVar1 == 0) {
    uVar1 = function3((int)param_1[0x40] >> 4 & 1);
.
.
.
else {
  uVar2 = 0;
}

## and it returns the value of uVar2 to the main function
return uVar2;
}

So, let’s see where the value of uVar2 is anything but 0 (which is the wrong password indicator)
Yikes…it’s the last of the 832 nested ifs:

if ((int)uVar1 == 0) {uVar1 = function831((int)param_1[0x14] >> & 1);
if ((int)uVar1 == 0) { uVar2 = 1;}
else {uVar2 = 0;}

So, this password has to pass 832 tests to return a 1 to indicate a correct password.
And what do these functions do? They all seem to:

## return the original param_1 value raised to the power of 1…so nothing at all.
return (ulong)(param_1 ^ 1);
## [Note from Future-Me: Yes, I know this is wrong now]

The next question is…what is the test looking for?
It appears to be checking the integer value of various positions in param_1 to see if it’s a 1 or not (& 1 checks to see if it’s a 1…if it is we return a 1). We want the AND operation to never return a 1 (always uVar1 == 0), so that it passes that check and goes on to the next check.

And the value for param_1 changes, starting with 0x40, then 0x26, and so on. And the checks are in unique batches of 8 (the original and right shifts of 1-7), so we’re just checking every bit position of that byte as it reaches the LSB from the shifts, and we want to see the opposite bit or a 0 in each position. This way we always return 0 (only 1 AND 1 is 1).

My first thought is that all zeroes would be easiest, but uninformative, since I suspect that the flag is somewhere in the text string made up of those opposite bytes. Otherwise, we’ll just get the Good Job! message and nothing to submit. A quick test of the program, with 136 zeroes does not give us a pass, and 137 or more gives us:

*** stack smashing detected ***: terminated

So, we are limited to 136 characters, which is good, because the checkFlag runs through 832 bitwise tests (divided by 8, that’s 104 characters). Testing with 104 zeroes does not work either, though.

Collecting all of the unique param_1 bytes in checkFlag in sequence, with the occasional integer numbers converted to hex like the rest of them, we have these 104 bytes.

4026435815445f24274d655a0630173e51534f2c191134031d661c5d102b4e093c464741280c380d39254507543a595b0e522d4c4 2624b2f6122506756054a011233481a081f2a553121363f2e04291849231e5c0b57323520636013100f3764023b5e3d16001b14

Converting them to binary, we end up with:

01000000001001100100001101011000000101010100010001011111001001000010011101001101011001010101101000000110 00110000000101110011111001010001010100110100111100101100000110010001000100110100000000110001110101100110 00011100010111010001000000101011010011100000100100111100010001100100011101000001001010000000110000111000 00001101001110010010010101000101000001110101010000111010010110010101101100001110010100100010110101001100 01000010011000100100101100101111011000010010001001010000011001110101011000000101010010100000000100010010 00110011010010000001101000001000000111110010101001010101001100010010000100110110001111110010111000000100 00101001000110000100100100100011000111100101110000001011010101110011001000110101001000001001100101100000 00010011000100000000111100110111011001000000001000111011010111100011110100010110000000000001101100010100

Flipping each of those bits, we get:

10111111110110011011110010100111111010101011101110100000110110111101100010110010100110101010010111111001 11001111111010001100000110101110101011001011000011010011111001101110111011001011111111001110001010011001 11100011101000101110111111010100101100011111011011000011101110011011100010111110110101111111001111000111 11110010110001101101101010111010111110001010101111000101101001101010010011110001101011011101001010110011 10111101100111011011010011010000100111101101110110101111100110001010100111111010101101011111111011101101 11001100101101111110010111110111111000001101010110101010110011101101111011001001110000001101000111111011 11010110111001111011011011011100111000011010001111110100101010001100110111001010110111110110011010011111 11101100111011111111000011001000100110111111110111000100101000011100001011101001111111111110010011101011

But converting that to text, we get:

¿Ù¼§ê» Ûزš¥ùÏèÁ®¬°ÓæîËüâ™ã¢ïÔ±öù¸¾×óÇòÆÚºø«Å¦¤ñ­Ò³½´ÐžÝ¯˜©úµþíÌ·å÷àÕªÎÞÉÀÑûÖç¶Üá£ô¨ÍÊßfŸìïðÈ›ýÄ¡Âéÿäë

Not helpful…

Oops. Well, this explains it…

Problem Number 1:

In the numbered functions (0-831) the ^ (i.e, param_1 ^ 1) does not mean “raised to the power of”; it means XOR [Get it together, Past-Me]. XOR is going to return 0 for a bit match for 1 XOR param_1 (i.e, param_1 is a 1), and it returns a 1 for a bit mismatch for 1 XOR param_1 (i.e, param_1 is a 0). And we can’t have any mismatches because that would return a 1 from the function and we can’t have 1 or we won’t move to the next check. So, those bits have to be zeroes.

And it just happened that all of the functions I looked at initially were XORs. There are plenty that are not…and this is why 104 zeroes didn’t get us a Good Job! message.

return (ulong)param_1;

So, now I’ve got 832 functions to go through…looking for which ones are XOR’d against my bit, and which are not.

Problem Number 2:

I’ve also got my order of operations wrong…we’re doing the AND first, then passing that to the function where it may or may not get XOR’d.

Solution:

So, taking all of the checks in order, we compare the bit in the designated position with 1 and we want to return a 1 if it is going to be XOR’d by the function (so that we end up with a match and return a 0 to proceed to the next step). And we want a 0 in that position (which would have also ANDed to 0) if it is not being XOR’d by the function, because then it’s just returning that 0 unmolested and that’s what we need to proceed. This simplifies things, because now we just need to know if it’s being XOR’d or not. But with 832 functions to check, I need some way to automate that…

Ghidra lets me export all of the decompiled C code to a single file. I can isolate the functions and replace all of the param_1 ^ 1 functions with 1s, and all of the plain param_1 functions with 0s.

11101000101011101011110111011011101000100011100011110010111011001011100001100110000011110011011010101001 01001110001111001011110100101111101101110110001111111001111001101110101011001100100001010100010110001111 11010100111101001111010111111100101010000101100100001101010110011011101111111010001001100100010111001111 10111011101101101101011111100010110010000100101111100010101111010001010100101001100011110011110000110011 10001010111001011110010110110001011101101101110010011010111110011110110100001111111100100010110011001111 10010111011001101001010000001111011011001111101011111001101111101010101010110100000110010111000010110111 11011110101110110101101011111100100111111000111011110011010010011101000111101011010011001001100010100100 10011101110100111100010101001101010110101011011111011100101101110111100010110011000101100000111110110111

Crap…

讽ۢ8òì¸f6©N<½/·cùæê̅EÔôõü¨Y Y»ú&EÏ»¶×âÈKâ½)<3Šåå±vܚùíò,ϗf”lúù¾ª´p·Þ»ZüŸŽóIÑëL˜¤ÓÅMZ·Ü·x³·

Maybe my 1s and 0s are opposed? I don’t think so, but…

Nope.

QB$]Ç G™ðÉV±ÃBÐHœ3zºp+ W¦ò¦DÙº0DI(7´BêÖpÃÌuN‰#eð Ó0h™kð“AUKæH!D¥ `q¶.³g[b,:²¥H#H‡LéðH

This was as far as I got in the time I had that day.

Post-CTF thoughts…I’m pretty sure I’ve got the meaning of param_1[0x40], etc. wrong. In point of fact, nowhere have I tested our password attempt against these operations. These must be array positions in the submitted password or memory addresses (they seem awfully small for the latter, though). That said, it shouldn’t have any bearing on the outcome…those bits still need to be ANDed with 1 and XOR’d with 1 or not, and there’s only one way for that to return a zero every time. Hmph.

Ah…but if it’s looking at different byte positions in an array of the password characters, then those octets would be arranged differently in the submitted password. Not sure that gets us anything but the unreadable mess above in a different order, though, but OK. Clearly, I need to brush up on my C basics (it’s been 25 years), if I’m going to have any success with these RE challenges.

All those hex bytes are unique numbers from 00-67 (i.e. decimals 0-103):

40 26 43 58 15 44 5f 24 27 4d 65 5a 06 30 17 3e 51 53 4f 2c 19 11 34 03 1d 66 1c 5d 10 2b 4e 09 3c 46 47 41 28 0c 38 0d 39 25 45 07 54 3a 59 5b 0e 52 2d 4c 42 62 4b 2f 61 22 50 67 56 05 4a 01 12 33 48 1a 08 1f 2a 55 31 21 36 3f 2e 04 29 18 49 23 1e 5c 0b 57 32 35 20 63 60 13 0a 0f 37 64 02 3b 5e 3d 16 00 1b 14

00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f 40 41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4e 4f 50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f 60 61 62 63 64 65 66 67

These must be positions in the password string:
64 38 67 88 21 68 95 36 39 77 101 90 6 48 23 62 81 83 79 44 25 17 52 3 29 102 28 93 16 43 78 9 60 70 71 65 40 12 56 13 57 37 69 7 84 58 89 91 14 82 45 76 66 98 75 47 97 34 80 103 86 5 74 1 18 51 72 26 8 31 42 85 49 33 54 63 46 4 41 24 73 35 30 92 11 87 50 53 32 99 96 19 10 15 55 100 2 59 94 61 22 0 27 20

So, in each loop, a different bit of each character byte in the password is checked. Every bit must be ANDed with 1 first, and the result must XOR with 1 (or already be) a zero. The XOR checks must take in 1s to XOR with 1 and return a zero, and in order to be a 1 after an AND operation with 1, the original bit must also be a 1. The non-XOR checks must take in 0s to simply return a zero, and in order to be a 0 after an AND operation with 1, the original bit must be a 0.

XOR checked bits must start out as 1s.
Non-XOR checked bits must start out as 0s.

This gives us the 832 bits we had above, but the bytes they represent are checked out of order (as are the bits, but we’re still looking at the same byte), and the order of the octets of bits needs to be rearranged accordingly. The param_1[position] order must be used to reorder the octets. So, the first position is 0x40 (or 64), so the first octet should be in position 64 of the password.

Reordered, the binary is:

11001111110011111001010001001100111010100000111101011010001001101011101110110111000101101010010011110010 00101001100001011111001011111100100011101011101101001011100011111011011110001010110110111111110000001111 11110101110001010010111111001000110111100110011011101101111110101111100110010111101101101010100101110110 01001110110111000100010101101100111011001111001110011010100110001001110100111100100111111110001001110000 01100110101101110001100100010101110111001011101101011010101101111101000100111000101101001010111001100011 00110011101111101101010010111000010110011110001001001001100011110101100111100101001011001011110110100010 11010111010001011010101011111010101010001101001100110110111010110011110011100101000011010111100010110111 11111001000011111011110110110001101100111011110111111001010011010000111111001100111010001111010011100110

Which decodes to:

.,·.·.©È.YÓóE»)ÅõêÏ.·¢³<»æ..ÔE.lLªÜüì×®¸&Þúüù<p±N¾Ñ.Ìë´M϶âÜ x½.èú.½8âY»fZòå3f¨c./.·KùíIÛ½6..ô·ò¤vå.Z..ù

Oh…and I just noticed Problem Number 3:
[Note from Past-Me: Really, Future-Me? And you’re giving me grief?]

Before I realised that there were some XOR and some non-XOR checks, I assumed that the order of the bit checks in each byte didn’t matter – that it was a red herring of meaningless complexity…OK, I was wrong. The checks are in unique batches of 8 (the original and right shifts of 1-7), and we are checking every bit position of that byte as it reaches the LSB from the shifts, but now we don’t just want to see the opposite bit or a 0 in each position. Now we need to know which positions are getting XOR’d and which aren’t. This is going to take some more processing.

OK. I did the processing…and I still get junk

FINAL RESULTS

1 point for demonstrating that I can submit a flag in “Sanity Check”
Top score with the diminishing points value system was 2722 for solutions to all of the challenges.