
PicoCTF 2019 Rev Writeups
Preface
PicoCTF was my first introduction to the world of CTF when I played PicoCTF 2021. PicoCTF 2019 is the only CTF available on the PicoGym that I did not participate in. As such, I decided to go back and solve the challenges and write up my solutions. Below are the solutions to all the reverse engineering challenges from PicoCTF 2019. Each writeup is a brief (informal) explanation of the solution written while solving the challenge, sorted by difficulty.
Challenge Writeups
vault-door-training (50 points)
For this challenge, we’re given the source code through the file VaultDoorTraining.java
import java.util.*;
class VaultDoorTraining {
public static void main(String args[]) {
VaultDoorTraining vaultDoor = new VaultDoorTraining();
Scanner scanner = new Scanner(System.in);
System.out.print("Enter vault password: ");
String userInput = scanner.next();
String input = userInput.substring("picoCTF{".length(),userInput.length()-1);
if (vaultDoor.checkPassword(input)) {
System.out.println("Access granted.");
} else {
System.out.println("Access denied!");
}
}
// The password is below. Is it safe to put the password in the source code?
// What if somebody stole our source code? Then they would know what our
// password is. Hmm... I will think of some ways to improve the security
// on the other doors.
//
// -Minion #9567
public boolean checkPassword(String password) {
return password.equals("w4rm1ng_Up_w1tH_jAv4_3808d338b46");
}
}
If we look at the last return statement, the password is obvious. Wrapping that with our picoCTF{}
flag wrapper, we get the flag. picoCTF{w4rm1ng_Up_w1tH_jAv4_3808d338b46}
vault-door-1 (100 points)
Similar to the last challenge, we’re given the java source code for a file called VaultDoor1.java
.
public boolean checkPassword(String password) {
return password.length() == 32 &&
password.charAt(0) == 'd' &&
password.charAt(29) == 'a' &&
password.charAt(4) == 'r' &&
password.charAt(2) == '5' &&
password.charAt(23) == 'r' &&
password.charAt(3) == 'c' &&
password.charAt(17) == '4' &&
password.charAt(1) == '3' &&
password.charAt(7) == 'b' &&
password.charAt(10) == '_' &&
password.charAt(5) == '4' &&
password.charAt(9) == '3' &&
password.charAt(11) == 't' &&
password.charAt(15) == 'c' &&
password.charAt(8) == 'l' &&
password.charAt(12) == 'H' &&
password.charAt(20) == 'c' &&
password.charAt(14) == '_' &&
password.charAt(6) == 'm' &&
password.charAt(24) == '5' &&
password.charAt(18) == 'r' &&
password.charAt(13) == '3' &&
password.charAt(19) == '4' &&
password.charAt(21) == 'T' &&
password.charAt(16) == 'H' &&
password.charAt(27) == '6' &&
password.charAt(30) == 'f' &&
password.charAt(25) == '_' &&
password.charAt(22) == '3' &&
password.charAt(28) == 'd' &&
password.charAt(26) == 'f' &&
password.charAt(31) == '4';
}
For this challenge, we see that the password verification is a little bit different. Essentially what the new password check function is doing is individually checking each character of the password. For example, the line password.charAt(0) == 'd' &&
checks to make sure that the character at the zeroth index of the password is d
.
Putting them all together, we get our flag. picoCTF{d35cr4mbl3_tH3_cH4r4cT3r5_f6daf4}
vault-door-3 (200 points)
We’re given another java file for this vault door. Password verification is once again different. Let’s see how!
public boolean checkPassword(String password) {
if (password.length() != 32) {
return false;
}
char[] buffer = new char[32];
int i;
for (i=0; i<8; i++) {
buffer[i] = password.charAt(i);
}
for (; i<16; i++) {
buffer[i] = password.charAt(23-i);
}
for (; i<32; i+=2) {
buffer[i] = password.charAt(46-i);
}
for (i=31; i>=17; i-=2) {
buffer[i] = password.charAt(i);
}
String s = new String(buffer);
return s.equals("jU5t_a_sna_3lpm12g94c_u_4_m7ra41");
}
char[] buffer = new char[32];
int i;
for (i=0; i<8; i++) {
buffer[i] = password.charAt(i);
}
This initializes an array of characters called buffer, and then puts the corresponding first 8 characters (indeces 0 to 7, inclusive) into the same posistions as they were.
for (; i<16; i++) {
buffer[i] = password.charAt(23-i);
}
For the next 8 characters in buffer (indeces 8 to 15, inclusive), this loop takes from string positions (23-8) to (23-15), or in other words goes from (15 to 8 inclusive). Essentially all this does is reverse the string from positions 8 to 15.
for (; i<32; i+=2) {
buffer[i] = password.charAt(46-i);
}
For this one, in our iterator, we see that it’s going from indeces 16 to 31. However, it’s iterating by adding 2 to i
each time. Note that the previous loop increments i
to 16, but i
is never run because it does not satisfy the conditional i < 16
. Combined with the increment of two, that means we’re actually starting at index 16, and going 16,18,20 … 30. Furthermore, 46 - 16 = 30, so it’s moving backwards from 30 all the way to 46 - 30 = 16.
for (i=31; i>=17; i-=2) {
buffer[i] = password.charAt(i);
}
This one does a similar take, but moves from indices 31 to 17 (inclusive), and decrements by two each time, so it takes every other character in the string and puts it in the same place as the buffer.
String s = new String(buffer);
return s.equals("jU5t_a_sna_3lpm12g94c_u_4_m7ra41");
The last two lines convert the char array into a string, and checks to make sure that the string we get at the end of all those functions should equal that string.
Let’s write a python script to reverse the operations done to get our original string!
original_string = "jU5t_a_sna_3lpm12g94c_u_4_m7ra41"
password_string = [""] * 32
# take first 8 characters and keep them the same
for i in range(8):
password_string[i] = original_string[i]
# for the next 8 characters, reverse them
for i in range(8, 16):
password_string[i] = original_string[23 - i]
# for the next 8 characters, take every other character
for i in range(16, 32, 2):
password_string[46-i] = original_string[i]
# for the next 8 characters, go backwards from the end
for i in range(31, 15, -2):
password_string[i] = original_string[i]
# convert to string and print
print(''.join(password_string))
Running this gets us our password, which we can then use to get our flag. picoCTF{jU5t_a_s1mpl3_an4gr4m_4_u_c79a21}
vault-door-4 (250 points)
Another java file, another different password check function.
public boolean checkPassword(String password) {
byte[] passBytes = password.getBytes();
byte[] myBytes = {
106 , 85 , 53 , 116 , 95 , 52 , 95 , 98 ,
0x55, 0x6e, 0x43, 0x68, 0x5f, 0x30, 0x66, 0x5f,
0142, 0131, 0164, 063 , 0163, 0137, 0146, 064 ,
'a' , '8' , 'c' , 'd' , '8' , 'f' , '7' , 'e' ,
};
for (int i=0; i<32; i++) {
if (passBytes[i] != myBytes[i]) {
return false;
}
}
return true;
}
Looking at this, we see that the comparison loop just checks to make sure that each character is equivalent to a certain number, or another character. In Java (and other languages), characters can also be equivalent to numbers. In this case, we’re given decimal numbers, hex numbers (prefix 0x), octal numbers (prefix 0), and just plain chars (represented with the single quotes '
).
Simple python script to convert these with the chr()
function. Note that in python octal numbers are represented with 0o
, so we have to change our script to accomadate that.
password_bytes = [
106,
85,
53,
116,
95,
52,
95,
98,
0x55,
0x6E,
0x43,
0x68,
0x5F,
0x30,
0x66,
0x5F,
0o142,
0o131,
0o164,
0o63,
0o163,
0o137,
0o146,
0o64,
"a",
"8",
"c",
"d",
"8",
"f",
"7",
"e",
]
password_string = ""
for char in password_bytes:
# if number convert to char
if isinstance(char, int):
password_string += chr(char)
# if string just add it
else:
password_string += char
print(password_string)
Running this gets us our password/flag. picoCTF{jU5t_4_bUnCh_0f_bYt3s_f4a8cd8f7e}
vault-door-5 (300 points)
Same java file, checkPassword function changed and this time we have a couple of helper functions.
public String base64Encode(byte[] input) {
return Base64.getEncoder().encodeToString(input);
}
public String urlEncode(byte[] input) {
StringBuffer buf = new StringBuffer();
for (int i=0; i<input.length; i++) {
buf.append(String.format("%%%2x", input[i]));
}
return buf.toString();
}
public boolean checkPassword(String password) {
String urlEncoded = urlEncode(password.getBytes());
String base64Encoded = base64Encode(urlEncoded.getBytes());
String expected = "JTYzJTMwJTZlJTc2JTMzJTcyJTc0JTMxJTZlJTY3JTVm"
+ "JTY2JTcyJTMwJTZkJTVmJTYyJTYxJTM1JTY1JTVmJTM2"
+ "JTM0JTVmJTM4JTM0JTY2JTY0JTM1JTMwJTM5JTM1";
return base64Encoded.equals(expected);
}
We don’t really need to understand what these do too much, it’s a pretty self explanatory base64encode and urlencode, so we just run the respective decodes. Here is a python script that does that.
# imports that decode stuff for us
import base64
import urllib.parse
encoded = "JTYzJTMwJTZlJTc2JTMzJTcyJTc0JTMxJTZlJTY3JTVmJTY2JTcyJTMwJTZkJTVmJTYyJTYxJTM1JTY1JTVmJTM2JTM0JTVmJTM4JTM0JTY2JTY0JTM1JTMwJTM5JTM1"
base64_decoded = base64.b64decode(encoded)
url_decoded = urllib.parse.unquote(base64_decoded)
print(url_decoded)
Running the script gets us our flag! picoCTF{c0nv3rt1ng_fr0m_ba5e_64_84fd5095}
vault-door-6 (350 points)
Basically same java file, new checkPassword function. This one is different because it uses XOR.
public boolean checkPassword(String password) {
if (password.length() != 32) {
return false;
}
byte[] passBytes = password.getBytes();
byte[] myBytes = {
0x3b, 0x65, 0x21, 0xa , 0x38, 0x0 , 0x36, 0x1d,
0xa , 0x3d, 0x61, 0x27, 0x11, 0x66, 0x27, 0xa ,
0x21, 0x1d, 0x61, 0x3b, 0xa , 0x2d, 0x65, 0x27,
0xa , 0x66, 0x36, 0x30, 0x67, 0x6c, 0x64, 0x6c,
};
for (int i=0; i<32; i++) {
if (((passBytes[i] ^ 0x55) - myBytes[i]) != 0) {
return false;
}
}
return true;
}
For those who don’t yet know how XOR works, check out the Wikipedia. What’s important for us however, is that if you take (A XOR B) XOR B
, you’ll get A back again. In other words, the inverse function to XOR is just XOR itself. Check out this stackoverflow post for more information.
We see that we’re given an array of numbers, and that our password XOR 0x55 must be equal to that array. So to find the password, we simply reverse each character in that array by 0x55. Here’s a python script implementing it.
password_pre_xor_bytes = [
0x3b, 0x65, 0x21, 0xa, 0x38, 0x0, 0x36, 0x1d, 0xa, 0x3d, 0x61, 0x27, 0x11,
0x66, 0x27, 0xa, 0x21, 0x1d, 0x61, 0x3b, 0xa, 0x2d, 0x65, 0x27, 0xa, 0x66,
0x36, 0x30, 0x67, 0x6c, 0x64, 0x6c
]
# xor every byte with 0x55 and convert to ascii
password = ''.join([chr(b ^ 0x55) for b in password_pre_xor_bytes])
print(password)
Running that script gets us our flag! picoCTF{n0t_mUcH_h4rD3r_tH4n_x0r_3ce2919}
vault-door-7 (400 points)
Another java file! This time we have a new checkPassword function, and a new helper function. Seems like this one is a bit more complicated and uses bit-shifts.
public int[] passwordToIntArray(String hex) {
int[] x = new int[8];
byte[] hexBytes = hex.getBytes();
for (int i=0; i<8; i++) {
x[i] = hexBytes[i*4] << 24
| hexBytes[i*4+1] << 16
| hexBytes[i*4+2] << 8
| hexBytes[i*4+3];
}
return x;
}
public boolean checkPassword(String password) {
if (password.length() != 32) {
return false;
}
int[] x = passwordToIntArray(password);
return x[0] == 1096770097
&& x[1] == 1952395366
&& x[2] == 1600270708
&& x[3] == 1601398833
&& x[4] == 1716808014
&& x[5] == 1734293296
&& x[6] == 842413104
&& x[7] == 1684157793;
}
Let’s first try to figure out what the passwordToIntArray function is doing. The first and last lines of the function tell us that it retuns an integer array x
of length 8. The hex is also converted to bytes, and then somehow converted into the int array. Let’s examine how it’s being converted into an int.
for (int i=0; i<8; i++) {
x[i] = hexBytes[i*4] << 24
| hexBytes[i*4+1] << 16
| hexBytes[i*4+2] << 8
| hexBytes[i*4+3];
}
Essentially, we loop over every set of four bytes (8 loops for 32 total). Each character takes up one byte, or 8 bits. We also see that the fourth hex byte in each group is shifted to the left by 24 bits, three bytes, or 3 characters. The second hex byte is shifted by two characters, and the third by one, and the fourth by zero. They are then paired together using the |
OR operator, which essentially serves to concatenate them in this case. I’d reccomend researching more about how this works if you don’t understand. What this means however, is that we can write a python script that converts those integers back to groups of four characters.
x = [1096770097, 1952395366, 1600270708, 1601398833, 1716808014, 1734293296, 842413104, 1684157793]
# convert to integers to hex, then bytes then decode to ascii
reversed = ''.join([bytes.fromhex(hex(i)[2:]).decode('utf-8') for i in x])
print(reversed)
Running the script, we get our flag! picoCTF{A_b1t_0f_b1t_sh1fTiNg_702640db5a}
vault-door-8 (450 points)
We open our java file again, but this time it seems like the file has been scrambled around a bit.
// These pesky special agents keep reverse engineering our source code and then
// breaking into our secret vaults. THIS will teach those sneaky sneaks a
// lesson.
//
// -Minion #0891
import java.util.*; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec;
import java.security.*; class VaultDoor8 {public static void main(String args[]) {
Scanner b = new Scanner(System.in); System.out.print("Enter vault password: ");
String c = b.next(); String f = c.substring(8,c.length()-1); VaultDoor8 a = new VaultDoor8(); if (a.checkPassword(f)) {System.out.println("Access granted."); }
else {System.out.println("Access denied!"); } } public char[] scramble(String password) {/* Scramble a password by transposing pairs of bits. */
char[] a = password.toCharArray(); for (int b=0; b<a.length; b++) {char c = a[b]; c = switchBits(c,1,2); c = switchBits(c,0,3); /* c = switchBits(c,14,3); c = switchBits(c, 2, 0); */ c = switchBits(c,5,6); c = switchBits(c,4,7);
c = switchBits(c,0,1); /* d = switchBits(d, 4, 5); e = switchBits(e, 5, 6); */ c = switchBits(c,3,4); c = switchBits(c,2,5); c = switchBits(c,6,7); a[b] = c; } return a;
} public char switchBits(char c, int p1, int p2) {/* Move the bit in position p1 to position p2, and move the bit
that was in position p2 to position p1. Precondition: p1 < p2 */ char mask1 = (char)(1 << p1);
char mask2 = (char)(1 << p2); /* char mask3 = (char)(1<<p1<<p2); mask1++; mask1--; */ char bit1 = (char)(c & mask1); char bit2 = (char)(c & mask2); /* System.out.println("bit1 " + Integer.toBinaryString(bit1));
System.out.println("bit2 " + Integer.toBinaryString(bit2)); */ char rest = (char)(c & ~(mask1 | mask2)); char shift = (char)(p2 - p1); char result = (char)((bit1<<shift) | (bit2>>shift) | rest); return result;
} public boolean checkPassword(String password) {char[] scrambled = scramble(password); char[] expected = {
0xF4, 0xC0, 0x97, 0xF0, 0x77, 0x97, 0xC0, 0xE4, 0xF0, 0x77, 0xA4, 0xD0, 0xC5, 0x77, 0xF4, 0x86, 0xD0, 0xA5, 0x45, 0x96, 0x27, 0xB5, 0x77, 0xD2, 0xD0, 0xB4, 0xE1, 0xC1, 0xE0, 0xD0, 0xD0, 0xE0 }; return Arrays.equals(scrambled, expected); } }
Definitely quite hard to read, so we run it through a java beautifier, and then extract the important functions.
public char[] scramble(String password) {
/* Scramble a password by transposing pairs of bits. */
char[] a = password.toCharArray();
for (int b = 0; b < a.length; b++) {
char c = a[b];
c = switchBits(c, 1, 2);
c = switchBits(c, 0, 3); /* c = switchBits(c,14,3); c = switchBits(c, 2, 0); */
c = switchBits(c, 5, 6);
c = switchBits(c, 4, 7);
c = switchBits(c, 0, 1); /* d = switchBits(d, 4, 5); e = switchBits(e, 5, 6); */
c = switchBits(c, 3, 4);
c = switchBits(c, 2, 5);
c = switchBits(c, 6, 7);
a[b] = c;
}
return a;
}
public char switchBits(char c, int p1, int p2) {
/* Move the bit in position p1 to position p2, and move the bit
that was in position p2 to position p1. Precondition: p1 < p2 */
char mask1 = (char)(1 << p1);
char mask2 = (char)(1 << p2); /* char mask3 = (char)(1<<p1<<p2); mask1++; mask1--; */
char bit1 = (char)(c & mask1);
char bit2 = (char)(c & mask2);
/* System.out.println("bit1 " + Integer.toBinaryString(bit1));
System.out.println("bit2 " + Integer.toBinaryString(bit2)); */
char rest = (char)(c & ~(mask1 | mask2));
char shift = (char)(p2 - p1);
char result = (char)((bit1 << shift) | (bit2 >> shift) | rest);
return result;
}
public boolean checkPassword(String password) {
char[] scrambled = scramble(password);
char[] expected = {
0xF4,
0xC0,
0x97,
0xF0,
0x77,
0x97,
0xC0,
0xE4,
0xF0,
0x77,
0xA4,
0xD0,
0xC5,
0x77,
0xF4,
0x86,
0xD0,
0xA5,
0x45,
0x96,
0x27,
0xB5,
0x77,
0xD2,
0xD0,
0xB4,
0xE1,
0xC1,
0xE0,
0xD0,
0xD0,
0xE0
};
return Arrays.equals(scrambled, expected);
}
Let’s just take the switchBits function for what it is, and assume it swaps bits. For the scrambling function, all we need to do is write an unscramble function that swaps the bits around in the opposite order, and that takes in the expected array. Let’s just hop into implementing this in python.
scrambled = [
0xF4,
0xC0,
0x97,
0xF0,
0x77,
0x97,
0xC0,
0xE4,
0xF0,
0x77,
0xA4,
0xD0,
0xC5,
0x77,
0xF4,
0x86,
0xD0,
0xA5,
0x45,
0x96,
0x27,
0xB5,
0x77,
0xD2,
0xD0,
0xB4,
0xE1,
0xC1,
0xE0,
0xD0,
0xD0,
0xE0,
]
def switch_bits(c, p1, p2):
mask1 = 1 << p1
mask2 = 1 << p2
bit1 = c & mask1
bit2 = c & mask2
rest = c & ~(mask1 | mask2)
shift = p2 - p1
result = (bit1 << shift) | (bit2 >> shift) | rest
return result
def unscramble(scrambled):
unscrambled = []
for i in scrambled:
new_char = switch_bits(i, 6, 7)
new_char = switch_bits(new_char, 2, 5)
new_char = switch_bits(new_char, 3, 4)
new_char = switch_bits(new_char, 0, 1)
new_char = switch_bits(new_char, 4, 7)
new_char = switch_bits(new_char, 5, 6)
new_char = switch_bits(new_char, 0, 3)
new_char = switch_bits(new_char, 1, 2)
unscrambled.append(new_char)
return unscrambled
# convert unscrambled to ascii
unscrambled_ascii_string = "".join([chr(i) for i in unscramble(scrambled)])
print(unscrambled_ascii_string)
Running this, we get our flag! picoCTF{s0m3_m0r3_b1t_sh1fTiNg_91c642112}
asm1 (200 points)
Well here’s our first assembly challenge! We’re supposed to figure out what asm1(0x8be) returns, and submit that as our flag.
asm1:
<+0>: push ebp
<+1>: mov ebp,esp
<+3>: cmp DWORD PTR [ebp+0x8],0x71c
<+10>: jg 0x512 <asm1+37>
<+12>: cmp DWORD PTR [ebp+0x8],0x6cf
<+19>: jne 0x50a <asm1+29>
<+21>: mov eax,DWORD PTR [ebp+0x8]
<+24>: add eax,0x3
<+27>: jmp 0x529 <asm1+60>
<+29>: mov eax,DWORD PTR [ebp+0x8]
<+32>: sub eax,0x3
<+35>: jmp 0x529 <asm1+60>
<+37>: cmp DWORD PTR [ebp+0x8],0x8be
<+44>: jne 0x523 <asm1+54>
<+46>: mov eax,DWORD PTR [ebp+0x8]
<+49>: sub eax,0x3
<+52>: jmp 0x529 <asm1+60>
<+54>: mov eax,DWORD PTR [ebp+0x8]
<+57>: add eax,0x3
<+60>: pop ebp
<+61>: ret
Note that at the start, the stack looks something like this:
| 0x8b3 <- ebp + 8
| return address <- ebp + 4
| old esp <- ebp points here
Here is a commented and slightly edited version to trace through the assembly.
asm1:
<+0>: push ebp ; pushes 0x8be from stack to ebp
<+1>: mov ebp,esp ; moves 0x8be in ebp to esp
<+3>: cmp DWORD PTR [ebp+0x8],0x71c ; sets up comparison between 0x8be and 0x71c
<+10>: jg 0x512 <asm1+37> ; jump if greater, 0x8be > 0x71c, so jump to <asm1+37>
<+37>: cmp DWORD PTR [ebp+0x8],0x8be ; place jumped to, setup 0x8be comparison with 0x8be
<+44>: jne 0x523 <asm1+54> ; jump if not equal, is equal, so no jump
<+46>: mov eax,DWORD PTR [ebp+0x8] ; moves 0x8be to eax
<+49>: sub eax,0x3 ; subtracts 0x3 from eax, eax now 0x8bb
<+52>: jmp 0x529 <asm1+60> ; jump to 60
<+60>: pop ebp ; pops the stack
<+61>: ret ; returns whatever value is at stack (0x8bb)
So the final hex we get, which is our flag, is: 0xbb
asm2 (250 points)
We are once again given an asm file. We’re told to give the output for asm2(0xb, 0x2e).
asm2:
<+0>: push ebp
<+1>: mov ebp,esp
<+3>: sub esp,0x10
<+6>: mov eax,DWORD PTR [ebp+0xc]
<+9>: mov DWORD PTR [ebp-0x4],eax
<+12>: mov eax,DWORD PTR [ebp+0x8]
<+15>: mov DWORD PTR [ebp-0x8],eax
<+18>: jmp 0x509 <asm2+28>
<+20>: add DWORD PTR [ebp-0x4],0x1
<+24>: sub DWORD PTR [ebp-0x8],0xffffff80
<+28>: cmp DWORD PTR [ebp-0x8],0x63f3
<+35>: jle 0x501 <asm2+20>
<+37>: mov eax,DWORD PTR [ebp-0x4]
<+40>: leave
<+41>: ret
Note that at the start of the program, the stack looks something like this (after <+3>):
| 0x2e <- ebp + 12
| 0xb <- ebp + 8
| return address <- ebp + 4
| old esp <- ebp points here
| esp now <- ebp - 16???
asm2:
<+0>: push ebp ; Saves old value of ebp
<+1>: mov ebp,esp ; Saves old value of esp
<+3>: sub esp,0x10 ; Allocate 0x10 in stack
<+6>: mov eax,DWORD PTR [ebp+0xc] ; moves whatever's at 0xc (0x2e) to eax
<+9>: mov DWORD PTR [ebp-0x4],eax ; moves eax (0x2e) to four below the base pointer, aka 12.
<+12>: mov eax,DWORD PTR [ebp+0x8] ; moves (0xb) at ebp+0x8 to eax
<+15>: mov DWORD PTR [ebp-0x8],eax ; moves this nothing at eax to ebp-0x8
<+18>: jmp 0x509 <asm2+28> ; jumps to line 28
<+20>: add DWORD PTR [ebp-0x4],0x1 ; adds 0x1 to ebp-0x4 a couple times.
<+24>: sub DWORD PTR [ebp-0x8],0xffffff80 ; keeps on subtracting by 0xffffff80, basically add 0x80
<+28>: cmp DWORD PTR [ebp-0x8],0x63f3 ; (0x63f3 - 0xb)/ 0x80 = basically 200.
<+35>: jle 0x501 <asm2+20> ; jumps back to line 20 if 0x63f3 less than or equal. Essentially while loop
<+37>: mov eax,DWORD PTR [ebp-0x4] ; moves arg in 0x4 back to ebp. arg4 is now 200 + 0x2e
<+40>: leave ; this and below essentially resets and returns whats in eax
<+41>: ret ; returns 0xF6
At the end, this should return our flag, which is: 0xF6
asm3 (300 points)
Given an ASM file, asked what asm3(0xba6c5a02,0xd101e3dd,0xbb86a173) returns.
asm3:
<+0>: push ebp
<+1>: mov ebp,esp
<+3>: xor eax,eax
<+5>: mov ah,BYTE PTR [ebp+0xb]
<+8>: shl ax,0x10
<+12>: sub al,BYTE PTR [ebp+0xd]
<+15>: add ah,BYTE PTR [ebp+0xc]
<+18>: xor ax,WORD PTR [ebp+0x12]
<+22>: nop
<+23>: pop ebp
<+24>: ret
The stack looks like this.
| 0xbb86a173 <- ebp + 16
| 0xd101e3dd <- ebp + 12
| 0xba6c5a02 <- ebp + 8
| return address <- ebp + 4
| old esp <- ebp points here
Lets annotate the code and see how it goes.
asm3:
<+0>: push ebp
<+1>: mov ebp,esp ; func prologue
<+3>: xor eax,eax ; xors eax with itself, so all zeros
<+5>: mov ah,BYTE PTR [ebp+0xb] ; moves byte (0xba) at 0xb to ah
<+8>: shl ax,0x10 ; bit shifts ax left by 16 bits, eax now zero
<+12>: sub al,BYTE PTR [ebp+0xd] ; subtracts the byte (0xdd), so 0x00 - 0xe3 = 0x1d
<+15>: add ah,BYTE PTR [ebp+0xc] ; add to ah what's at 0xc (0xdd), ah = 0xdd
<+18>: xor ax,WORD PTR [ebp+0x12] ; xor ax (0xdd1d) with what's at ebp+0x12 (0xbb86)
<+22>: nop ; nothing happens here
<+23>: pop ebp ; returns whats on eax which is just = 0x669b
<+24>: ret
As such, we find the output, and our flag to be: 0x669b
asm4 (400 points)
The final ASM challenge! We need to find what asm4(“picoCTF_f97bb”) returns. This one is the scariest by far.
asm4:
<+0>: push ebp
<+1>: mov ebp,esp
<+3>: push ebx
<+4>: sub esp,0x10
<+7>: mov DWORD PTR [ebp-0x10],0x27a
<+14>: mov DWORD PTR [ebp-0xc],0x0
<+21>: jmp 0x518 <asm4+27>
<+23>: add DWORD PTR [ebp-0xc],0x1
<+27>: mov edx,DWORD PTR [ebp-0xc]
<+30>: mov eax,DWORD PTR [ebp+0x8]
<+33>: add eax,edx
<+35>: movzx eax,BYTE PTR [eax]
<+38>: test al,al
<+40>: jne 0x514 <asm4+23>
<+42>: mov DWORD PTR [ebp-0x8],0x1
<+49>: jmp 0x587 <asm4+138>
<+51>: mov edx,DWORD PTR [ebp-0x8]
<+54>: mov eax,DWORD PTR [ebp+0x8]
<+57>: add eax,edx
<+59>: movzx eax,BYTE PTR [eax]
<+62>: movsx edx,al
<+65>: mov eax,DWORD PTR [ebp-0x8]
<+68>: lea ecx,[eax-0x1]
<+71>: mov eax,DWORD PTR [ebp+0x8]
<+74>: add eax,ecx
<+76>: movzx eax,BYTE PTR [eax]
<+79>: movsx eax,al
<+82>: sub edx,eax
<+84>: mov eax,edx
<+86>: mov edx,eax
<+88>: mov eax,DWORD PTR [ebp-0x10]
<+91>: lea ebx,[edx+eax*1]
<+94>: mov eax,DWORD PTR [ebp-0x8]
<+97>: lea edx,[eax+0x1]
<+100>: mov eax,DWORD PTR [ebp+0x8]
<+103>: add eax,edx
<+105>: movzx eax,BYTE PTR [eax]
<+108>: movsx edx,al
<+111>: mov ecx,DWORD PTR [ebp-0x8]
<+114>: mov eax,DWORD PTR [ebp+0x8]
<+117>: add eax,ecx
<+119>: movzx eax,BYTE PTR [eax]
<+122>: movsx eax,al
<+125>: sub edx,eax
<+127>: mov eax,edx
<+129>: add eax,ebx
<+131>: mov DWORD PTR [ebp-0x10],eax
<+134>: add DWORD PTR [ebp-0x8],0x1
<+138>: mov eax,DWORD PTR [ebp-0xc]
<+141>: sub eax,0x1
<+144>: cmp DWORD PTR [ebp-0x8],eax
<+147>: jl 0x530 <asm4+51>
<+149>: mov eax,DWORD PTR [ebp-0x10]
<+152>: add esp,0x10
<+155>: pop ebx
<+156>: pop ebp
<+157>: ret
Not going to do the usual comment as this one needs to be broken down a lot more.
Let’s start with the prologue.
<+0>: push ebp ; pushes ebp onto stack
<+1>: mov ebp,esp ; puts esp to ebp
<+3>: push ebx ; pushes ebx ... array onto stack
<+4>: sub esp,0x10 ; decrements the stack pointer by 16 bits
Currently, the stack looks like this.
# stack
| 0x400000 <- ebp + 8
^ address points to the contents of the string
| return address <- ebp + 4
| old esp <- ebp points here
| stuff underneath
# somewhere in memory lets say addr 0x400000
| "picoCTF_f97bb"
Let’s look at the next section now.
<+7>: mov DWORD PTR [ebp-0x10],0x27a ; Add an 0x27a to 0x10.
<+14>: mov DWORD PTR [ebp-0xc],0x0 ; Set 0xc to 0x0
<+21>: jmp 0x518 <asm4+27> ; Jump straight to line 27
<+23>: add DWORD PTR [ebp-0xc],0x1 ; add one to ebp-0xc
<+27>: mov edx,DWORD PTR [ebp-0xc] ; set edx equal to whatevers in ebp-0xc
<+30>: mov eax,DWORD PTR [ebp+0x8] ; set address of string to eax
<+33>: add eax,edx ; add value of edx to eax.
<+35>: movzx eax,BYTE PTR [eax] ; copies the value of the pointer to eax, and extends to 32 bits w/ zeros
<+38>: test al,al ; does bitwise & between al and al.
<+40>: jne 0x514 <asm4+23> ; if al != 0, then jump back up! basically keep on going until eax == 0
Essentially what this code is doing is going all the way until it finds the null, which is at the end of the string. The memory looks like this now.
# stack
| 0x400000 <- ebp + 8
^ address points to the contents of the string
| return address <- ebp + 4
| old esp <- ebp points here
| len("picoCTF_f97bb") <- ebp - 12
| 0x27a <- ebp - 16
# not stack
| 0 <- eax, null byte at end of string
# somewhere in memory lets say addr 0x400000
| "picoCTF_f97bb"
Let’s look at the next (warning: HUGE) chunk.
<+42>: mov DWORD PTR [ebp-0x8],0x1 ; set ebp-0x8 to 0x1
<+49>: jmp 0x587 <asm4+138> ; jump to line 138
<+51>: mov edx,DWORD PTR [ebp-0x8] ; set edx to ebp-0x8 (0x1 on first run)
<+54>: mov eax,DWORD PTR [ebp+0x8] ; set eax to address of string
<+57>: add eax,edx ; add iterator count (think as i = 0, i++ etc.) to eax
<+59>: movzx eax,BYTE PTR [eax] ; copy the current character to eax, do a zero extend too
<+62>: movsx edx,al ; copy the current character to edx (from eax)
<+65>: mov eax,DWORD PTR [ebp-0x8] ; set eax to ebp-0x8 (0x1 on first run)
<+68>: lea ecx,[eax-0x1] ; set ecx to value one less than whats at the address eax
<+71>: mov eax,DWORD PTR [ebp+0x8] ; set eax to address of string
<+74>: add eax,ecx ; add ecx to eax. Basically one less than our iterator. (i-1)
<+76>: movzx eax,BYTE PTR [eax] ; copy value of pointer to eax, and extend to 32 bits w/ zeros
<+79>: movsx eax,al ; copy value of al to eax, and extend to 32 bits w/ sign
<+82>: sub edx,eax ; subtract that character in eax from edx
<+84>: mov eax,edx ; set eax to edx
<+86>: mov edx,eax ; set edx to eax
<+88>: mov eax,DWORD PTR [ebp-0x10] ; set eax to ebp-0x10 (0x27a on first run)
<+91>: lea ebx,[edx+eax*1] ; set ebx to edx + eax * 1
<+94>: mov eax,DWORD PTR [ebp-0x8] ; set eax to ebp-0x8 (0x1 on first run)
<+97>: lea edx,[eax+0x1] ; set edx to eax + 0x1
<+100>: mov eax,DWORD PTR [ebp+0x8] ; set eax to address of string
<+103>: add eax,edx ; add edx to eax
<+105>: movzx eax,BYTE PTR [eax] ; copy value of pointer to eax, and extend to 32 bits w/ zeros
<+108>: movsx edx,al ; copy value of al to edx, and extend to 32 bits w/ sign
<+111>: mov ecx,DWORD PTR [ebp-0x8] ; set ecx to ebp-0x8 (0x1 on first run)
<+114>: mov eax,DWORD PTR [ebp+0x8] ; set eax to address of string
<+117>: add eax,ecx ; add ecx to eax
<+119>: movzx eax,BYTE PTR [eax] ; copy value of pointer to eax, and extend to 32 bits w/ zeros
<+122>: movsx eax,al ; copy value of al to eax, and extend to 32 bits w/ sign
<+125>: sub edx,eax ; subtract eax from edx
<+127>: mov eax,edx ; set eax to edx
<+129>: add eax,ebx ; add ebx to eax
<+131>: mov DWORD PTR [ebp-0x10],eax ; set ebp-0x10 to eax
<+134>: add DWORD PTR [ebp-0x8],0x1 ; increment ebp-0x8
<+138>: mov eax,DWORD PTR [ebp-0xc] ; set eax to [ebp-0xc], len("picoCTF_f97bb")
<+141>: sub eax,0x1 ; subtract 1 from eax
<+144>: cmp DWORD PTR [ebp-0x8],eax ; compare [ebp-0x8] to eax
<+147>: jl 0x530 <asm4+51> ; if [ebp-0x8] < eax, jump back up to line 51. Basically iterate over the string.
Well that was nice. Essentially this just goes through the string starting from the 1st index all the way to one before length of the string. It then adds the value of each character to some value that is stored, subtracts the value of the character before it, and then adds the value of the next character subtracted by the value of the current character. Note that the current character is added and subtracted, so it cancels out. So basically it iterates over each character in the string, and each time adds the value of the next character, but subtracts the last character.
These are the final lines
<+149>: mov eax,DWORD PTR [ebp-0x10] ; move back ebp-0x10 to eax
<+152>: add esp,0x10 ; return stuff to stack probably
<+155>: pop ebx ; pop ebx
<+156>: pop ebp ; pop ebp
<+157>: ret ; return eax stuffs
The equivalent python code is below. I used kusano_k’s writeup as a reference.
s = list(map(ord, "picoCTF_f97bb"))
a = 0x27a
for i in range(1, len(s)-1):
a += s[i+1] - s[i-1]
print(hex(a))
Running that, we get our hex value, which is also our flag. 0x265
reverse_cipher (300 points)
We’re given a binary and a text file. The text file contains a slightly encrypted flag. Let’s assume the binary is the program that encrypted the flag. So we’ll need to reverse the binary to figure out how it did that, and then we can undo the encryption.
Putting it through binary ninja’s decompiler, we get this as our main function.
int32_t main(int32_t argc, char ** argv, char ** envp) {
FILE * rax = fopen("flag.txt", & data_2008);
FILE * rax_1 = fopen("rev_this", & data_2013);
if (rax == 0) {
puts("No flag found, please make sure …");
}
if (rax_1 == 0) {
puts("please run this on the server");
}
void var_58;
if (fread( & var_58, 0x18, 1, rax) <= 0) {
exit(0);
/* no return */
}
for (int32_t var_10 = 0; var_10 <= 7; var_10 = (var_10 + 1)) {
fputc( * ( & var_58 + var_10), rax_1);
}
for (int32_t var_14 = 8; var_14 <= 0x16; var_14 = (var_14 + 1)) {
char rax_9 = * ( & var_58 + var_14);
char var_9_3;
if ((var_14 & 1) != 0) {
var_9_3 = (rax_9 - 2);
} else {
var_9_3 = (rax_9 + 5);
}
fputc(var_9_3, rax_1);
}
char var_41;
fputc(var_41, rax_1);
fclose(rax_1);
return fclose(rax);
}
Running this through my local LLM, I get it to rename variables and functions to produce a more readable output.
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv, char **envp) {
FILE *flagFile = fopen("flag.txt", "r");
FILE *revFile = fopen("rev_this", "w");
if (flagFile == NULL) {
puts("No flag found. Please make sure the file 'flag.txt' exists.");
}
if (revFile == NULL) {
puts("Please run this on the server.");
}
char flagData[24];
if (fread(flagData, sizeof(char), sizeof(flagData), flagFile) <= 0) {
exit(0);
}
for (int i = 0; i <= 7; i++) {
fputc(flagData[i], revFile);
}
for (int i = 8; i <= 22; i++) {
char currentChar = flagData[i];
char modifiedChar;
if ((i & 1) != 0) {
modifiedChar = currentChar - 2;
} else {
modifiedChar = currentChar + 5;
}
fputc(modifiedChar, revFile);
}
char lastChar;
fputc(lastChar, revFile);
fclose(revFile);
fclose(flagFile);
return 0;
}
Well it should be pretty trivial to write a python script to reverse this now that we know what the c binary is doing. Let’s try that.
encrypted_text = "picoCTF{w1{1wq8/7376j.:}"
decrypted_text = ""
# keep chars 1 through 7 inclusive the same
for i in range(8):
decrypted_text += encrypted_text[i]
# for chars 8 through 22 inclusive
for i in range(8, 23):
# if i & 1 != 0, i is odd
if i & 1 != 0:
decrypted_text += chr(ord(encrypted_text[i]) + 2)
# else i is even
else:
decrypted_text += chr(ord(encrypted_text[i]) - 5)
# put the last char in the decrypted text
decrypted_text += encrypted_text[23]
print(decrypted_text)
Running that, we get our flag. picoCTF{r3v3rs312528e05}
Need For Speed
Let’s just toss this into binary ninja again. We get a main function and several helper functions
uint64_t decrypt_flag(int32_t arg1) {
int32_t var_1c = arg1;
int32_t var_c = 0;
uint64_t rax_19;
while (true) {
rax_19 = var_c;
if (rax_19 > 0x36) {
break;
}
int32_t temp0_1;
int32_t temp1_1;
temp0_1 = HIGHD(var_c);
temp1_1 = LOWD(var_c);
uint32_t rdx_3 = (temp0_1 >> 0x1f);
*(var_c + & flag) = ( * (var_c + & flag) ^ * ((((temp1_1 + rdx_3) & 1) - rdx_3) + & var_1c));
int32_t temp2_1;
int32_t temp3_1;
temp2_1 = HIGHD((var_c * 0x55555556));
temp3_1 = LOWD((var_c * 0x55555556));
int32_t rdx_6 = (temp2_1 - (var_c >> 0x1f));
if ((var_c - ((rdx_6 + rdx_6) + rdx_6)) == 2) {
var_1c = (var_1c + 1);
}
var_c = (var_c + 1);
}
return rax_19;
}
uint64_t calculate_key() {
int32_t var_c = 0xce61f8de;
do {
var_c = (var_c - 1);
} while (var_c != 0xe730fc6f);
return var_c;
}
int64_t alarm_handler() __noreturn {
int32_t rdi;
int32_t var_c = rdi;
puts("Not fast enough. BOOM!");
exit(0);
/* no return */
}
int64_t set_timer() {
int32_t var_14 = 1;
if (__sysv_signal(0xe, alarm_handler) != -1) {
return alarm(1);
}
puts("\n\nSomething bad happened here.…");
exit(0);
/* no return */
}
int64_t get_key() {
puts("Creating key...");
key = calculate_key();
return puts("Finished");
}
int64_t print_flag() {
puts("Printing flag:");
decrypt_flag(key);
return puts( & flag);
}
int64_t header() {
puts("Keep this thing over 50 mph!");
for (int32_t var_c = 0; var_c <= 0x1b; var_c = (var_c + 1)) {
putchar(0x3d);
}
return puts( & data_a6d);
}
int32_t main(int32_t argc, char ** argv, char ** envp) {
int32_t var_c = argc;
char ** var_18 = argv;
header();
set_timer();
get_key();
print_flag();
return 0;
}
Let’s break down what the code does. When we run the prgoram, we get the following output.
Keep this thing over 50 mph!
============================
Creating key...
Not fast enough. BOOM!
We see that the "Not fast enough. BOOM!"
is called in alarm_handler()
, which is called inside set_timer()
. This prevents us from running the program and actually getting to the print_flag()
function.
I tried opening this in GDB, and stepping through and directly running the print_flag()
function, but that didn’t work. The reason why this didn’t work was that I tried running it at the beginning of the program, before get_key()
is run. It was probably possible to use gdb of some sort to get this challenge working, but I ended up using a different method.
This different method is of patching the binary. Binary Ninja has a really simple tool for binary patching. I’m not sure if other decompilers like ghidra have this feature, but it’s really easy to use with binary ninja. We simply patch out the call to set_timer()
and we can run the program without the alarm going off, and we get our flag.
PICOCTF{Good job keeping bus #190ca38b speeding along!}
Forky
Let’s open this binary in Binary Ninja. We get the following decompilation.
{
void* const var_4 = __return_addr;
int32_t* var_10 = &argc;
int32_t* eax = mmap(nullptr, 4, 3, 0x21, 0xffffffff, 0);
*(int32_t*)eax = 0x3b9aca00;
fork();
fork();
fork();
fork();
*(int32_t*)eax = (*(int32_t*)eax + 0x499602d2);
doNothing(*(int32_t*)eax);
return 0;
}
My initial plan was to step through this with gdb. However, it didn’t run through properly on my computer.
Running it in the picowebshell, and setting a breakpoint on doNothing (b doNothing
), and then printing the value of eax (p $eax
) gives us the flag.
This felt too easy. Looking at other writeups, it seemed like another solution would be to realize that fork calls 16 times, and then run 16 of the eax adds, or to just recompile this in C again with some extra print statements. Not sure why this was a 500 point challenge.
picoCTF{-721750240}