Preamble
This week I review some solutions to several challenges from the 2020 Houseplant CTF hosted by RiceTeaCatPanda.
Reverse Engineering
Many of the Reverse Engineering problems in this CTF required an understanding of object-oriented programming (namely Python and Java).
Fragile
Can you help me move my stuff? This one’s fragile!
The first reverse engineering problem in the Houseplant CTF - “Fragile” - provides us some java source code to assess and analyze:
import java.util.*;
public class fragile
{
public static void main(String args[]) {
Scanner scanner = new Scanner(System.in);
System.out.print("Enter flag: ");
String userInput = scanner.next();
String input = userInput.substring("rtcp{".length(),userInput.length()-1);
System.out.println(input);
if (check(input)) {
System.out.println("Access granted.");
} else {
System.out.println("Access denied!");
}
}
public static boolean check(String input){
boolean h = false;
String flag = "h1_th3r3_1ts_m3";
String theflag = "";
if(input.length() != flag.length()){
return false;
}
for(int i = 0; i < flag.length(); i++){
theflag += (char)((int)(flag.charAt(i)) + (int)(input.charAt(i)));
}
return theflag.equals("ÐdØÓ§åÍaèÒÁ¡");
}
}
This code defines two functions: main() and check(). When run, the code calls main() and prompts the user for input, with the expectation for the user to enter the correct flag; after the user enters their guess, the code calls check() to determine if the guess was the correct flag.
At a glance, one may notice that the check() function contains a String variable “flag” set to “h1_th3r3_1ts_m3”. However, if you read the code (or attempt to format/enter it in the program when it runs), you would discover that it is not the answer.
So what does check() do?
- First, it compares if the user’s guess is the same length as the String variable “flag”. If it’s not, then it’s not correct (returns False). NOTE: the main() function strips off the flag wrapper format “rtcp{…}”, so the length in question is just what is contained within the brackets.
- Then, the code takes every character in the user’s input and pairs it with every character in the String variable “flag”, adding their respective ASCII decimal values. It then converts the added results back into a new character by casting the integers with (char), storing the result in a String variable “theflag”.
- Finally, the code compares String variable “theflag” against a known jumble of characters “ÐdØÓ§åÍaèÒÁ¡”. Only the correct guess will return a result of True.
To get the solution to this problem, we need to start at the end and work backwards. Since we know the jumble that’s being compared in the final step and the “key” that was used to create it (String variable “flag”), we can derive the solution by going over each character in the jumble and subtracting the ASCII decimal value of its respective character from “flag”:
String flag1 = "h1_th3r3_1ts_m3";
String crypt = "ÐdØÓ§åÍaèÒÁ¡";
String ans = "";
for(int i = 0; i < crypt.length(); i++){
ans += (char)((int)(crypt.charAt(i) - (int)(flag1.charAt(i))));
}
System.out.println(ans); #Output: h3y_1ts_n0t_b4d
Breakable!
Okay…this one’s better, but still be careful!
As before, this challenge provides yet another source Java source code to assess & analyze:
import java.util.*;
public class breakable
{
public static void main(String args[]) {
Scanner scanner = new Scanner(System.in);
System.out.print("Enter flag: ");
String userInput = scanner.next();
String input = userInput.substring("rtcp{".length(),userInput.length()-1);
if (check(input)) {
System.out.println("Access granted.");
} else {
System.out.println("Access denied!");
}
}
public static boolean check(String input){
boolean h = false;
String flag = "k33p_1t_in_pl41n";
String theflag = "";
int i = 0;
if(input.length() != flag.length()){
return false;
}
for(i = 0; i < flag.length()-2; i++){
theflag += (char)((int)(flag.charAt(i)) + (int)(input.charAt(i+2)));
}
for(i = 2; i < flag.length(); i++){
theflag += (char)((int)(flag.charAt(i)) + (int)(input.charAt(i-2)));
}
String[] flags = theflag.split("");
for(; i < (int)((flags.length)/2); i++){
flags[i] = Character.toString((char)((int)(flags[i].charAt(0)) + 20));
}
return theflag.equals("Òdݾ¤¤¾ÙàåÐcÝÆ¥ÌÈáÏܦaã");
}
}
This code again defines two functions: main() and check(). Again, main() prompts for a user’s guess at the flag and check() validates the guess. This time, however, there is more at work inside of the check() function:
- First, the length of the guess is compared against String variable “flag” (in this case, length 16). As before, if the length’s are different, the guess is incorrect.
- Next, the first 14 characters (positions 0 through 13) are paired with the 14 characters in the user’s guess, offset by 2 (positions 2 through 15); the ASCII values are added, converted back into characters, and stored into String variable “theflag”.
- In the next for-loop, the last 14 characters (positions 2 through 15) of String variable “flag” are paired and added with the first 14 characters of the user’s guess (positions 0 through 13); the ASCII values are added, converted back into characters, and stored into String variable “theflag”
- Finally, “theflag” is compared against a known jumble String “Òdݾ¤¤¾ÙàåÐcÝÆ¥ÌÈáÏܦaã”. Only the correct guess will return True. NOTE: we ignored the last for-loop in check() because - ultimately - it didn’t affect or impact the result.
To get the solution to the problem, we can work backwards, as before. One thing we have to keep in mind this time is the fact that there is overlap in the two for-loops (positions 2 through 13 in the guess and “flag”); we can confirm this observation in looking at the length of the jumble string (28) against the lengths of what is stored in each for-loop (14 each).
Since there is no need to decode those positions twice, we can:
- Reverse the last half of the jumble string (the work that the second for-loop performed) by subtracting the appropriate ASCII decimal values from each character.
Reverse the last two characters in the first half of the jumble string (encoded by the work performed in the first for-loop) by subtracting the appropriate ASCII decimal values from each character.
String userinput = "Òdݾ¤¤¾ÙàåÐcÝÆ¥ÌÈáÏܦaã"; String flag = "k33p_1t_in_pl41n"; String ans = "rtcp{"; for(int i=14; i < 16; i++){ ans += (char)((int)(userinput.charAt(i)) - (int)(flag.charAt(i-12))); } for(int i=0; i < 14; i++){ ans += (char)((int)(userinput.charAt(i)) - (int)(flag.charAt(i))); } ans += "}"; System.out.println(ans); # Output: rtcp{0mg_1m_s0_pr0ud_}
Bendy
I see you’ve found my straw collection…(this is the last excessive for loop one i swear)
Again, there is some Java source code provided to assess and analyze:
import java.util.*;
public class bendy
{
public static void main(String args[]) {
Scanner scanner = new Scanner(System.in);
System.out.print("Enter flag: ");
String userInput = scanner.next();
String input = userInput.substring("rtcp{".length(),userInput.length()-1);
if (check(input)) {
System.out.println("Access granted.");
} else {
System.out.println("Access denied!");
}
}
public static boolean check(String input){
boolean h = false;
String flag = "r34l_g4m3rs_eXclus1v3";
String theflag = "";
int i = 0;
if(input.length() != flag.length()){
return false;
}
if(!input.substring(0,2).equals("h0")){
return false;
}
if(input.charAt(7) != 'u'){
return false;
}
for(i = 0; i < flag.length()-14; i++){
theflag += (char)((int)(flag.charAt(i)) + (int)(input.charAt(i+8)));
}
for(i = 10; i < flag.length()-6; i++){
theflag += (char)((int)(flag.charAt(i)) + (int)(input.charAt(i-8)));
}
for(; i < flag.length(); i++){
theflag += (char)((int)(flag.charAt(i-3)) + (int)(input.charAt(i)));
}
//Òdݾ¤¤¾ÙàåÐcÝÆ¥ÌÈáÏܦaã
String[] flags = theflag.split("");
for(i=0; i < (int)((flags.length)/2); i++){
flags[i] = Character.toString((char)((int)(flags[i].charAt(0)) + 20));
}
theflag = theflag.substring(flags.length/2);
for(int k = 0; k < ((flags.length)/2); k++){
theflag += flags[k];
}
return theflag.equals("ÄÑÓ¿ÂÒêáøz§è§ñy÷¦");
}
}
This challenge builds upon the previous two. As before, we have the functions main() and check(). This time, however, check() has been made more complicated. So what does it do?
- First, it compares the length of the user’s guess against String variable “flag” (length 21). It also checks that the first two characters in the guess are “h” and “0” and that the eighth character (position 7) is “u”.
- In the first for-loop, the ASCII decimal values of the first 7 characters in “flag” and middle 7 characters (positions 8 through 14) of the guess are added, converted back to ASCII text and written to String variable “theflag”.
- In the second for-loop, the ASCII decimal values of positions 10 through 14 of “flag” and positions 2 through 6 of the guess are added, converted, and written to “theflag”.
- In the third for-loop, the ASCII decimal values of position 12 through 17 of “flag” and positions 15 through 20 of the guess are added, converted, and written to “theflag”.
- Finally, “theflag” adds 20 to ASCII decimal values in its first half, swaps the two halves, and compares the result to the jumble string “ÄÑÓ¿ÂÒêáøz§è§ñy÷¦”
Aside from the final bullet in the list above, dur methodology doesn’t change from the first two problems. The jumble string must first be carefully reordered, then several passes must be taken to adjust the ASCII decimal values of the characters. We also must take care to insert the appropriate characters in the first, second, and eigthth positions (see first bullet):
String cipher = "ÄÑÓ¿ÂÒêáøz§è§ñy÷¦";
String flag = "r34l_g4m3rs_eXclus1v3";
String test = "";
int i = 0;
//first part
for (i=(cipher.length())/2; i < cipher.length(); i++){
test += cipher.charAt(i);
}
for (i=0; i<(cipher.length())/2; i++){
test += cipher.charAt(i);
}
//second part
String[] tarr = test.split("");
for(i=0; i < (int)((test.length())/2); i++){ //from 0 to half the lengths of flags[]
tarr[i] = Character.toString((char)((int)(tarr[i].charAt(0)) - 20)); //re-encode flags[i] as flags[ASCII + 20]
}
test = String.join("", tarr);
//third part
//the order of the characters by position: [8,9,10,11,12,13,14,2,3,4,5,6,15,16,17,18,19,20]
String word = "h0";
for(i=7; i<12; i++){
word += (char)((int)(test.charAt(i)) - (int)(flag.charAt(i+3)));
}
word += 'u';
for(i=0; i<7; i++){ //[8,9,10,11,12,13,14]
word += (char)((int)(test.charAt(i) - (int)(flag.charAt(i))));
}
for(i=12; i<18; i++){
word += (char)((int)(test.charAt(i) - (int)(flag.charAt(i))));
}
System.out.println(word);
EZ
I made a password system, bet you can’t get the flag
This challenge was meant to be more funny than challenging; in “EZ”, we were given a python source code file. All that we needed to do was open it to find the flag. There was also an easter egg commented within the code that alluded to a certain 80s pop singer.
PZ
Ok, I think I made it slightly better. Now you won’t get the flag this time!
This challenge provides another python source code:
def catchecKpass():
userinput = input("pwease enter youwr password... uwu~ nya!!: ")
if userinput == "rtcp{iT5_n0T_s1mPlY_1n_tH3_C0d3}":
return True
else:
return False
def checkpasss():
userinput = input("Enter the password: ")
if userinput == "rtcp{wH4t_aR3_y0u_SaY1nG?}":
return True
else:
return False
def acheckpass():
userinput = input("Enter the password: ")
if userinput == "rtcp{1_cAnt_QuiT3_hE@r_Y0u!}":
return True
else:
return False
def ccheckpass():
userinput = input("Enter the password: ")
if userinput == "rtcp{sPe4k_Up_4_b1T}":
return True
else:
return False
def checkpass():
userinput = input("Enter the password: ")
if userinput == "rtcp{iT5_s1mPlY_1n_tH3_C0d3}":
return True
else:
return False
def bcheckpass():
userinput = input("Enter the password: ")
if userinput == "rtcp{tH1s_i5_4_R3aL_fLaG_t0TalLy}":
return True
else:
return False
def catcheckpass():
userinput = input("pwease enter youwr password... uwu~ nya!!: ")
if userinput == "rtcp{iT5_s1mPlY_1n_tH3_C0d3}":
return True
else:
return False
def main():
access = checkpass()
if access == True:
print("Unlocked. The flag is the password.")
print("b-but i wunna show off my catswpeak uwu~... why wont you let me do my nya!!")
exit()
else:
print("Incorrect password!")
print("sowwy but now you gunnu have to listen to me spweak in cat giwrl speak uwu~")
catmain()
def catmain():
access = catcheckpass()
if access == True:
print("s-senpai... i unwocked it fowr you.. uwu~")
print("t-the fwlag is... the password.. nya!")
exit()
else:
print("sowwy but that wasnt quite rwight nya~")
catmain()
access = False
main()
This challenge is a prime example of the “security by obscurity” adage. In reading the code, we can see that there are 9 functions defined with similar-sounding names. Many of them have red-herring flags embedded in them; while you could solve this challenge by brute-force (as we’ll see in a moment, the flag is one of the strings in one of the methods), let’s exercise some analysis:
To help narrow our attention, we can trace the program calls:
main() if checkpass() else catmain() if catcheckpass() else catmain()
We can see that despite so many functions being called, only 4 functions are actually used. When run, the program asks the user for input, guessing what the flag is. The program then calls checkpass() to validate the input (returns False if incorrect). If incorrect, then it begins looping in catmain(). Thankfully, the flag is spelled out verbatim in the checkpass() function.
Lemon
Fine. I made it a bit more secure by not just leaving it directly in the code.
In this challenge, we are provided another python source code file to analyze and assess:
def checkpass():
userinput = input("Enter the password: ")
if userinput[0:4] == "rtcp":
if userinput[10:13] == "tHi":
if userinput[22:25] == "cuR":
if userinput[4:7] == "{y3":
if userinput[16:19] == "1nT":
if userinput[7:10] == "4H_":
if userinput[13:16] == "S_a":
if userinput[19:22] == "_sE":
if userinput [25:27] == "3}":
return True
else:
return False
def main():
access = checkpass()
if access == True:
print("Unlocked. The flag is the password.")
print("b-but i wunna show off my catswpeak uwu~... why wont you let me do my nya!!\noh well... good luck with the rest of the ctf :/\nbut I WANT TO SPWEAK CATGIRL NEXT TIME SO YOU BETTER LET ME >:(")
exit()
else:
print("Incorrect password!")
print("sowwy but now you gunnu have to listen to me spweak in cat giwrl speak uwu~")
catmain()
def catmain():
access = catcheckpass()
if access == True:
print("s-senpai... i unwocked it fowr you.. uwu~")
print("t-the fwlag is... the password.. nya!")
exit()
else:
print("sowwy but that wasnt quite rwight nya~")
catmain()
def catcheckpass():
userinput = input("pwease enter youwr password... uwu~ nya!!: ")
if userinput[0:4] == "rtcp":
if userinput[10:13] == "tHi":
if userinput[22:25] == "cuR":
if userinput[4:7] == "{y3":
if userinput[16:19] == "1nT":
if userinput[7:10] == "4H_":
if userinput[13:16] == "S_a":
if userinput[19:22] == "_sE":
if userinput [25:27] == "3}":
return True
else:
return False
access = False
main()
Just as before, this is another “security through obscurity” problem. The program calls main() which checks a user-entered flag guess using checkpass(). The rest of the program’s code can be ignored.
This checkpass() function - like the last problem - also literally compares the guess against strings within the code. Except in this case, checkpass() compares in out-of-order segments in 2-4 character chunks. Reassembling the flag can be done with simple copy/paste.
Squeezy
Ok this time, you aren’t getting anywhere near anything.
This challenge provides yet another python source code file to analyze and assess:
import base64
def checkpass():
userinput = input("Enter the password: ")
key = "meownyameownyameownyameownyameownya"
a = woah(key,userinput)
b = str.encode(a)
result = base64.b64encode(b, altchars=None)
if result == b'HxEMBxUAURg6I0QILT4UVRolMQFRHzokRBcmAygNXhkqWBw=':
return True
else:
return False
def main():
access = checkpass()
if access == True:
print("Unlocked. The flag is the password.")
print("pwease let me do my nya~ next time!!")
exit()
else:
print("Incorrect password!")
print("sowwy but now you gunnu have to listen to me spweak in cat giwrl speak uwu~")
catmain()
def catmain():
access = catcheckpass()
if access == True:
print("s-senpai... i unwocked it fowr you.. uwu~")
print("t-the fwlag is... the password.. nya!")
exit()
else:
print("sowwy but that wasnt quite rwight nya~... pwease twy again")
catmain()
def catcheckpass():
userinput = input("pwease enter youwr password... uwu~ nya!!: ")
key = "meownyameownyameownyameownyameownya"
a = woah(key,userinput)
b = str.encode(a)
result = base64.b64encode(b, altchars=None)
if result == b'HxEMBxUAURg6I0QILT4UVRolMQFRHzokRBcmAygNXhkqWBw=':
return True
else:
return False
def woah(s1,s2):
return ''.join(chr(ord(a) ^ ord(b)) for a,b in zip(s1,s2))
access = False
main()
This was the first python-based problem that did not contain the flag text immediately within the code. When run, the program calls main() and then checkpass(); checkpass() takes the user’s flag guess as input and then validates it with the help of the woah() function.
So what is happening in the code?
First, the code uses the variable “key” to encrypt the user input with the woah() function.
- The woah() function pairs each character in “key” with a corresponding one in the user’s guess, XORing them and storing them in variable “a”.
Next, the code encodes “a” with UTF-8 to a byte format, storing the result in variable “b”. This result is then encoded using base64.
Finally, the result is compared against a known base64 byte-string “b’HxEMBxUAURg6I0QILT4UVRolMQFRHzokRBcmAygNXhkqWBw=‘”.
Working in reverse, we can take the known base64 byte-string, decode it in base64 and UTF-8, then discover the final flag by XORing the result:
key = "meownyameownyameownyameownyameownya"
hash = 'HxEMBxUAURg6I0QILT4UVRolMQFRHzokRBcmAygNXhkqWBw='
dhash = base64.b64decode(hash) # b'\x1f\x11\x0c\x07\x15\x00Q\x18:#D\x08->\x14U\x1a%1\x01Q\x1f:$D\x17&\x03(\r^\x19*X\x1c'
prewoah = dhash.decode() # ^*X->U%1Q:$D&(
ans = ""
for idx in range(len(prewoah)):
decode = chr(ord(prewoah[idx]) ^ ord(key[idx])) #reverses what whoah() does
ans += decode
print(ans) //rtcp{y0u_L3fT_y0uR_x0r_K3y_bEh1nD!}
thedanzman
Fine. I made it even harder. It is now no longer “ez”, “pz”, “lemon” or “squeezy”. You will never get the flag this time.
We’re given yet another python source code file to analyze and assess:
import base64
import codecs
def checkpass():
userinput = input("Enter the password: ")
key = "nyameowpurrpurrnyanyapurrpurrnyanya"
key = codecs.encode(key, "rot_13")
a = nope(key,userinput)
b = str.encode(a)
c = base64.b64encode(b, altchars=None)
c = str(c)
d = codecs.encode(c, 'rot_13')
result = wow(d)
if result == "'=ZkXipjPiLIXRpIYTpQHpjSQkxIIFbQCK1FR3DuJZxtPAtkR'o":
return True
else:
return False
def main():
access = checkpass()
if access == True:
print("Unlocked. The flag is the password.")
print("pwease let me do my nya~ next time!!")
exit()
else:
print("Incorrect password!")
print("sowwy but now you gunnu have to listen to me spweak in cat giwrl speak uwu~")
catmain()
def catmain():
access = catcheckpass()
if access == True:
print("s-senpai... i unwocked it fowr you.. uwu~")
print("t-the fwlag is... the password.. nya!")
exit()
else:
print("sowwy but that wasnt quite rwight nya~... pwease twy again")
catmain()
def catcheckpass():
userinput = input("pwease enter youwr password... uwu~ nya!!: ")
key = "nyameowpurrpurrnyanyapurrpurrnyanya"
key = codecs.encode(key, "rot_13")
a = nope(key,userinput)
b = str.encode(a)
c = base64.b64encode(b, altchars=None)
c = str(c)
d = codecs.encode(c, 'rot_13')
result = wow(d)
if result == "'=ZkXipjPiLIXRpIYTpQHpjSQkxIIFbQCK1FR3DuJZxtPAtkR'o":
return True
else:
return False
def nope(s1,s2):
return ''.join(chr(ord(a) ^ ord(b)) for a,b in zip(s1,s2))
def wow(x):
return x[::-1]
access = False
main()
The final reverse engineering problem involving a python source code file builds upon the techniques employed in the last challenge. Again, the bulk of this program’s code can be ignored simply because it isn’t used or doesn’t involve flag discovery. Once more, main() calls checkpass(), which proceeds to manipulate, encode, and alter the user’s input before finally comparing it against a known jumbled string.
So what is happening in checkpass()? First, the code uses the codecs module to perform a ROT13 encryption on the user’s guess. Then, the program calls nope() to pair and XOR the characters from the encrypted guess and variable “key”, storing the results in variable “a”. That result is then UTF-8 encoded and base64 encoded before yet another ROT13 encryption is performed. Finally, the program calls wow() to flip the string in reverse order and compare it to the known jumbled string.
Like in the other challenges, the trick is simply to undo the steps listed above in reverse order, beginning with the known jumbled string:
key = "nyameowpurrpurrnyanyapurrpurrnyanya"
key = codecs.encode(key, "rot_13")
result = "'=ZkXipjPiLIXRpIYTpQHpjSQkxIIFbQCK1FR3DuJZxtPAtkR'o"
#reverse the string
result = result[::-1] # o'RktAPtxZJuD3RF1KCQbFIIxkQSjpHQpTYIpRXILiPjpiXkZ='
d = codecs.decode(result, 'rot_13') # b'ExgNCgkMWhQ3ES1XPDoSVVkxDFwcUDcGLVcEKVYvCwcvKxM='
hash = 'ExgNCgkMWhQ3ES1XPDoSVVkxDFwcUDcGLVcEKVYvCwcvKxM=' # stripped off the byte character
c = base64.b64decode(hash)
print(c)
b = c.decode()
ans = ""
for idx in range(len(b)):
decode = chr(ord(b[idx]) ^ ord(key[idx])) #reverses what whoah() does
ans += decode
print(ans) # rtcp{n0w_tH4T_w45_m0r3_cH4lL3NgiNG}