# Unbreakable Write-up >From: 1753CTF >Tags: Crypto >Problem statement: > Me: Is one-time-pad unbreakable? > > Chat GPT: Yes, Your Awesomeness, a one-time pad is theoretically unbreakable when used correctly. This is because each bit of plaintext is encrypted with a completely random bit of the key, and each key is used only once, making it impossible to derive the original message without the exact key. > > Me: Okay, let's get that completely random bits! > > https://dl.1753ctf.com/unbreakable/enc.cs?s=h4JhwfVc > > Solved by: Jackylkk2003 > File attached: > `unbreakable_enc.cs`: > ```csharp= > var seed = new DateTimeOffset(DateTime.Today).ToUnixTimeSeconds(); > var random = new Random((int)seed); > > var randomBuffer = new byte[flag.Length]; > random.NextBytes(randomBuffer); > > var flagBuffer = Encoding.ASCII.GetBytes(flag); > var resultBuffer = new byte[flag.Length]; > > for (var i = 0; i < flag.Length; i++) > resultBuffer[i] = (byte)(randomBuffer[i] ^ flagBuffer[i]); > > var encrypted = string.Concat(resultBuffer.Select(x => x.ToString("X").PadLeft(2, '0'))); > > Console.WriteLine(encrypted); // 22ECCDB90936D5C2454A65A5BB4C120FB1C8567381C6DB368EB57D4C6BE8B6D8C860E5C6FAC1F48BF2291A5C9EA3C354715857E7 > ``` ## Problem Analysis When I first see this question, my first thought is: "What is a .cs file?" Of course, it did not take me a lot of effort to find it out: ![file](https://hackmd.io/_uploads/SypdgNXC6.png) It looks like a C# program to me. At least it says so. ![file2](https://hackmd.io/_uploads/SkgslEQAT.png) Although I don't know C#, what the code does is obvious (Also hinted by the problem statement). It is a one-time pad, that is a XOR cipher. Some (not so useful) hints from the problem statement by Chat GPT: A one-time pad is theoretically unbreakable **when used correctly**. Let's look at the code in depth: ```csharp= var seed = new DateTimeOffset(DateTime.Today).ToUnixTimeSeconds(); var random = new Random((int)seed); var randomBuffer = new byte[flag.Length]; random.NextBytes(randomBuffer); ``` These lines generates a (**pseudo**-)random sequence of bytes having the same length as the flag using a seed **based on the current time**. ![Documentation](https://hackmd.io/_uploads/B1nVQVQC6.png) [Documentations](https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset.tounixtimeseconds?view=net-8.0) are our good friends! Now we know that the `ToUnixTimeSeconds` **returns an integer** (I mean int, int64, long, ..., just not floating point numbers). ```csharp= var flagBuffer = Encoding.ASCII.GetBytes(flag); var resultBuffer = new byte[flag.Length]; for (var i = 0; i < flag.Length; i++) resultBuffer[i] = (byte)(randomBuffer[i] ^ flagBuffer[i]); var encrypted = string.Concat(resultBuffer.Select(x => x.ToString("X").PadLeft(2, '0'))); Console.WriteLine(encrypted); // 22ECCDB90936D5C2454A65A5BB4C120FB1C8567381C6DB368EB57D4C6BE8B6D8C860E5C6FAC1F48BF2291A5C9EA3C354715857E7 ``` This part of the code does one-time pad on the flag and then output the hexadecimals as a string. Nothing looks problematic here. ## Solution The hints to the solution are already bolded above. We can construct our solution -- brute force attack. Recall that for pseudo-random generators, a fixed seed will always generate the same sequence. ~~(Well, maybe except when there are hardware failure or some strange cosmic rays hitting on the ram?)~~ We can brute force the seed of the random generator until we get the flag. Whether it works or not, it is always good to give it a try. Since the problem uses C#, it is good for us to also use C# to solve the challenge since the behaviors of the random number generators may not be the same for different languages. I am too lazy to test it out or look at the documentations ~~(Bye [friend](https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset.tounixtimeseconds?view=net-8.0))~~. First we have to turn the hex string back to a byte array. I am again lazy to do that and look at the types, syntax, other stuffs. I quickly come up with [this link](https://stackoverflow.com/questions/321370/how-can-i-convert-a-hex-string-to-a-byte-array) and gives me the code to do what I want. ```csharp= public static byte[] StringToByteArrayFastest(string hex) { if (hex.Length % 2 == 1) throw new Exception("The binary key cannot have an odd number of digits"); byte[] arr = new byte[hex.Length >> 1]; for (int i = 0; i < hex.Length >> 1; ++i) { arr[i] = (byte)((GetHexVal(hex[i << 1]) << 4) + (GetHexVal(hex[(i << 1) + 1]))); } return arr; } public static int GetHexVal(char hex) { int val = (int)hex; //For uppercase A-F letters: //return val - (val < 58 ? 48 : 55); //For lowercase a-f letters: //return val - (val < 58 ? 48 : 87); //Or the two combined, but a bit slower: return val - (val < 58 ? 48 : (val < 97 ? 55 : 87)); } ``` Borrowing some code online and from the challenge, we can finally come up with a brute force attack code: ```csharp= using System; public class Solve { public static byte[] StringToByteArrayFastest(string hex) { if (hex.Length % 2 == 1) throw new Exception("The binary key cannot have an odd number of digits"); byte[] arr = new byte[hex.Length >> 1]; for (int i = 0; i < hex.Length >> 1; ++i) { arr[i] = (byte)((GetHexVal(hex[i << 1]) << 4) + (GetHexVal(hex[(i << 1) + 1]))); } return arr; } public static int GetHexVal(char hex) { int val = (int)hex; //For uppercase A-F letters: //return val - (val < 58 ? 48 : 55); //For lowercase a-f letters: //return val - (val < 58 ? 48 : 87); //Or the two combined, but a bit slower: return val - (val < 58 ? 48 : (val < 97 ? 55 : 87)); } public static void Main(string[] args) { var ciphertext = StringToByteArrayFastest("22ECCDB90936D5C2454A65A5BB4C120FB1C8567381C6DB368EB57D4C6BE8B6D8C860E5C6FAC1F48BF2291A5C9EA3C354715857E7"); // u can find code for these kind of functions online if too lazy to write var initial_seed = new DateTimeOffset(DateTime.Today).ToUnixTimeSeconds(); for (var seed = initial_seed; seed > initial_seed - 8640000; seed--) { // Brute force 100 days from now var random = new Random((int)seed); var randomBuffer = new byte[ciphertext.Length]; random.NextBytes(randomBuffer); var resultBuffer = new byte[ciphertext.Length]; for (var i = 0; i < ciphertext.Length; i++) resultBuffer[i] = (byte)(randomBuffer[i] ^ ciphertext[i]); var plaintext = System.Text.Encoding.UTF8.GetString(resultBuffer); if (plaintext.Contains("1753c{")){ Console.WriteLine(plaintext.ToCharArray()); break; } } } } // Output: 1753c{you_will_never_guess_the_flag_coz_i_am_xorrro} ``` ## Assumptions Yes we got the flag. But for completeness, here are some assumptions behind the solution: * The challenge is attempted very close to the preparation date of the challenge. * That's why we choose to brute force 100 days. If the challenge is prepared 10 years ago, then it would take much longer time to run. * The problem author(s) did not mess with the system time when preparing this challenge. Well after solving the challenge, I tried brute forcing 1 year from now and test the runtime of the solution (with the break removed of course). The time it took is around 100 seconds (slightly less than that, and including compilation time). So even if the challenge is prepared 10 years ago, we can solve the challenge in 20 minutes. You can even do the dynamic memory allocations yourself and make the code runs faster (`unsafe` blocks in C#). But I am not familiar with C# and so I did not do anything about it. (And the challenge is not prepared 10 years ago!) ## Fun Facts When solving this challenge, the most time consuming part for me is to set up C# environments and check the syntax of C#. This challenge itself should be easy even to one without much Crypto background. Also, by looking at the official writeup, it looks like we can only brute force the seed for 00:00 everyday since that time function is returning the time of that day, but not that second. So even if the challenge is created thousands of years ago, it is still easily solvable.