首页 > AI前沿 > HellGates, custom CPU gate-level challenge

HellGates, custom CPU gate-level challenge

Hacker News 2026-09-20 05:44 8 阅读 查看原文
Introduction# In the Summer of 2025 I published a crackme called HellGates. Then I published it in November on crackmes.one. Basically I designed a custom 32-bit CPU encrypted & bit addressable (not byte addressable) in VHDL, synthesized it down to a gate-level netlist with multiple layers of obfuscation, anti-tamper, timing checks & anti-debug. It was designed for humans, but apparently worked well against LLMs too, until now. For a year, nobody solved it. Not the humans, they spent weeks/months on it and gave up. Not the LLMs, Claude failed, ChatGPT failed, and DeepSeek failed after days or weeks of work guided by hints I gave to people, ending with the model declaring the challenge “computationally infeasible with available resources.” Then, in September 2026, GPT-6 solved it in under 20-30 minutes on SRE-Bench. Thank you Zhuo Zhang for making this possible ! The short version: the crypto protecting the CPU’s state was lazily done and was very weak; there was a side-channel/differential analysis attack used by GPT-6, which made the runs replayable very easily after all obfuscation layers were removed. It gave hints on the encryption used, decrypted registers, re-encrypted registers, ran the netlist properly and dumped all the decrypted 1GB (data.bin) memory. The details of the challenge are here: The virtual CPU architecture. The program I used in the virtual CPU. The obfuscations on host side I used. The toolchains. CPU architecture# General information:# The architecture contains 16 32-bit general purpose registers, and some special registers: type special_registers is record overflow_flag : boolean; condition_flag : boolean; program_counter : cpu_address_type; key_modifiers : special_key_modifiers_type; index_for_key_modifier : cpu_word_index_type; tea_pseudo_random_state : tea_integer_type; end record special_registers; type registers_record is record general : register_array; special : special_registers; end record registers_record; overflow_flag is when any type of integer overflow happened, or division by zero. The operations were all signed integer operations and contained your basic ALU operations. (add, subtract, multiply …) condition_flag is used for branching between different locations. Example: IsEqual R2, 0 // condition_flag=1 Branch @loc ... instructions when condition_flag=0 ... loc: ... instructions when condition_flag=1 ... Now you may ask: what are key_modifiers/index_for_key_modifier ? I will explain the memory architecture a bit later below. Now all opcodes: -- Integer operations -- constant opcode_type_or : opcode_type := "00001"; constant opcode_type_and : opcode_type := "00010"; constant opcode_type_not : opcode_type := "00011"; constant opcode_type_add : opcode_type := "00100"; constant opcode_type_substract : opcode_type := "00101"; constant opcode_type_division : opcode_type := "00110"; constant opcode_type_multiply : opcode_type := "00111"; constant opcode_type_sla : opcode_type := "01000"; constant opcode_type_sra : opcode_type := "01001"; constant opcode_type_sll : opcode_type := "01010"; constant opcode_type_srl : opcode_type := "01011"; constant opcode_type_rol : opcode_type := "01100"; constant opcode_type_ror : opcode_type := "01101"; -- Memory operations -- constant opcode_type_read : opcode_type := "01110"; constant opcode_type_write : opcode_type := "01111"; -- Branch operations -- constant opcode_type_is_bigger : opcode_type := "10000"; constant opcode_type_is_lower : opcode_type := "10001"; constant opcode_type_is_equal : opcode_type := "10010"; constant opcode_type_had_integer_overflow : opcode_type := "10011"; -- Jumping, branches, set -- constant opcode_type_jump : opcode_type := "10100"; constant opcode_type_branch : opcode_type := "10101"; constant opcode_type_set : opcode_type := "10110"; -- Expanding instructions -- constant opcode_type_xor : opcode_type := "10111"; Okay not much obfuscation here, I could have used chained instruction encryption, shuffling the opcodes and a microcode engine, but I thought it was overkill at that time. (which it was, but now, not sure) The memory architecture:# When you are doing a CPU that runs on encrypted memory, you need to be careful, you can’t simply just fetch bits from memory at any bit address, otherwise you’ll just decrypt/encrypt garbage when you try to read/write an integer/instruction, so your CPU becomes useless. This is where it differs from non-encrypted memory because you can read/write directly at any bit address (in theory, most popular CPUs don’t do that of course). It needs read/write memory in an aligned address. A word in my context is the number of bits (and only that number) that the CPU can read/write into a specific slot index in RAM. On my architecture the word size is 64 bits, which is, as you guessed, the block size of the encryption method I’ve used. However this is harder to write the code into compared to plaintext memory, because you can’t simply read a word at a specific index and decode an instruction. Except if you’ve made your instruction size, integer size, word size all the same size and ALSO make the code impossible to jump to a specific bit address, but only to an address aligned to the word size, by design (the same applies for reading/writing integer to memory). For example 0x10000 would be fine, but what if you try to read an instruction at 0x10001 ? Your instruction is split in two parts (C++ example, most people are probably more familiar with this): // Instruction is between 0x10001 and 0x10041 // Let's imagine the first word is at 0x10000, second at 0x10040 // Okay let's start to read until 0x10040 static constexpr uint64_t WORD_SIZE_IN_BITS = 64; uint64_t read_bits = 0; uint64_t wanted_address = 0x10001; uint64_t bit_offset = wanted_address - (wanted_address % WORD_SIZE_IN_BITS); uint64_t word_index = (wanted_address - bit_offset) / WORD_SIZE_IN_BITS; // 0x400 bit_array_t word_bits = read_word_bits(word_index); instruction_bit_array_t instruction; for (uint64_t i = bit_offset; i < WORD_SIZE_IN_BITS; i++) { if (read_bits < sizeof(instruction)) { instruction[read_bits] = word_bits[i]; read_bits++; } } // read_bits is now 63, not 64 ! // So we miss one bit. We need to read the next word to get it bit_offset = 0; word_index++; word_bits = read_word_bits(word_index); for (uint64_t i = bit_offset; i < WORD_SIZE_IN_BITS; i++) { if (read_bits < sizeof(instruction)) { instruction[read_bits] = word_bits[i]; read_bits++; } } // Now the instruction is being completely fetched ! Safe to read. Now the VHDL code is a bit more complex than this, but you have to remember that the instructions/operations on integers are being split into words, and so we need multiple word indexes. Memory encryption:# Alright, now let’s talk about memory encryption. The algorithm I used to encrypt was TEA. It was very easy to implement from the C reference, but generates a lot of logic gates because of the number of operations and rounds. To be simple, there are 2 layers of encryption. One is for encrypting the words, where each word has its own set of keys which are dynamically generated based on CPU state (this is what key modifiers are about). Second, the word keys generated are themselves encrypted (KEK) so it couldn’t be retrieved by simply reading memory. The KEK for each word was also dynamically generated (deterministic though), based on a nonce between word_index and a static key, so if a wrong index is picked to decrypt a specific word, it will output garbage. This was also used to defend against side-channel attacks, so a word would surely never be encrypted the same way. Maybe overkill, but that’s what I did. On top of that, all word indexes were permuted to look random access on memory, so word_index at 0, would be something random like 0x2281FD. This is why there is a 1GB data.bin: the virtual CPU’s program is scattered all around the 1GB of data, mixed with entropy data! This was a nice trick to obfuscate the program, it was not a simple “static key” to find in the netlist. Unfortunately, GPT-6 didn’t even need to see this to decrypt memory. The key modifiers are basically just randomly encrypted generated keys. Their indexes are also permuted, hence why ‘index’ for key modifiers. They output to different memory. The weakness (the side-channel):# Here a sample of the code how the CPU encrypted its state (cpu_integer_type is just 32-bit signed integer): type fake_array is array(2 downto 0) of cpu_integer_type; variable fake_values : fake_array := (others => (others => '0')); for i in internal_registers.general'range loop internal_registers.general(i) := internal_registers.general(i) + fake_values(i mod fake_values'length); internal_registers.general(i) := internal_registers.general(i) xor (cpu_integer_type(internal_registers.special.tea_pseudo_random_state) + i + 1); end loop; fake_values: for i in fake_values'range loop fake_values(i) := fake_values(i) + cpu_integer_type(internal_registers.general(i mod internal_registers.general'length)) + cpu_integer_type(internal_registers.special.tea_pseudo_random_state + i + 1); end loop; As you can see it was clearly weak crypto. I actually left this on purpose because if I used real cryptography, the logic gates count would have increased to a larger number and I thought nobody would see the side-channel anyway and so moved on because I needed to test my design rapidly after synthesis. I was wrong, this mistake cost me a lot: it made GPT-6 partially solve it by just calling the netlist with the proper CPU state to decrypt memory. Even though it was probably a difficult differential analysis for a human, GPT-6 did it and predicted exactly what needed to be done to discover it. And you can do it too now ! The program# I’ve made my own assembly language in ANTLR, and a compiler for it. This is the current CTF program, written in hgasm (HellGatesAssembly): #define DMA_ADDRESS_CHARACTER 0x80001000 #define DMA_ADDRESS_RECEIVED_CHARACTER 0x80001008 #define DMA_ADDRESS_LCD_CLEAR 0x80001FF0 #define DMA_ADDRESS_LCD 0x80002000 #define DMA_ADDRESS_MARK_FOR_DEBUGGER 0x82000014 #define DMA_ADDRESS_ANTITAMPER 0x82000127 #define DMA_ADDRESS_TIME 0x81000000 #define DMA_ANTI_TAMPER_DEBUG_BITS 0xA0000000 #define LCD_MAX_X 128 #define LCD_MAX_Y 32 #define LCD_MAX_PACK_4_CHARACTERS 1024 #define PASSWORD_LENGTH 100 #define MAX_CHARACTER_COUNT_FOR_ASK_PASSWORD 120 #define CPU_SHELLCODE_HASH 0xa7e2fb5c boot@0x00000000: Jump @code_start data@§: lcd_square_init: "--------------------------------------------------------------------------------------------------------------------------------" "| . .. . =- .::::-. .--*- |" "| +* *+ ..:. .%: .@= -=-:. *.#..-*:. ..:. .... |" "| -%=---#+.-+-:=- -# :# -%. -++- -==#+.#: -#=:-= =+--- |" "| =#.-@#-.. -%: :#: .*=...-@* -#=-=%:.#- :#=-:: ---*+ |" "| . =: -- :..-: ..::...::. --==--....::.:: .:.. ::-:...:::. .. |" "| :::::...::.::::....:::..:::::...:..::::::.::.::.::::.:::::::::-::::. |" "| .. .+#%@@@@%#=. .. |" "| ..::::... ..:....... :++#@@#+++=. ...::--:..::::.::... |" "| ....::--:..-=-..:.. ... #=.x.-=-- X .-+ ... .....:==-::-=--:::... |" "| ::.:::.:.:-=: .:. .:-::..=+==#*--+*=-=-..:::.....:: :--:.===-.::...... |" "| .:...:---:..-=- :. ..:::. . -=-=-- . .:::. :: -=-.:=---. .:.. |" "| ::..-==-=-..:::... :::. :-:---::-: .::: ....::..-::.. :--:. |" "| ....:.:--:. .::-: ::-: :.=*+=. :::.. :-::: .::...:-::.... |" "| .....:...:==- .... .:::- -::.. .:.. ...::. .:.:::. |" "| .....:-::--==: .:: .:-:-----S3nd-l3tt3r-t0-h3ll----:|:::. ... :-==-:::... .. |" "| .::::...::.::::. :--: ..::- --::. .--: .:::..:::....... |" "| ..:::::..:-==-:-: .:-. .::-- -:.:: .-:. ....::.:..:..... |" "| .:..:...:--:::... :--. .::-- ::::: .-.. ::--+-..::--:.. . |" "| ...............:::::.. :--. ::::- |---| --::: :=:: ---=::::::::. .. |" "| ..... . .....:::====- :--. .:::- | X | ----: .--: ::::::::.... . .... |" "| ... ......::.:--:===-: .::. ::::- |---| -:::: .--: ...::..... ..::.... |" "| ........:::.::==-..::::-: ::. ...:: -:::. :-:. .:--...::::..:.......... |" "| .. . ..::.::..:-:..::.-=-... .:::: ::::. .:.:===::..:...:...:..::::... |" "| ........:-::.:::-:::::: .-=::=: ..::: ::... ..:=--:. ..::-=-:.. .::::..:... |" "| ......:::::::.:...::====- .....::. ...: :.... ::--::--:..:-: ... . ..:::.... |" "| .. ........:.:---:-:..:=--=-: ....:..:..: ....::.::.::::..::::::.....-:.:..:=-:::---:......:...::. ..... ... |" "| .. ... .:::-::::-::. .:::-:.....:::::::::-====+++++===+++=----::::...-:..:::. .:--:.. ....... .. .... |" "| ...... ...::...:::::::::-=====+=====++++##+=++++===++==------=----::..:::........:::.:::. ..... .. |" "| ... .:::...:::----:::::---=-==+==++-+=+++++*++=++++*+==++=--==+==+=+*++=======--:....::. .:. |" "| |" "--------------------------------------------------------------------------------------------------------------------------------" lcd_square_init_2: "Please wait for LCD message ...................................................................................................." "| . .. . =- .::::-. .--*- |" "| +* *+ ..:. .%: .@= -=-:. *.#..-*:. ..:. .... |" "| -%=---#+.-+-:=- -# :# -%. -++- -==#+.#: -#=:-= =+--- |" "| =#.-@#-.. -%: :#: .*=...-@* -#=-=%:.#- :#=-:: ---*+ |" "| . =: -- :..-: ..::...::. --==--....::.:: .:.. ::-:...:::. .. |" "| :::::...::.::::....:::..:::::...:..::::::.::.::.::::.:::::::::-::::. |" "| .. .+#%@@@@%#=. .. |" "| ..::::... ..:....... :++#@@#+++=. ...::--:..::::.::... |" "| ....::--:..-=-..:.. ... #=.o.-=-- O .-+ ... .....:==-::-=--:::... |" "| ::.:::.:.:-=: .:. .:-::..=+==#*--+*=-=-..:::.....:: :--:.===-.::...... |" "| .:...:---:..-=- :. ..:::. . -=-=-- . .:::. :: -=-.:=---. .:.. |" "| ::..-==-=-..:::... :::. :-:---::-: .::: ....::..-::.. :--:. |" "| ....:.:--:. .::-: ::-: :.=*+=. :::.. :-::: .::...:-::.... |" "| .....:...:==- .... .:::- -::.. .:.. ...::. .:.:::. |" "| .....:-::--==: .:: .:-:- Wow. Congratulations ! :|:::. ... :-==-:::... .. |" "| .::::...::.::::. :--: ..::- https://www.youtube.com/ --::. .--: .:::..:::....... |" "| ..:::::..:-==-:-: .:-. .::-- watch?v=Un4p-6lzIpI -:.:: .-:. ....::.:..:..... |" "| .:..:...:--:::... :--. .::-- ::::: .-.. ::--+-..::--:.. . |" "| ...............:::::.. :--. ::::- You can now love yourself. --::: :=:: ---=::::::::. .. |" "| ..... . .....:::====- :--. .:::- I wonder how much time, ----: .--: ::::::::.... . .... |" "| ... ......::.:--:===-: .::. ::::- you wasted on this. -:::: .--: ...::..... ..::.... |" "| ........:::.::==-..::::-: ::. ...:: But I took pleasure, -:::. :-:. .:--...::::..:.......... |" "| .. . ..::.::..:-:..::.-=-... .:::: from your agony. ::::. .:.:===::..:...:...:..::::... |" "| ........:-::.:::-:::::: .-=::=: ..::: Thank you for staying. ::... ..:=--:. ..::-=-:.. .::::..:... |" "| ......:::::::.:...::====- .....::. ...: :.... ::--::--:..:-: ... . ..:::.... |" "| .. ........:.:---:-:..:=--=-: ....:..:..: ....::.::.::::..::::::.....-:.:..:=-:::---:......:...::. ..... ... |" "| .. ... .:::-::::-::. .:::-:.....:::::::::-====+++++===+++=----::::...-:..:::. .:--:.. ....... .. .... |" "| ...... ...::...:::::::::-=====+=====++++##+=++++===++==------=----::..:::........:::.:::. ..... .. |" "| ... .:::...:::----:::::---=-==+==++-+=+++++*++=++++*+==++=--==+==+=+*++=======--:....::. .:. |" "| |" "--------------------------------------------------------------------------------------------------------------------------------" lcd_square_init_3: "Please wait for LCD message ...................................................................................................." "| . .. . =- .::::-. .--*- |" "| +* *+ ..:. .%: .@= -=-:. *.#..-*:. ..:. .... |" "| -%=---#+.-+-:=- -# :# -%. -++- -==#+.#: -#=:-= =+--- |" "| =#.-@#-.. -%: :#: .*=...-@* -#=-=%:.#- :#=-:: ---*+ |" "| . =: -- :..-: ..::...::. --==--....::.:: .:.. ::-:...:::. .. |" "| :::::...::.::::....:::..:::::...:..::::::.::.::.::::.:::::::::-::::. |" "| .. .+#%@@@@%#=. .. |" "| ..::::... ..:....... :++#@@#+++=. ...::--:..::::.::... |" "| ....::--:..-=-..:.. ... #=.x.-=-- X .-+ ... .....:==-::-=--:::... |" "| ::.:::.:.:-=: .:. .:-::..=+==#*--+*=-=-..:::.....:: :--:.===-.::...... |" "| .:...:---:..-=- :. ..:::. . -=-=-- . .:::. :: -=-.:=---. .:.. |" "| ::..-==-=-..:::... :::. :-:---::-: .::: ....::..-::.. :--:. |" "| ....:.:--:. .::-: ::-: :.=*+=. :::.. :-::: .::...:-::.... |" "| .....:...:==- .... .:::- -::.. .:.. ...::. .:.:::. |" "| .....:-::--==: .:: .:-:- Congratulations !!! :|:::. ... :-==-:::... .. |" "| .::::...::.::::. :--: ..::- --::. .--: .:::..:::....... |" "| ..:::::..:-==-:-: .:-. .::-- You suck. -:.:: .-:. ....::.:..:..... |" "| .:..:...:--:::... :--. .::-- ::::: .-.. ::--+-..::--:.. . |" "| ...............:::::.. :--. ::::- Do it the real way. --::: :=:: ---=::::::::. .. |" "| ..... . .....:::====- :--. .:::- Like a real man. ----: .--: ::::::::.... . .... |" "| ... ......::.:--:===-: .::. ::::- Try again bruteforcing, -:::: .--: ...::..... ..::.... |" "| ........:::.::==-..::::-: ::. ...:: and there will be ... -:::. :-:. .:--...::::..:.......... |" "| .. . ..::.::..:-:..::.-=-... .:::: Unforeseen Consequences. ::::. .:.:===::..:...:...:..::::... |" "| ........:-::.:::-:::::: .-=::=: ..::: YOU'VE BEEN WARNED. ::... ..:=--:. ..::-=-:.. .::::..:... |" "| ......:::::::.:...::====- .....::. ...: :.... ::--::--:..:-: ... . ..:::.... |" "| .. ........:.:---:-:..:=--=-: ....:..:..: ....::.::.::::..::::::.....-:.:..:=-:::---:......:...::. ..... ... |" "| .. ... .:::-::::-::. .:::-:.....:::::::::-====+++++===+++=----::::...-:..:::. .:--:.. ....... .. .... |" "| ...... ...::...:::::::::-=====+=====++++##+=++++===++==------=----::..:::........:::.:::. ..... .. |" "| ... .:::...:::----:::::---=-==+==++-+=+++++*++=++++*+==++=--==+==+=+*++=======--:....::. .:. |" "| |" "--------------------------------------------------------------------------------------------------------------------------------" animations_right_eye: "0 .-" "o .-" "= .-" "- .-" " .-" "- .-" "= .-" "o .-" "O .-" animations_left_eye: "o.-=" "=.-=" "-.-=" " .-=" " .-=" " .-=" " .-=" " .-=" "o.-=" animation_index: 0x00000000 last_animate_time: 0x00000000 last_dma_time: 0xFFFFFFFF had_timeouted: 0x00000000 had_debugger_on: 0x00000000 had_mismatched_hash: 0x00000000 fake_smc_index: 0x00000000 anti_tamper_triggered: 0x00000000 fake_smc_jump_back: 0x00000000 count_characters: 0x00000000 data_end: 0x00 code_start@§: Set R7, 1 Write R7, @DMA_ADDRESS_MARK_FOR_DEBUGGER // Set R7, 0 // Write R7, @DMA_ANTI_TAMPER_DEBUG_BITS Set R0, @DMA_ADDRESS_LCD_CLEAR Set R1, 1 Write R1, R0 Jump @init_lcd init_lcd: Set R0, @DMA_ADDRESS_LCD Set R1, @lcd_square_init Set R2, 0 loop_init_lcd: Add R2, 1 Read R3, R1 Write R3, R0 Add R0, 32 Add R1, 32 loop_init_lcd_tmp_label: IsLower R2, @LCD_MAX_PACK_4_CHARACTERS Branch @loop_init_lcd Jump @ask_password // Fake SMC fake_smc: // Let's rewrite a word to confuse people with fake SMC with encryption Read R7, @fake_smc_index Read R6, R7 // Insert a random value for RAM encryption Read R5, @DMA_ADDRESS_TIME Write R6, R7 IsBigger R7, @code_end Branch @reset_fake_smc_index Add R7, 64 Jump @write_fake_smc_index reset_fake_smc_index: Set R7, 0 write_fake_smc_index: Write R7, @fake_smc_index Read R7, @fake_smc_jump_back Jump R7 check_anti_tamper: // Jump to fake SMC Set R7, @continue_debugger_check Write R7, @fake_smc_jump_back Jump @fake_smc continue_debugger_check: Read R7, @had_debugger_on // Read R6, @DMA_ANTI_TAMPER_DEBUG_BITS // SLL R7, 0 // Or R7, R6 // Write R7, @DMA_ANTI_TAMPER_DEBUG_BITS IsBigger R7, 0 Branch @anti_tamper_check_not_passed // Debugger check, override old value to be sure it's not a simply nop anyway. Set R7, 1 Write R7, @DMA_ADDRESS_MARK_FOR_DEBUGGER Read R7, @DMA_ADDRESS_MARK_FOR_DEBUGGER IsEqual R7, 0 Branch @check_hash Set R7, 1 Write R7, @had_debugger_on // Else ask a new password and continue like nothing happened Jump @anti_tamper_check_not_passed check_hash: Read R7, @had_mismatched_hash // Read R6, @DMA_ANTI_TAMPER_DEBUG_BITS // SLL R7, 1 // Or R7, R6 // Write R7, @DMA_ANTI_TAMPER_DEBUG_BITS IsBigger R7, 0 Branch @anti_tamper_check_not_passed // Verify CPU netlist code, override hash so that it forces the user to write it again. Set R7, 0 Write R7, @DMA_ADDRESS_ANTITAMPER Read R7, @DMA_ADDRESS_ANTITAMPER IsEqual R7, @CPU_SHELLCODE_HASH Branch @check_timeout Set R7, 1 Write R7, @had_mismatched_hash Jump @anti_tamper_check_not_passed check_timeout: Read R7, @had_timeouted // Read R6, @DMA_ANTI_TAMPER_DEBUG_BITS // SLL R7, 2 // Or R7, R6 // Write R7, @DMA_ANTI_TAMPER_DEBUG_BITS IsBigger R7, 0 Branch @anti_tamper_check_not_passed Read R7, @DMA_ADDRESS_TIME Read R5, @last_dma_time Write R7, @last_dma_time IsEqual R5, 0xFFFFFFFF // For the first loop, don't check it. Branch @anti_tamper_check_passed Subtract R7, R5 IsBigger R7, 1 Branch @check_higher_time Jump @weird_constant_time check_higher_time: IsLower R7, 10000000 // If it's 10 seconds that we stopped, it's probably a debugger, don't continue ever here. Branch @anti_tamper_check_passed weird_constant_time: Set R7, 1 Write R7, @had_timeouted Jump @anti_tamper_check_not_passed anti_tamper_check_not_passed: Set R7, 1 Write R7, @anti_tamper_triggered Jump @anti_tamper_check_passed ask_password: Set R4, 0 // R4 is pw index ask_letter_and_do_animation: IsLower R4, 0 // Check if negative Branch @ask_password // Do animation, let's see if we can animate Read R6, @last_animate_time Read R7, @DMA_ADDRESS_TIME Subtract R7, R6 IsLower R7, 400000 Branch @wait_for_letter Read R6, @DMA_ADDRESS_TIME Write R6, @last_animate_time Read R7, @animation_index Add R7, 1 IsBigger R7, 8 Branch @set_zero_animation_index Jump @write_animation_index set_zero_animation_index: Set R7, 0 write_animation_index: Write R7, @animation_index Set R6, @animations_right_eye Multiply R7, 32 Add R6, R7 Read R7, R6 Set R6, 1218 // Set cursor to the right skull eye Multiply R6, 8 Add R6, @DMA_ADDRESS_LCD Write R7, R6 Read R7, @animation_index Set R6, @animations_left_eye Multiply R7, 32 Add R6, R7 Read R7, R6 Set R6, 1211 // Set cursor to the left skull eye Multiply R6, 8 Add R6, @DMA_ADDRESS_LCD Write R7, R6 wait_for_letter: // Check first for debugger etc. Jump @check_anti_tamper // Check the actual character now anti_tamper_check_passed: Read R1, @DMA_ADDRESS_RECEIVED_CHARACTER IsEqual R1, 1 Branch @ask_letter_and_do_animation Read R0, @DMA_ADDRESS_CHARACTER And R0, 0x000000FF Set R3, R0 // Save letter Set R6, 0x00000024 Set R7, 1 Add R7, R4 Multiply R6, R7 And R6, 0x000000FF XOR R3, R6 // XOR password, at least give a chance to the challenger Set R1, 1 Write R1, @DMA_ADDRESS_RECEIVED_CHARACTER Set R1, @DMA_ADDRESS_LCD Set R2, 2622 // Set cursor pos to the square X Multiply R2, 8 Add R1, R2 Add R0, 0x207C2000 Write R0, R1 // Write the LCD Set R5, @password Set R6, R4 // Get current password character index Multiply R6, 8 // Get the bit position Add R5, R6 // Add the bit position Read R6, R5 // We care only about the 8 bits character And R6, 0x000000FF Jump @check_character check_character: // Bruteforcing needs a special case where I need to insult the person who does this. // I don't know why, it's stronger than me. (joking lmao) Read R7, @count_characters Add R7, 1 Write R7, @count_characters IsBigger R7, @MAX_CHARACTER_COUNT_FOR_ASK_PASSWORD Branch @you_suck // If anti tamper is triggered, do not check password. Read R7, @anti_tamper_triggered IsBigger R7, 0 Branch @ask_password // Check if character is correct XOR R3, R6 // If the values are the same, it will be zero Subtract R4, R3 // Substract zero if correct Add R4, 1 // Increment index IsLower R4, @PASSWORD_LENGTH Branch @ask_letter_and_do_animation Jump @good_password // Send congratulations good_password: Set R0, @DMA_ADDRESS_LCD_CLEAR Set R1, 1 Write R1, R0 init_lcd_2: Set R0, @DMA_ADDRESS_LCD Set R1, @lcd_square_init_2 Set R2, 0 loop_init_lcd_2: Add R2, 1 Read R3, R1 Write R3, R0 Add R0, 32 Add R1, 32 IsLower R2, @LCD_MAX_PACK_4_CHARACTERS Branch @loop_init_lcd_2 Jump @init_lcd_2 you_suck: Set R0, @DMA_ADDRESS_LCD_CLEAR Set R1, 1 Write R1, R0 init_lcd_3: Set R0, @DMA_ADDRESS_LCD Set R1, @lcd_square_init_3 Set R2, 0 loop_init_lcd_3: Add R2, 1 Read R3, R1 Write R3, R0 Add R0, 32 Add R1, 32 IsLower R2, @LCD_MAX_PACK_4_CHARACTERS Branch @loop_init_lcd_3 Jump @init_lcd_3 code_end: Jump @code_start Here is how the snapshot encrypted RAM created it with the compiled program: RAMCreator I decided not to give the entire code, but you have the architecture now. I’ve also removed the password in the assembly, but it should be easily retrievable in the RAMCreator, so you can solve it yourself ! :) To summarize, it uses: Fake SMC/FSMC = fake self-modifying code, fake because in fact, nothing in plaintext changes, but it forces the keys to rotate so memory appears to self-modify all the time. Anti-bruteforcing (You couldn’t type more than 120 characters) Anti-tamper watchdog on host (I will explain later how it was implemented host-side which was also a weakness but on purpose) on the netlist running. Anti-debug watchdog on host. Tries to avoid branching to avoid a side-channel attack (by checking where the CPU reads for the next instruction) and uses XOR operations instead. (urgh, I know this was bad) Has an easter egg if a side-channel was used, ironically was still solved anyway because of several weaknesses related to keyboard input and weak password check (GPT-6 used these weaknesses). A keygen would have been much better in this regard. An animated skull. (this was fun to write in assembly) You can notice DMA time, this one was used to measure how long the netlist would run, so for example if you ran it under an emulator, like qemu, the password check would fail instead of making the program just exit. It was also used to generate more entropy to generate new word keys, because the keys are dynamically generated and are dependent on CPU registers/state. The same anti-stuffs made the password check fail instead of exiting/breaking the host program. This is evil, I know. Obfuscation used on host side# In short, here’s what I used on the host side: Recursively encrypted (using variant of ChaCha20) nested shellcodes (called CXE, for calvin-xutaxkamay-executable, as this was also intended for a reverse-engineering hypervisor, hi Calvin and my friends if you see this! Thank you for supporting me all these years) that contains the scattered netlist inside the exception handler. I had to make my own tool to create my own shellcode generator, so that it properly self-relocates (it is position independent and shellcode can be written in C/C++, I will detail that in toolchains). Recursively encrypted (using variant of ChaCha20) nested shellcodes (called CXE, for calvin-xutaxkamay-executable, as this was also intended for a reverse-engineering hypervisor, hi Calvin and my friends if you see this! Thank you for supporting me all these years) that contains the scattered netlist inside the exception handler. I had to make my own tool to create my own shellcode generator, so that it properly self-relocates (it is position independent and shellcode can be written in C/C++, I will detail that in toolchains). A recursive runtime decryption/encryption exception handler. A recursive runtime decryption/encryption exception handler. Control-flow obfuscation but exception based on signal return using specific hardcoded addresses and some classic debug instructions int 3/.byte 0xF1 that drives to anti-debug/anti-tamper or the netlist itself. The CFO itself contained parts of the netlist. The fun part is that it breaks disassemblers and misinterprets some bytes, and is not easy to trace without dynamic analysis: Control-flow obfuscation but exception based on signal return using specific hardcoded addresses and some classic debug instructions int 3/.byte 0xF1 that drives to anti-debug/anti-tamper or the netlist itself. The CFO itself contained parts of the netlist. The fun part is that it breaks disassemblers and misinterprets some bytes, and is not easy to trace without dynamic analysis: Here is the complete flow of the cpp code, which will be easier than explaining with words, the code should be easy enough to read through (snippets, not full code): constexpr auto CPUShellCodeSize = sizeof(cpu_cxe_h_CXE_BINARY_BLOB); constexpr auto AntiDebugShellCodeSize = sizeof( antidebug_cxe_h_CXE_BINARY_BLOB); CXEHeader* CPUShellCodeCXEHeader = nullptr; bool InitializedShellCode = false; size_t LastDecryptedPageByteIndex = std::numeric_limits ::max(); extern "C" bool shellcode_entry( const inputs_central_processing_unit_t& inputs, outputs_central_processing_unit_t& outputs) { if (not __cxe_self_relocate()) { return false; } cpu(inputs, outputs); return true; } void __attribute__((noinline)) EraseInstructions(void* ptr) { auto bytes = reinterpret_cast (ptr); for (size_t i = 0; i < HellGates::PageSize; i++) { bytes[i] = 0x90; } } inline void CPUShellCodeAntiTamper(HellGates::RAM& RAM) { auto start_hash_from = reinterpret_cast ( CPUShellCodeCXEHeader) + CPUShellCodeCXEHeader->offset_to_shellcode + cpu_cxe_h_CXE_SYMBOL_OFFSETS [cpu_cxe_h_CXE___code_start]; constexpr auto hashed_size = cpu_cxe_h_CXE_SYMBOL_OFFSETS [cpu_cxe_h_CXE___code_end] - cpu_cxe_h_CXE_SYMBOL_OFFSETS [cpu_cxe_h_CXE___code_start]; auto hash = HellGates::HashArray(start_hash_from, hashed_size); auto HashedCPUShellCode = static_cast (hash) ^ static_cast (hash >> 32); RAM.Set(HellGates::DMA_ADDRESS_ANTITAMPER, std::bitset<32>(HashedCPUShellCode)); } extern "C" void __attribute__((aligned(4096))) InitCPU( decltype(mprotect) linux_mprotect, decltype(mmap) linux_mmap, HellGates::RAM& RAM) { auto data = reinterpret_cast (RAM.bits.get_data()); data = data - (data % HellGates::PageSize); linux_mprotect(reinterpret_cast (data), RAM.bits.data_size() * __SIZEOF_LONG__, PROT_READ | PROT_WRITE | PROT_EXEC); CPUShellCodeCXEHeader = reinterpret_cast< decltype(CPUShellCodeCXEHeader)>( data + HellGates::CPU_SHELLCODE_BYTE_ADDRESS); auto bytes = reinterpret_cast (CPUShellCodeCXEHeader); for (size_t i = 0; i < CPUShellCodeSize; i++) { bytes[i] = cpu_cxe_h_CXE_BINARY_BLOB[i]; } constexpr auto EncryptEverytimeForThisSize = 0x40000; for (size_t i = 0; i < CPUShellCodeSize; i += HellGates::PageSize) { if (i >= EncryptEverytimeForThisSize and i % EncryptEverytimeForThisSize == 0) { linux_mprotect(bytes + i, HellGates::PageSize, PROT_NONE); continue; } HellGates::ChaCha20::DecryptPageSize( &bytes[i], i, std::to_array(cpu_cxe_h_CXE_KEY)); } InitializedShellCode = true; CPUShellCodeAntiTamper(RAM); EraseInstructions(reinterpret_cast (InitCPU)); __asm__(".space 4096 - ( . - InitCPU ), 0x90\n"); } inline void RuntimeDecryption(decltype(mprotect) linux_mprotect, siginfo_t* info, int sig) { auto bytes = reinterpret_cast (CPUShellCodeCXEHeader); if (LastDecryptedPageByteIndex != std::numeric_limits ::max()) { auto bytes_to_encrypt = bytes + LastDecryptedPageByteIndex; auto bytes_to_copy = &cpu_cxe_h_CXE_BINARY_BLOB [LastDecryptedPageByteIndex]; for (size_t i = 0; i < HellGates::PageSize; i++) { bytes_to_encrypt[i] = bytes_to_copy[i]; } linux_mprotect(bytes_to_encrypt, HellGates::PageSize, PROT_NONE); LastDecryptedPageByteIndex = std::numeric_limits ::max(); } if (sig == SIGSEGV) { auto aligned_address = reinterpret_cast (info->si_addr) - (reinterpret_cast (info->si_addr) % HellGates::PageSize); if (aligned_address < reinterpret_cast (CPUShellCodeCXEHeader) or aligned_address >= (reinterpret_cast (CPUShellCodeCXEHeader) + CPUShellCodeSize)) { NanomitesErrors::Do( NanomitesErrors::NOT_WITHIN_SHELLCODE_SCOPE, reinterpret_cast (aligned_address)); return; } size_t page_index = aligned_address - bytes; LastDecryptedPageByteIndex = page_index; linux_mprotect(aligned_address, HellGates::PageSize, PROT_READ | PROT_EXEC | PROT_WRITE); HellGates::ChaCha20::DecryptPageSize( aligned_address, page_index, std::to_array(cpu_cxe_h_CXE_KEY)); } } inline void AntiDebug(decltype(mmap) linux_mmap, decltype(munmap) linux_munmap, HellGates::RAM& RAM, bool AntiTamper) { auto AntiDebugCXEHeader = reinterpret_cast ( linux_mmap(nullptr, AntiDebugShellCodeSize, PROT_EXEC | PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0)); auto bytes = reinterpret_cast (AntiDebugCXEHeader); for (size_t i = 0; i < AntiDebugShellCodeSize; i++) { bytes[i] = antidebug_cxe_h_CXE_BINARY_BLOB[i]; } for (size_t i = 0; i < AntiDebugShellCodeSize; i += HellGates::PageSize) { HellGates::ChaCha20::DecryptPageSize( &bytes[i], i, std::to_array(antidebug_cxe_h_CXE_KEY)); } auto AntiDebugShellCodeEntryFunction = reinterpret_cast< void (*)(HellGates::RAM&, CXEHeader*, size_t&, bool&, uint32_t&, bool)>( reinterpret_cast (AntiDebugCXEHeader) + AntiDebugCXEHeader->offset_to_shellcode + antidebug_cxe_h_CXE_SYMBOL_OFFSETS [antidebug_cxe_h_CXE_shellcode_entry]); static size_t CounterCPUSCHash = 0; static bool DebuggerDetected = false; static uint32_t HashedCPUShellCode = 0; AntiDebugShellCodeEntryFunction(RAM, CPUShellCodeCXEHeader, CounterCPUSCHash, DebuggerDetected, HashedCPUShellCode, AntiTamper); for (size_t i = 0; i < AntiDebugShellCodeSize; i++) { bytes[i] = 0x90; } linux_munmap(AntiDebugCXEHeader, AntiDebugShellCodeSize); } inline void CPUShellCode(const inputs_central_processing_unit_t& inputs, outputs_central_processing_unit_t& outputs, decltype(mprotect) linux_mprotect, decltype(mmap) linux_mmap, decltype(munmap) linux_munmap, decltype(mremap) linux_mremap, // decltype(printf) linux_printf, int sig, siginfo_t* info, void* ucontext, HellGates::RAM& RAM) { if (not InitializedShellCode) { InitCPU(linux_mprotect, linux_mmap, RAM); } auto CPUShellCodeEntryFunction = reinterpret_cast ( reinterpret_cast (CPUShellCodeCXEHeader) + CPUShellCodeCXEHeader->offset_to_shellcode + cpu_cxe_h_CXE_SYMBOL_OFFSETS[cpu_cxe_h_CXE_shellcode_entry]); CPUShellCodeEntryFunction(inputs, outputs); RuntimeDecryption(linux_mprotect, info, sig); } void RuntimeCPUNanomites(const inputs_central_processing_unit_t& inputs, outputs_central_processing_unit_t& outputs, decltype(mprotect) linux_mprotect, decltype(mmap) linux_mmap, decltype(munmap) linux_munmap, decltype(mremap) linux_mremap, int sig, siginfo_t* info, mcontext_t* mcontext, uint8_t* HellGatesCodeStart, size_t HellGatesCodeSize, HellGates::RAM& RAM) { auto addr = reinterpret_cast (info->si_addr); auto local_central_processing_unit = reinterpret_cast ( CPUShellCodeCXEHeader) + CPUShellCodeCXEHeader ->offset_to_shellcode + cpu_cxe_h_CXE_SYMBOL_OFFSETS [cpu_cxe_h_CXE_nanomites_inputs]; auto shared_central_processing_unit = reinterpret_cast ( CPUShellCodeCXEHeader) + CPUShellCodeCXEHeader ->offset_to_shellcode + cpu_cxe_h_CXE_SYMBOL_OFFSETS [cpu_cxe_h_CXE_shared_central_processing_unit]; bool should_increment_rip = true; // I will let you discover how many there is of those: switch (addr) { case 0xDEADC0DE: { local_central_processing_unit[0] = (shared_central_processing_unit [1144] or shared_central_processing_unit [457]); // ... *reinterpret_cast (0xFADE) = 0xB00B; asm volatile(".byte 0x00"); break; } case 0xFADE: { local_central_processing_unit[1841] = not( shared_central_processing_unit[1160] and shared_central_processing_unit[281]); asm volatile(".byte 0x00"); break; } case 0xEFFACED: { local_central_processing_unit[1867] = not( shared_central_processing_unit[1078] and shared_central_processing_unit[583]); ... break; } case 0xB00BFACE: { shared_central_processing_unit[1072] = true; break; } default: { should_increment_rip = false; RuntimeDecryption(linux_mprotect, info, sig); break; } } if (should_increment_rip) { auto byte = reinterpret_cast (mcontext->gregs[REG_RIP]); while (*reinterpret_cast (byte) != 0xB00B) { byte++; } mcontext->gregs[REG_RIP] = reinterpret_cast (byte) + 5; } } // Exception handler starts here extern "C" bool shellcode_entry( const inputs_central_processing_unit_t& inputs, outputs_central_processing_unit_t& outputs, decltype(mprotect) linux_mprotect, decltype(mmap) linux_mmap, decltype(munmap) linux_munmap, decltype(mremap) linux_mremap, // decltype(printf) linux_printf, int sig, siginfo_t* info, void* ucontext, uint8_t* HellGatesCodeStart, size_t HellGatesCodeSize, HellGates::RAM& RAM) { if (not __cxe_self_relocate()) { return false; } bool Int3Debugged = false; auto context = reinterpret_cast (ucontext); auto mcontext = &context->uc_mcontext; if (sig == SIGTRAP) { auto byte = *reinterpret_cast (mcontext->gregs[REG_RIP] - 1); if (byte == 0xF1) { CPUShellCode(inputs, outputs, linux_mprotect, linux_mmap, linux_munmap, linux_mremap, // linux_printf, sig, info, ucontext, RAM); } else if (byte == 0xCC) { Int3Debugged = true; } else { NanomitesErrors::Do(NanomitesErrors::UNKNOWN_TRAP); } } else if (sig == SIGSEGV) { RuntimeCPUNanomites(inputs, outputs, linux_mprotect, linux_mmap, linux_munmap, linux_mremap, sig, info, mcontext, HellGatesCodeStart, HellGatesCodeSize, RAM); } else { NanomitesErrors::Do(NanomitesErrors::UNKNOWN_SIGNAL); } AntiDebug(linux_mmap, linux_munmap, RAM, Int3Debugged); return true; } inline bool IsDebuggerPresent(HellGates::RAM& RAM) { constexpr auto xor_path = HellGates::X0RString("/proc/self/status"); char buffer[1024]; long fd; long bytes_read; auto path = xor_path.decrypt_no_compile_time(); asm volatile("mov $2, %%rax\n" "syscall\n" : "=a"(fd) : "D"(path.data()), "S"(0), "d"(0) : "rcx", "r11", "memory"); if (fd < 0) { return false; } asm volatile("mov $0, %%rax\n" "syscall\n" : "=a"(bytes_read) : "D"(fd), "S"(buffer), "d"(sizeof(buffer)) : "rcx", "r11", "memory"); asm volatile("mov $3, %%rax\n" "syscall\n" : : "D"(fd) : "rax", "rcx", "r11", "memory"); if (bytes_read <= 0) { return false; } constexpr auto xor_marker = HellGates::X0RString("TracerPid:"); constexpr size_t marker_len = xor_marker.SIZE - 1; const size_t bytes_available = static_cast (bytes_read); if (bytes_available < marker_len) { return false; } auto marker = xor_marker.decrypt_no_compile_time(); for (size_t i = 0; i <= bytes_available - marker_len; i++) { if (buffer[i] == 'T' && buffer[i + 9] == ':') { bool match = true; for (size_t j = 0; j < marker_len; j++) { if (buffer[i + j] != marker[j]) { match = false; break; } } if (match) { i += marker_len; while (i < bytes_available && (buffer[i] == ' ' || buffer[i] == '\t')) { i++; } long pid = 0; while (i < bytes_available && buffer[i] >= '0' && buffer[i] <= '9') { pid = pid * 10 + (buffer[i++] - '0'); } return pid != 0; } } } return false; } inline void CPUShellCodeAntiTamper(HellGates::RAM& RAM, CXEHeader* CPUShellCodeCXEHeader, uint32_t& HashedCPUShellCode) { auto start_hash_from = reinterpret_cast ( CPUShellCodeCXEHeader) + CPUShellCodeCXEHeader->offset_to_shellcode + cpu_cxe_h_CXE_SYMBOL_OFFSETS [cpu_cxe_h_CXE___code_start]; constexpr auto hashed_size = cpu_cxe_h_CXE_SYMBOL_OFFSETS [cpu_cxe_h_CXE___code_end] - cpu_cxe_h_CXE_SYMBOL_OFFSETS [cpu_cxe_h_CXE___code_start]; auto hash = HellGates::HashArray(start_hash_from, hashed_size); HashedCPUShellCode = static_cast (hash) ^ static_cast (hash >> 32); } extern "C" bool shellcode_entry(HellGates::RAM& RAM, CXEHeader* CPUShellCodeCXEHeader, size_t& CounterCPUSCHash, bool& DebuggerDetected, uint32_t& HashedCPUShellCode, bool AntiTamper) { if (not __cxe_self_relocate()) { return false; } bool debugger_present = IsDebuggerPresent(RAM); if (debugger_present) { DebuggerDetected = true; } RAM.Set(HellGates::DMA_ADDRESS_MARK_FOR_DEBUGGER, { DebuggerDetected }); if (AntiTamper) { if (CounterCPUSCHash == 0) { CPUShellCodeAntiTamper(RAM, CPUShellCodeCXEHeader, HashedCPUShellCode); static HellGates::SimpleRand simpleRand; CounterCPUSCHash = simpleRand.RandomInteger(64, 128); } else { CounterCPUSCHash--; } } RAM.Set(HellGates::DMA_ADDRESS_ANTITAMPER, std::bitset<32>(HashedCPUShellCode)); return true; } // CPU shellcode bool shared_central_processing_unit[1173] = {false,false,false,false,false,false,false,...}; bool nanomites_inputs[0x1000]; bool nanomites_outputs[0x1000]; void cpu(const inputs_central_processing_unit_t& inputs, outputs_central_processing_unit_t& outputs) { bool local_central_processing_unit[388716]; *reinterpret_cast (0xDEADC0DE) = 0xB00B; asm volatile(".byte 0x00"); // for(std::size_t i = 0; i < 1870;i++){ // local_central_processing_unit[i] = nanomites_inputs[i]; // } local_central_processing_unit[0] = nanomites_inputs[0]; local_central_processing_unit[1] = nanomites_inputs[1]; local_central_processing_unit[2] = nanomites_inputs[2]; local_central_processing_unit[3] = nanomites_inputs[3]; local_central_processing_unit[4] = nanomites_inputs[4]; local_central_processing_unit[5] = nanomites_inputs[5]; local_central_processing_unit[6] = nanomites_inputs[6]; .... huge code .... *reinterpret_cast (0xB00BFACE) = 0xB00B; asm volatile(".byte 0x00"); outputs.index_for_special_key_modifiers_43_ = local_central_processing_unit[251358]; outputs.index_for_special_key_modifiers_44_ = local_central_processing_unit[251915]; outputs.index_for_special_key_modifiers_45_ = local_central_processing_unit[250703]; outputs.index_for_special_key_modifiers_46_ = local_central_processing_unit[252032]; outputs.index_for_special_key_modifiers_47_ = local_central_processing_unit[250379]; outputs.index_for_special_key_modifiers_48_ = local_central_processing_unit[248991]; outputs.index_for_special_key_modifiers_49_ = local_central_processing_unit[250380]; outputs.index_for_special_key_modifiers_50_ = local_central_processing_unit[251698]; outputs.index_for_special_key_modifiers_51_ = local_central_processing_unit[251463]; shared_central_processing_unit[1169] = local_central_processing_unit[226965]; shared_central_processing_unit[1170] = local_central_processing_unit[225981]; shared_central_processing_unit[1171] = local_central_processing_unit[227727]; shared_central_processing_unit[1172] = local_central_processing_unit[227726]; } This should give you a strong view of the obfuscations used now. Toolchains# I’ve used yosys and GHDL to generate the netlist. I’ve also made my own tool called blif2cpp to try different generation designs (including performance ones in a private repository which I keep for myself) and contributed to yosys so that DFFs can be initialized. For CXE (shellcode generator), I’ve used also LLVM and especially ELFIO, a very cool library, which I’ve also contributed for auxiliary vectors support (which was needed for another injector I’ve made in Kokabiel). I had to make my own linker script (ld file) to discard regions I didn’t need. Then I converted the built ELF into an encrypted byte-array CXE so I could include generated code directly + symbols as enums, so strings related symbols are completely stripped. (as shown in the snippet) The nice thing about it is that the shellcodes can be used as a library ! (as shown in previous snippets) auto AntiDebugShellCodeEntryFunction = reinterpret_cast< void (*)(HellGates::RAM&, CXEHeader*, size_t&, bool&, uint32_t&, bool)>( reinterpret_cast (AntiDebugCXEHeader) + AntiDebugCXEHeader->offset_to_shellcode + antidebug_cxe_h_CXE_SYMBOL_OFFSETS [antidebug_cxe_h_CXE_shellcode_entry]); And voilà, here is a sample of a generated shellcode. Conclusion# The challenge had music I liked. You should have enough to get a password now ! I want to say that this was partially solved because a real solver would have recovered all algorithms and not just using side-channel attacks. The next challenge will be designed for LLMs and humans this time, I’ve already mostly finished designing another architecture while writing this. It solves already all side-channels I talked about and even do more to avoid this. On a note, yes a LLM solved this challenge, but honestly even if I was impressed how fast it solved it (20-30 mins), it used a side-channel. It was not a complete reversal of the sequential netlist. This changes things. It’s not that I dismiss the LLM for solving it this way, the less hard path is always the best, but if there is no practical side-channels, analyzing millions of logic gates would be much harder. On a smaller note, even if slower, I personally think that humans are way better at thinking process, this is what made our survival possible after all, we would not live without arts. Even art could be considered in a way, useless except one thing: thrive for living instead of surviving. Despite all the troubles I had with humans, I still believe in humanity. I hope you had a fun read and that it wasn’t too difficult to follow. If you have questions or improvements I could make on the blog post, send me mails or contact me through XMPP !