Итак, публикуя анонс задачи по взлому программы, работающей на однокомандном процессоре NORCPU, я наивно думал, что хотя бы до конца марта я получу какое-нибудь решение.
Замечание: все цитируемые ниже фрагменты приводятся как есть, с минимальными правками в плане форматирования на странице.
В реальности все было иначе. Буквально через пару часов после поста на Хабре пришло письмо от Vasiliy Artemev:
Secret code: 139471
Это было правильным ответом. Василий официально стал первым и получил законные 100$ призовых. Он любезно рассказал, что взломал методом грубой силы, перебором. Действительно, простейшая переборная атака заставляла программу выдать секретное слово уже на 3-х символьном пароле. Увы, моя реализация хеш-функции для пароля стала уязвимым звеном.
Но полного анализа не было, и к тому же Василий предложил спонсировать дальнейший анализ задачи в виде 50$ из начального призового фонда. В общем, анализ продолжался.
Тем временем я сделал новую версию задачи, где уже не было хеширования, а был просто зашифрованный пароль. Я думал, это усложнит взлом. Но вечером того же дня приходит письмо от Anton Bukov:
Ответ на NORCPU hackme challenge, Version 2: “R0und2 D0ne!” ?
Это правильный ответ. Я недоумевал. Как можно было так быстро разобраться?
Антон также любезно рассказал, как ему удалось получить ответ:
Мой взлом основан на строке:
mem = mem_0.slice(0);
Тут массив копируется, если дважды вызвать функцию calc на одном массиве, то для любого пароля будет отображен ответ. У меня в коде это выглядело так:
wcout << calc(L"0") << endl << calc(L"0") << endl;
Я так догадываюсь для предотвращения этого и осуществлялось копирование. Боюсь это не тот способ, которого вы ждали. Я и сам удивился, когда заметил ответ в output-е. Ну а после уже разобрался из-за чего он вылез.
Антон случайно нашел баг, из-за которого программа опять сама выдавала секрет. Надо было запустить программу повторно без приведения памяти эмулятора в исходное состояния.
Корень проблемы:
... IS_0(flag) JZ okay EXIT(1) okay: ; print the secret ...
После первого неправильного прогона и отказа в выводе секрета программа останавливалась в строке 4, и регистр комманд ip
уже указывал на метку okay
. Поэтому если интерпретатор просто запустить еще раз без инициализации памяти (а все регистры, включая ip
находятся в памяти), он продолжит выполнение с метки okay
и выведет секрет.
Досадно. Я быстро исправил проблему и выложил версию 2.1, в котором этого эффекта уже не было.
Прошло пару дней.
И вот приходит письмо от пользователя a5b с Хабра:
Хотпатчинг переходов и ответ pw:
abcd
resp:R0und2 D0ne!
- пароль естественно неверный
- инвертировал записываемые данные в
(i == 27692 || i == 31712)
Формально, это решение, но нет пароля, а значит полного анализа тоже нет.
Но через час от a5b приходит довесок:
h1cKmE1fUsAn input data: chr conI LEET xor CHR1 13417 13313 104 h CHR2 39953 39968 49 1 CHR3 54302 54397 99 c CHR4 32223 32148 75 K CHR5 30900 30937 109 m CHR6 27373 27304 69 E CHR7 16420 16405 49 1 CHR8 49210 49244 102 f CHR9 16740 16689 85 U CHR10 50115 50096 115 s CHR11 19308 19245 65 A CHR12 57802 57764 110 n static init: CHRi 59609= 59651 CONi 59610= 59634 CNTR 59611=12 SUM 59608=0 LEET 59607 = 13313 1: [59609]+1 -> [59609] // select next chr [59610]+1 -> [59610] // select next con SUM |= CHRi ^ LEET ^ CONi LEET = LEET * 3 + 29 [59611]: if([59611] != 0) Loop 1 ... if(SUM != 0) exit else print R0und2 D0ne // эту часть подробно не смотрел. // Судя по модификациям кода (продвижение индекса), загружает 12 констант: // 29528 22899 2971 9089 27542 17353 52278 25635 11626 34909 39131 51838, // над каждой колдует и пишет в вывод
А вот это уже анализ. a5b стал первым, кто прислал алгоритм задачи номер 2.
В тот же день вечером приходит письмо от Max Filippov:
Algorithm that was used to check password correctness in the first round was the following:
bool check(const char *p) { int v = 0x1040; for(; *p; ++p) { v ^= *p; for (int i = 0; i < 8; ++i) { int f = v & 1; v >>= 1; if (f) v ^= 0x1408; } } return v == 0x1c89; }
that is, sort of CRC.
To discover it I’ve collected NORCPU execution trace and “disassembled” it.
Modified NORCPU source and disassembler are attached, and also may be found there: http://jcmvbkbc.spb.ru/git/?p=dumb/norcpu.git;a=summary
И довесок:
The method used is pretty straightforward:
- collect execution trace
- recognize instruction patterns and collapse sequences of primitive instructions to more complex ones
- analyze disassembled trace
So, first I needed trace: I copied javascript text into cpp source, fixed lingual differences and inserted the following printf:
while (1) { int i = mem[ip]; printf("%04x:NOR(%04x, %04x => %04x) ", i, mem[i], mem[i + 1], mem[i + 2]); int a = mem[i + 0];
so that I got a long line (about 8Mb) of primitive instruction execution trace.
Then I started constructing
sed
script that would make it readable.First, it broke the trace line-wise, one instruction per line (288323 lines, will read it in case of insomnia). I took a look at processed trace and recorded several obvious instruction patterns into
sed
. Then reran script, took next look, recorded more patterns, …This way I figured out all boolean logic commands and jumps. Then rotations left. Each time new command got recognized, new filtered processed trace was suggesting next step, e.g. 15 ROTL equals ROTR etc.
Then I looked into your article at “Модель процессора с одной командой”. And found addition pattern in disassembly. And recorded it in
sed
script.After that I was able to just read the trace (which shrunk to 1035 lines). Its inner loop fit into one page, I just made some notes on a scratchpad:
[f1ba]: current in-T index (i) [f1b4]: LEN [f1b5]: 8 0012-0035:[f1b9] ^= (T[i] & 0xff) 006d-007b:[f1b8] = [f1b9] & 1 008a-0158:[f1b9] >>= 1 0167-10c7:[f1aa] = [f1b8] + -1, [f1ab] = !carry 10ca-10e6:jmp on carry to 1145:110d 110d-111b:[f1b9] ^= 1408 1145-1f4f:--[f1b5] 20a5-3005:[f1aa] = [f1b5] + -1, f1ab = !carry 3008-3024:jmp on carry to 006d:304b 304b-304b:++i 3fab-3fab:--LEN 4f0b-5e8a:jmp on carry to 5eb1:6
then I browsed through the repetitions of this inner loop and found the end of the outer loop.
5eb1-6e74:check 1c89
Then just translated it into C. It all took me three evenings.
Затем от Max Filippov пришло решение и второй задачи.
Ответ на второй тур –
h1cKmE1fUsAn
Результат –
R0und2 D0ne!
Алгоритм проверки пароля такой:
bool check(const char *p) { static const int xor_array[] = { 0x3469, 0x9c11, 0xd41e, 0x7ddf, 0x78b4, 0x6aed, 0x4024, 0xc03a, 0x4164, 0xc3c3, 0x4b6c, 0xe1ca, }; int v = 0; int x = 0x3401; for (int i = 0; i < 12; ++i) { int f = p[i] ^ x ^ xor_array[i]; // printf("x: %04x, f: %04x\n", x, f); v |= f; x = (x * 3 + 0x1d) & 0xffff; } return !v; }
Закомментированный
printf
выводит ключевую фразу по ходу.Методика анализа – как и в первом туре – дизассемблирование трассы выполнения.
Первый тур был откровенно интереснее.
И, наконец, последнее полученное решение от Salo Kril для первой задачи.
Особых пояснений нет – просто исходники.
// Генерация паролей // Brute_force(3); WORD ks_f(char *buff, int len) { int i, j; WORD ks = 0x1040; for (i = 0; i < len; i++) { ks ^= buff[i]; for (j = 0; j < 8; j++) { if((ks & 1) == 0) ks = ks >> 1; else ks = (ks >> 1) ^ 0x1408; } } return ks; } void Brute_force(int n) { int i; static char alphabet[] = "\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2A\x2B\x2C\x2D\x2E\x2F" "\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3A\x3B\x3C\x3D\x3E\x3F" "\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4A\x4B\x4C\x4D\x4E\x4F" "\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5A\x5B\x5C\x5D\x5E\x5F" "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6A\x6B\x6C\x6D\x6E\x6F" "\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7A\x7B\x7C\x7D\x7E"; if(n == 0) { if(ks_f(buff_bf, BF_N) == 0x1c89) printf("%s\n",buff_bf); return; } n--; for (i = 0; alphabet[i]; i++) { buff_bf[n] = alphabet[i]; Brute_force(n); } }
и реконструированный код:
#define DEST_COUNT 0xF1FE extern WORD mem[]; /* Secret code: 139471 */ void reconstructed_fn(void) { int i, j; WORD src, dest, key, count, hash, hash_OK, key_const; src = mem[0xF1BA]; // input string count = mem[0xF1ED]; // input string length dest = mem[0xF1BB]; // 0xf1ff hash_OK = mem[0xF1BC]; // 0x1c89 hash = mem[0xF1B9]; // 0x1040 for(i = 0; i < count; i++) { hash ^= mem[src + i] & 0xFF; for (j = 0; j < 8; j++) { if ((hash & 1) == 0) hash = hash >> 1; else hash = (hash >> 1) ^ 0x1408; } } if (hash == hash_OK) { src = mem[0x6EB6]; // "Secret code: 139471" count = mem[0xF1C7]; // 19 key = ((hash >> 8) ^ hash) + 1; key_const = mem[0x6EA8]; // 11 } else { src = mem[0x5EBE]; // "Wrong password!" count = mem[0xF1DC]; // 15 key = mem[0xF1BD]; key_const = mem[0xF1BE]; // 17 } mem[DEST_COUNT] = count; for (i = 0; i < count; i++) { mem[dest + i] = mem[src + i] ^ key; key = key * 3 + key_const; } } /* -------------------------------------------------------------------------------------------- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -------------------------------------------------------------------------------------------- */ WORD and(WORD w1, WORD w2) { return w1 & w2; } WORD or(WORD w1, WORD w2) { return w1 | w2; } WORD xor(WORD w1, WORD w2) { return w1 ^ w2; } WORD rol(WORD w1, WORD n) { return (w1 << n) | (w1 >> (16 - n)); } WORD ror(WORD w1, WORD n) { return (w1 >> n) | (w1 << (16 - n)); } WORD extend_16(WORD k1) { int i, k2; for (i = 0, k2 = 0; i < 16; i++) { k2 = or(k2, k1); k1 = rol(k1, 1); } return k2; } WORD add(WORD *kk1, WORD a, WORD b) { WORD mask_bit, aa0, aa1, tmp1, i, k1, k2; k1 = 0; k2 = 0; mask_bit = 1; for (i = 0; i < 16; i++) { k1 = and(k1, mask_bit); aa0 = xor(a, b); tmp1 = xor(aa0, k1); tmp1 = and(tmp1, mask_bit); k2 = or(tmp1, k2); aa1 = and(a, b); aa0 = and(k1, aa0); k1 = or(aa1, aa0); k1 = rol(k1, 1); mask_bit = rol(mask_bit, 1); } k1 = and(k1, mask_bit); *kk1 = extend_16(k1); return k2; } void reconstr(void) { WORD k1, k2, src, dest, tmp1, key, count, tmp2, tmp3, i, ks, ks_OK, key_a; WORD mask_bit, aa1, aa0; k1 = mem[0xF1AF]; // 0x88 k2 = mem[0xF1A3]; // 0x47 src = mem[0xF1BA]; // input string count = mem[0xF1ED]; // input string length dest = mem[0xF1BB]; // 0xf1ff ks_OK = mem[0xF1BC]; // 0x1c89 "w":ks=0x0ACC "WWW":0x0FCE "123456789":ks=0x05E3 ks = mem[0xF1B9]; // 0x1040 l_0006: ks = xor(ks, and(mem[src], 0xFF)); i = 8; l_006D: tmp3 = and(ks, 1); ks = ror(ks, 1); ks = and(ks, 0x7FFF); tmp2 = add(&k2, tmp3, -1); if(k2 != 0) { ks = xor(ks, 0x1408); } l_1145: i = add(&k1, i, -1); tmp2 = add(&k2, i, -1); if(k2 != 0) goto l_006D; l_304B: src = add(&k1, src, 1); count = add(&k1, count, -1); tmp2 = add(&k2, count, -1); if(k2 != 0) goto l_0006; src = mem[0x5EBE]; // Wrong password! count = mem[0xF1DC]; // 15 key = mem[0xF1BD]; key_a = mem[0xF1BE]; ks_OK = xor(ks_OK, ks); tmp2 = add(&k2, ks_OK, -1); if(k2 != 0) goto l_8552; l_6E9B: src = mem[0x6EB6]; // Secret code: 139471 count = mem[0xF1C7]; // 19 key = ks; key_a = mem[0x6EA8]; key = ror(key, 8); key = and(key, 0xFF); key = xor(ks, key); key = and(key, 0x7FFF); key = add(&k1, key, 1); l_8552: mem[DEST_COUNT] = count; l_8558: mem[dest] = xor(mem[src], key); tmp3 = add(&k1, key, key); key = add(&k1, key, tmp3); key = add(&k1, key, key_a); src = add(&k1, src, 1); dest = add(&k1, dest, 1); count = add(&k1, count, -1); tmp2 = add(&k2, count, -1); if(k2 != 0) goto l_8558; l_F186: mem[0xF1B2] = mem[0xF193]; // nc }
Итак, как вы уже поняли, решения были полностью исчерпывающими.
Всем приславшим огромное спасибо за проявленное внимание.
Ниже привожу оригинальные исходники обоих задач. Надо просто запустить питоновский скрипт, он скомпилирует код, написанный через функции-макросы, сделает тестовый прогон и сгенерирует html-страничку (нужен файл-шаблон template.html
).
Весь архив вместе c решениями-взломами доступны в виде git репозитория.
Файл norcpu.py (template.html):
import sys, re, time, string, binascii verbose = False verbose_cpu = False scramble = True test_wrong_crc = 0 secret_code = "Secret code: 139471" password = "h0cKmE1fUsAn" guess = "123456789012" guess = password # Secret code message encryption mask. secret_coef_add = 17 message_text = "Wrong password!" # Wrong password message encryption mask. message_mask = 0x6301 message_coef_add = 11 # Non-standard CRC initial value (should be 0xFFFF). crc16_initial_value = 0x1040 # Non-standard CRC constant (should be 0x8401). crc16_constant = 0x1408 code_segment = [] data_segment = [] label_count = 0 def dump(data, length = 8): result = [] for i in xrange(0, len(data), length): line = data[i:i + length] hex_line = ' '.join(["%04X" % x for x in line]) result.append("%04X: %-*s\n" % (i, length*5, hex_line)) return ''.join(result) def dump_js(data, length = 8): result = [] for i in xrange(0, len(data), length): line = data[i:i + length] hex_line = ' '.join(["0x%04X," % x for x in line]) result.append("%-*s\n" % (length*5, hex_line)) return ''.join(result) def calc_crc16(data): global crc16_initial_value global crc16_constant crc16 = crc16_initial_value for i in range(0, len(data)): ch = ord(data[i]) & 0xff crc16 = crc16 ^ ch for j in range(0, 8): if ((crc16 & 1) != 0): crc16 = (crc16 >> 1) ^ crc16_constant else: crc16 = crc16 >> 1 return crc16 crc16 = calc_crc16(password) def encode_string(data, name, mask, coef_add): global mem, names offset = names[name] offset_sz = names[name + "_sz"] for i in range(0, len(data)): mem[offset + i] = ord(data[i]) ^ mask mask = (mask * 3 + coef_add) & 0xffff mem[offset_sz] = len(data) def put_string(data, name): global mem, names offset = names[name] offset_sz = names[name + "_sz"] for i in range(0, len(data)): mem[offset + i] = ord(data[i]) mem[offset_sz] = len(data) def save_mem(name, size = -1): f = open(name, "w") if size == -1: size = len(mem) for i in (mem[0:size]): hex = "%04X" % i bin = binascii.a2b_hex(hex) f.write(bin) f.close() def next_label(): global label_count label_count = label_count + 1 return "label_%04d" % label_count def code_rem(comment): code_segment.append('; ' + comment) def data_rem(comment): data_segment.append('; ' + comment) def data_label(name): data_segment.append(name + ":") def code_label(name): code_segment.append(name + ":") def code(value): printed = value if type(value).__name__ == 'int': printed = "%d" % value code_segment.append(" dw %s" % printed) scramble_counter = 0x27 def next_scramble_counter(): global scramble_counter scramble_counter = scramble_counter * 3 + 7 return scramble_counter & 0xff def word(value): if value == -1: if scramble: value = next_scramble_counter() else: value = 0 printed = value if type(value).__name__ == 'int': printed = "%d" % value data_segment.append(" dw %s" % printed) def buffer(length, value = -1): for i in range(0, length): word(value) def var(name, value = -1): data_label(name); word(value); def NOR(a, b, r): code_rem('NOR ' + str(a) + ' ' + str(b) + ' ' + str(r)) code(a) code(b) code(r) def NOT(a, r): NOR(a, a, r); def OR(a, b, r): NOR(a, b, "or_reg") NOT("or_reg", r) var("or_reg") def AND(a, b, r): NOT(a, "and_reg_a") NOT(b, "and_reg_b") OR("and_reg_a", "and_reg_b", "and_reg_a") NOT("and_reg_a", r) var("and_reg_a") var("and_reg_b") def ANDi(a, imm, r): MOVi(imm, "and_i_reg") AND(a, "and_i_reg", r) var("and_i_reg") def XOR(a, b, r): NOT(a, "xor_reg_a") NOT(b, "xor_reg_b") AND(a, "xor_reg_b", "xor_reg_b") AND(b, "xor_reg_a", "xor_reg_a") OR("xor_reg_a", "xor_reg_b", r) var("xor_reg_a") var("xor_reg_b") def XORi(a, imm, r): MOVi(imm, "xor_i_reg") XOR(a, "xor_i_reg", r) var("xor_i_reg") def MOV(a, b): code_rem('MOV ' + str(a) + ' ' + str(b)) NOT(a, "move_reg") NOT("move_reg", b) code_rem('MOV END') var("move_reg") def JMP(a): code_rem('JMP ' + str(a)) MOV(a, "ip") def JMPi(a): code_rem('JMPi ' + str(a)) label = next_label() JMP(label) code_label(label) code(a) def MOVi(imm, a): code_rem('MOVi #' + str(imm) + ' ' + str(a)) label_data = next_label() label_jump = next_label() MOV(label_data, a) JMPi(label_jump) code_label(label_data) code(imm) code_label(label_jump) # [a] -> b def PEEK(a, b): label1 = next_label() label2 = next_label() MOV(a, label1) MOV(a, label2) code_label(label1) # NOT(0, 0, move_reg) code(0) # <- a code_label(label2) # code(0) # <- a code("move_reg") # NOT("move_reg", b) # a -> [b] def POKE(a, b): code_rem('POKE ' + str(a) + ' [' + str(b) + ']') label = next_label() MOV(b, label) NOT(a, "move_reg") # +3 (three operations) code("move_reg") # +4 code("move_reg") # +5 code_label(label) code(0) # <- b # imm -> [a] def POKEi(imm, a): MOVi(imm, "poke_i_reg") POKE("poke_i_reg", a) var("poke_i_reg") def EXIT(a): MOV(a, "exit_reg") def EXITi(a): MOVi(a, "exit_reg") def FADD(mask, carry, a, b, r): AND(a, mask, "fadd_reg_a") # zero bits in 'a' except mask'ed AND(b, mask, "fadd_reg_b") # zero bits in 'b' except mask'ed AND(carry, mask, carry) # zero bits in 'carry' except mask'ed # SUM = (a ^ b) ^ carry XOR(a, b, "fadd_reg_t1") XOR("fadd_reg_t1", carry, "fadd_reg_bit_r") # Leave only 'mask'ed bit in bit_r. AND("fadd_reg_bit_r", mask, "fadd_reg_bit_r") # Add current added bit to the result. OR("fadd_reg_bit_r", r, r) # CARRY = (a & b) | (carry & (a ^ b)) AND(a, b, "fadd_reg_t2") AND(carry, "fadd_reg_t1", "fadd_reg_t1") # CARRY is calculated, and 'shift_reg' contains the same value # but shifted the left by 1 bit. OR("fadd_reg_t2", "fadd_reg_t1", carry) # CARRY is shifted the left by 1 bit to be used on the next round. MOV("shift_reg", carry) # shift_reg = mask << 1 MOV(mask, mask) # mask = shift (effectively "mask = mask << 1") MOV("shift_reg", mask) AND(carry, mask, carry) var("fadd_reg_a") var("fadd_reg_b") var("fadd_reg_bit_r") var("fadd_reg_t1") var("fadd_reg_t2") def ZERO(a): XOR(a, a, a) def FADC(a, b, r): ZERO("fadc_reg_t") MOV("const_1", "fadc_reg_mask") for i in range(0, 16): FADD("fadc_reg_mask", "carry_reg", a, b, "fadc_reg_t") MOV("fadc_reg_t", r) ZERO("fadc_reg_t") for i in range(0, 16): OR("fadc_reg_t", "carry_reg", "fadc_reg_t") MOV("carry_reg", "carry_reg") MOV("shift_reg", "carry_reg") MOV("fadc_reg_t", "carry_reg") var("fadc_reg_mask") var("fadc_reg_t") def ADD(a, b, r): ZERO("carry_reg") FADC(a, b, r) def ADDi(a, imm, r): MOVi(imm, "add_i_reg") ADD(a, "add_i_reg", r) var("add_i_reg") def PUSH(a): ADD("stack_reg", "const_minus_1", "stack_reg") POKE(a, "stack_reg") def PUSHi(imm): MOVi(imm, "push_i_reg") PUSH("push_i_reg") var("push_i_reg") def POP(a): PEEK("stack_reg", a) ADD("stack_reg", "const_1", "stack_reg") def CALL(a): label = next_label() PUSHi(label) JMP(a) code_label(label) def CALLi(a): label = next_label() PUSHi(label) JMPi(a) code_label(label) def RET(): POP("ip") # Jump 'a', if cond = FFFF, and 'b' if conf = 0000 def BRANCH(a, b, cond): AND(a, cond, "branch_reg_a") # reg_a = a & cond NOT(cond, "branch_reg_b") # reg_b = !cond AND(b, "branch_reg_b", "branch_reg_b") # reg_b = b & reg_b = b & !cond OR("branch_reg_a", "branch_reg_b", "ip") # ip = (a & cond) | (b & !cond) var("branch_reg_a") var("branch_reg_b") # Jump 'a', if cond = FFFF, and 'b' if conf = 0000 def BRANCHi(a, b, cond): MOVi(a, "branch_i_reg_a") MOVi(b, "branch_i_reg_b") BRANCH("branch_i_reg_a", "branch_i_reg_b", cond) var("branch_i_reg_a") var("branch_i_reg_b") # if a != 0 -> carry = FFFF else carry = 0000 def IS_0(a): ZERO("carry_reg") FADC(a, "const_minus_1", "is_0_reg") NOT("carry_reg", "zero_reg") var("is_0_reg") var("zero_reg") # ip = (zero_reg == FFFF ? a : ip) def JZi(a): label = next_label() BRANCHi(a, label, "zero_reg") code_label(label) # ip = (zero_reg == FFFF ? a : ip) def JNZi(a): label = next_label() BRANCHi(label, a, "zero_reg") code_label(label) def ROL(a, b): MOV(a, a) # shift_reg = a << 1 MOV("shift_reg", b) def ROR(a, b): MOV(a, "ror_reg") for i in range(0, 15): ROL("ror_reg", "ror_reg") MOV("ror_reg", b) var("ror_reg") def SHL(a, b): ROL(a, b) ANDi(b, 0x0001, b) def SHR(a, b): ROR(a, b) ANDi(b, 0x7FFF, b) # NORCPU code var("ip", "start") var("shift_reg") var("carry_reg") var("const_1", 1) var("const_minus_1", 0xFFFF) var("exit_reg") var("stack_reg", "stack") code_label("start") var("i") var("j") var("ch") var("mask") var("t") var("crc16", crc16_initial_value) var("ptr", "password") MOV("password_sz", "i") crc_loop = next_label() code_label(crc_loop) # crc_loop # ch = *ptr PEEK("ptr", "ch") # ch &= 0xFF ANDi("ch", 0xFF, "ch") # crc16 ^= ch XOR("crc16", "ch", "crc16") MOVi(8, "j") crc_loop_j = next_label() code_label(crc_loop_j) # crc_loop_j # t = crc16 & 1 ANDi("crc16", 1, "t") # crc16 >>= 1 SHR("crc16", "crc16") IS_0("t") crc_loop_1 = next_label() JZi(crc_loop_1) # crc16 ^= crc16_constant XORi("crc16", crc16_constant, "crc16") code_label(crc_loop_1) # crc_loop_1 ADD("j", "const_minus_1", "j") IS_0("j") JNZi(crc_loop_j) # ptr += 1 ADD("ptr", "const_1", "ptr") # i = i - 1 ADD("i", "const_minus_1", "i") IS_0("i") JNZi(crc_loop) var("ptr2", "result") correct_crc = crc16 + test_wrong_crc var("correct_crc", correct_crc) # By default we're going to decrypt 'Wrong...' message. MOVi("message", "ptr") MOV("message_sz", "i") var("message_mask", message_mask) MOV("message_mask", "mask") var("coef_add", message_coef_add) wrong_label = next_label() XOR("correct_crc", "crc16", "correct_crc") IS_0("correct_crc") JNZi(wrong_label) # Now we switch to descrypt the secret message. MOVi(secret_coef_add, "coef_add") MOVi("secret", "ptr") MOV("secret_sz", "i") # mask = ((crc16 & 0xff) | ((crc16 >> 8) & 0xff)) + 1 MOV("crc16", "mask") for i in range(0, 8): SHR("mask", "mask") XOR("crc16", "mask", "mask") ANDi("mask", 0xff, "mask") ADD("mask", "const_1", "mask") code_label(wrong_label) MOV("i", "result_sz") loop = next_label() code_label(loop) # loop # ch = *ptr PEEK("ptr", "ch") # ch ^= mask XOR("ch", "mask", "ch") POKE("ch", "ptr2") # mask = mask * 3 + 11 ADD("mask", "mask", "t") ADD("mask", "t", "mask") ADD("mask", "coef_add", "mask") # ptr += 1 ADD("ptr", "const_1", "ptr") # ptr2 += 1 ADD("ptr2", "const_1", "ptr2") # i = i - 1 ADD("i", "const_minus_1", "i") IS_0("i") JNZi(loop) EXITi(0x00) buffer(8) data_label("stack") var("secret_sz", len(secret_code)) data_label("secret") buffer(len(secret_code) + 1) var("message_sz") data_label("message") buffer(16) var("password_sz") data_label("password") buffer(16) # The buffer holding the result string. var("result_sz") data_label("result") buffer(32) # Compiler text = code_segment text.extend(data_segment) if verbose: print "\n".join(text) # Phase 1. Calculate names. addr = 0 names = {} for line in text: if line[0] == ';': continue if line[0] != ' ': name = line.partition(':')[0] names[name] = addr else: addr = addr + 1 if verbose: print names raw_text = "\n".join(text) # Resolve names. for name in names: if verbose: print name, names[name], type(names[name]) name_re = re.compile(r'dw ' + name + '$', re.M) value = "%d" % names[name] raw_text = name_re.sub('dw ' + value, raw_text) text = raw_text.split("\n") if verbose: print "\n".join(text) # Phase 2. Compilation. addr = 0 comment = "" mem = [] for line in text: if line[0] == ';' or line[0] != ' ': comment = comment + line + ' ' else: value = int(line.strip().partition(" ")[2]) if verbose: print "%04X: %04X ; %s" % (addr, value, comment) mem.append(value) addr = addr + 1 comment = "" # Interpretation ip = names["ip"] exit_reg = names["exit_reg"] shift_reg = names["shift_reg"] carry_reg = names["carry_reg"] def nor(a, b): r = a | b r = r ^ 0xFFFF return r & 0xFFFF def norcpu(): while 1: i = mem[ip]; a = mem[i + 0] b = mem[i + 1] r = mem[i + 2] mem[ip] = i + 3 f = nor(mem[a], mem[b]) mem[r] = f mem[shift_reg] = ((f >> 15) & 1) | ((f & 0x7FFF) << 1) if verbose_cpu: print "%04X: %04X [%04X] %04X [%04X] -> %04X [%04X]" % \ (i, a, mem[a], b, mem[b], r, mem[r]) if r == exit_reg: break print "Starting from [%04X]" % mem[ip] # Encrypt the secret code. secret_mask = ((crc16 & 0xff) ^ ((crc16 >> 8) & 0xff)) + 1 encode_string(secret_code, "secret", secret_mask, secret_coef_add); # Encrypt 'Wrong...' message. encode_string(message_text, "message", message_mask, message_coef_add); mem_js = dump_js(mem) save_mem("norcpu-1-before.bin") mem_sz = len(mem) if len(mem) >= 0x10000: print "Too much code (%08X, %04X)" % (len(mem), len(mem) - 0x10000) sys.exit() # Inject plain password in the last moment (for testing). put_string(guess, "password") save_mem("norcpu-2-before-with-password.bin") if verbose: print "Original memory:" print dump(mem) start_time = time.time() norcpu() end_time = time.time() save_mem("norcpu-3-after.bin", mem_sz) if verbose: print "Memory after:" dump(mem) print print "Size: %X" % len(mem) print "Time: %d" % (end_time - start_time) print "Exit: %04X" % mem[exit_reg] print("CRC : %04X (%04X)" % (crc16, correct_crc)) result = names["result"] result_value = "" for i in range(0, mem[names["result_sz"]]): result_value = result_value + chr(mem[result + i] & 0xff) if result_value != secret_code: print "ERROR: [%s] != [%s]" % (secret_code, result_value) js = string.Template(open('template.html', 'r').read()) js = js.substitute( \ ip = names["ip"], exit_reg = names["exit_reg"], shift_reg = names["shift_reg"], password = names["password"], password_sz = names["password_sz"], result = names["result"], result_sz = names["result_sz"], mem_js = mem_js ) f = open("norcpu.html", "w") f.write(js) f.close()
Файл norcpu.py (template.html).
import sys, re, time, string, binascii verbose = False verbose_cpu = False scramble = True secret_password = "h1cKmE1fUsAn" secret_password_xor_mask = 0x3401 secret_password_add = 29 secret_code = "R0und2 D0ne!" secret_code_xor_mask = 0x730A secret_code_add = 37 guess = "123456789012" guess = secret_password code_segment = [] data_segment = [] label_count = 0 def dump(data, length = 8): result = [] for i in xrange(0, len(data), length): line = data[i:i + length] hex_line = ' '.join(["%04X" % x for x in line]) result.append("%04X: %-*s\n" % (i, length*5, hex_line)) return ''.join(result) def dump_js(data, length = 8): result = [] for i in xrange(0, len(data), length): line = data[i:i + length] hex_line = ' '.join(["0x%04X," % x for x in line]) result.append("%-*s\n" % (length*5, hex_line)) return ''.join(result) def encode_string(data, name, mask, coef_add): global mem, names offset = names[name] offset_sz = names[name + "_sz"] for i in range(0, len(data)): mem[offset + i] = ord(data[i]) ^ mask mask = (mask * 3 + coef_add) & 0xffff mem[offset_sz] = len(data) def put_string(data, name): global mem, names offset = names[name] offset_sz = names[name + "_sz"] for i in range(0, len(data)): mem[offset + i] = ord(data[i]) mem[offset_sz] = len(data) def save_mem(name, size = -1): f = open(name, "w") if size == -1: size = len(mem) for i in (mem[0:size]): hex = "%04X" % i bin = binascii.a2b_hex(hex) f.write(bin) f.close() def next_label(): global label_count label_count = label_count + 1 return "label_%04d" % label_count def code_rem(comment): code_segment.append('; ' + comment) def data_rem(comment): data_segment.append('; ' + comment) def data_label(name): data_segment.append(name + ":") def code_label(name): code_segment.append(name + ":") def code(value): printed = value if type(value).__name__ == 'int': printed = "%d" % value code_segment.append(" dw %s" % printed) scramble_counter = 0x2743 def next_scramble_counter(): global scramble_counter scramble_counter = scramble_counter * 3 + 7 return scramble_counter & 0xffff def word(value): if value == -1: if scramble: value = next_scramble_counter() else: value = 0 printed = value if type(value).__name__ == 'int': printed = "%d" % value data_segment.append(" dw %s" % printed) def buffer(length, value = -1): for i in range(0, length): word(value) def var(name, value = -1): data_label(name); word(value); # Macros def NOR(a, b, r): code_rem('NOR ' + str(a) + ' ' + str(b) + ' ' + str(r)) code(a) code(b) code(r) def NOT(a, r): NOR(a, a, r); def OR(a, b, r): NOR(a, b, "or_reg") NOT("or_reg", r) var("or_reg") def AND(a, b, r): NOT(a, "and_reg_a") NOT(b, "and_reg_b") OR("and_reg_a", "and_reg_b", "and_reg_a") NOT("and_reg_a", r) var("and_reg_a") var("and_reg_b") def ANDi(a, imm, r): MOVi(imm, "and_i_reg") AND(a, "and_i_reg", r) var("and_i_reg") def XOR(a, b, r): NOT(a, "xor_reg_a") NOT(b, "xor_reg_b") AND(a, "xor_reg_b", "xor_reg_b") AND(b, "xor_reg_a", "xor_reg_a") OR("xor_reg_a", "xor_reg_b", r) var("xor_reg_a") var("xor_reg_b") def XORi(a, imm, r): MOVi(imm, "xor_i_reg") XOR(a, "xor_i_reg", r) var("xor_i_reg") def MOV(a, b): code_rem('MOV ' + str(a) + ' ' + str(b)) NOT(a, "move_reg") NOT("move_reg", b) code_rem('MOV END') var("move_reg") def JMP(a): code_rem('JMP ' + str(a)) MOV(a, "ip") def JMPi(a): code_rem('JMPi ' + str(a)) label = next_label() JMP(label) code_label(label) code(a) def MOVi(imm, a): code_rem('MOVi #' + str(imm) + ' ' + str(a)) label_data = next_label() label_jump = next_label() MOV(label_data, a) JMPi(label_jump) code_label(label_data) code(imm) code_label(label_jump) # [a] -> b def PEEK(a, b): label1 = next_label() label2 = next_label() MOV(a, label1) MOV(a, label2) code_label(label1) # NOT(0, 0, move_reg) code(0) # <- a code_label(label2) # code(0) # <- a code("move_reg") # NOT("move_reg", b) # a -> [b] def POKE(a, b): code_rem('POKE ' + str(a) + ' [' + str(b) + ']') label = next_label() MOV(b, label) NOT(a, "move_reg") # +3 (three operations) code("move_reg") # +4 code("move_reg") # +5 code_label(label) code(0) # <- b # imm -> [a] def POKEi(imm, a): MOVi(imm, "poke_i_reg") POKE("poke_i_reg", a) var("poke_i_reg") def EXIT(a): MOV(a, "exit_reg") def EXITi(a): MOVi(a, "exit_reg") def FADD(mask, carry, a, b, r): AND(a, mask, "fadd_reg_a") # zero bits in 'a' except mask'ed AND(b, mask, "fadd_reg_b") # zero bits in 'b' except mask'ed AND(carry, mask, carry) # zero bits in 'carry' except mask'ed # SUM = (a ^ b) ^ carry XOR(a, b, "fadd_reg_t1") XOR("fadd_reg_t1", carry, "fadd_reg_bit_r") # Leave only 'mask'ed bit in bit_r. AND("fadd_reg_bit_r", mask, "fadd_reg_bit_r") # Add current added bit to the result. OR("fadd_reg_bit_r", r, r) # CARRY = (a & b) | (carry & (a ^ b)) AND(a, b, "fadd_reg_t2") AND(carry, "fadd_reg_t1", "fadd_reg_t1") # CARRY is calculated, and 'shift_reg' contains the same value # but shifted the left by 1 bit. OR("fadd_reg_t2", "fadd_reg_t1", carry) # CARRY is shifted the left by 1 bit to be used on the next round. MOV("shift_reg", carry) # shift_reg = mask << 1 MOV(mask, mask) # mask = shift (effectively "mask = mask << 1") MOV("shift_reg", mask) AND(carry, mask, carry) var("fadd_reg_a") var("fadd_reg_b") var("fadd_reg_bit_r") var("fadd_reg_t1") var("fadd_reg_t2") def ZERO(a): XOR(a, a, a) def FADC(a, b, r): ZERO("fadc_reg_t") MOV("const_1", "fadc_reg_mask") for i in range(0, 16): FADD("fadc_reg_mask", "carry_reg", a, b, "fadc_reg_t") MOV("fadc_reg_t", r) ZERO("fadc_reg_t") for i in range(0, 16): OR("fadc_reg_t", "carry_reg", "fadc_reg_t") MOV("carry_reg", "carry_reg") MOV("shift_reg", "carry_reg") MOV("fadc_reg_t", "carry_reg") var("fadc_reg_mask") var("fadc_reg_t") def ADD(a, b, r): ZERO("carry_reg") FADC(a, b, r) def ADDi(a, imm, r): MOVi(imm, "add_i_reg") ADD(a, "add_i_reg", r) var("add_i_reg") def PUSH(a): ADD("stack_reg", "const_minus_1", "stack_reg") POKE(a, "stack_reg") def PUSHi(imm): MOVi(imm, "push_i_reg") PUSH("push_i_reg") var("push_i_reg") def POP(a): PEEK("stack_reg", a) ADD("stack_reg", "const_1", "stack_reg") def CALL(a): label = next_label() PUSHi(label) JMP(a) code_label(label) def CALLi(a): label = next_label() PUSHi(label) JMPi(a) code_label(label) def RET(): POP("ip") # Jump 'a', if cond = FFFF, and 'b' if conf = 0000 def BRANCH(a, b, cond): AND(a, cond, "branch_reg_a") # reg_a = a & cond NOT(cond, "branch_reg_b") # reg_b = !cond AND(b, "branch_reg_b", "branch_reg_b") # reg_b = b & reg_b = b & !cond OR("branch_reg_a", "branch_reg_b", "ip") # ip = (a & cond) | (b & !cond) var("branch_reg_a") var("branch_reg_b") # Jump 'a', if cond = FFFF, and 'b' if conf = 0000 def BRANCHi(a, b, cond): MOVi(a, "branch_i_reg_a") MOVi(b, "branch_i_reg_b") BRANCH("branch_i_reg_a", "branch_i_reg_b", cond) var("branch_i_reg_a") var("branch_i_reg_b") # if a != 0 -> carry = FFFF else carry = 0000 def IS_0(a): ZERO("carry_reg") FADC(a, "const_minus_1", "is_0_reg") NOT("carry_reg", "zero_reg") var("is_0_reg") var("zero_reg") # ip = (zero_reg == FFFF ? a : ip) def JZi(a): label = next_label() BRANCHi(a, label, "zero_reg") code_label(label) # ip = (zero_reg == FFFF ? a : ip) def JNZi(a): label = next_label() BRANCHi(label, a, "zero_reg") code_label(label) def ROL(a, b): MOV(a, a) # shift_reg = a << 1 MOV("shift_reg", b) def ROR(a, b): MOV(a, "ror_reg") for i in range(0, 15): ROL("ror_reg", "ror_reg") MOV("ror_reg", b) var("ror_reg") def SHL(a, b): ROL(a, b) ANDi(b, 0x0001, b) def SHR(a, b): ROR(a, b) ANDi(b, 0x7FFF, b) def MUL3(a, b): ADD(a, a, "mul3_reg") # mul3_reg = a + a ADD("mul3_reg", a, b) # b = mul3_reg + a var("mul3_reg") # NORCPU code var("ip", "start") var("shift_reg") var("carry_reg") var("const_1", 1) var("const_minus_1", 0xFFFF) var("exit_reg") var("stack_reg", "stack") code_label("start") var("ch") var("t") var("xor_mask") var("cmp_flag") var("ptr") var("ptr2") var("i") MOVi("exchange", "ptr") MOVi("secret_password", "ptr2") MOVi(secret_password_xor_mask, "xor_mask") MOVi(0, "cmp_flag") MOVi(len(secret_password), "i") cmp_loop = next_label() code_label(cmp_loop) # cmp_loop: PEEK("ptr", "ch") # ch = *ptr XOR("ch", "xor_mask", "ch") # ch ^= xor_mask PEEK("ptr2", "t") # t = *ptr2 XOR("ch", "t", "ch") # ch = ch ^ t OR("cmp_flag", "ch", "cmp_flag") # cmp_flag |= ch ADD("ptr", "const_1", "ptr") # ptr += 1 ADD("ptr2", "const_1", "ptr2") # ptr2 += 1 MUL3("xor_mask", "xor_mask") # xor_mask *= 3 ADDi("xor_mask", secret_password_add, "xor_mask") # xor_mask += add_const ADD("i", "const_minus_1", "i") # i -= 1 IS_0("i") JNZi(cmp_loop) MOVi(0, "exchange_sz") ok_label = next_label() IS_0("cmp_flag") JZi(ok_label) exit_label = next_label() JMPi(exit_label) code_label(ok_label) MOVi("secret_code", "ptr") MOV("secret_code_sz", "i") MOVi(secret_code_xor_mask, "xor_mask") MOVi("exchange", "ptr2") MOV("i", "exchange_sz") loop = next_label() code_label(loop) # loop: PEEK("ptr", "ch") # ch = *ptr XOR("ch", "xor_mask", "ch") # ch ^= xor_mask POKE("ch", "ptr2") # *ptr2 = ch MUL3("xor_mask", "xor_mask") # xor_mask *= 3 ADDi("xor_mask", secret_code_add, "xor_mask") # xor_mask += add_const ADD("ptr", "const_1", "ptr") # ptr += 1 ADD("ptr2", "const_1", "ptr2") # ptr2 += 1 ADD("i", "const_minus_1", "i") # i = i - 1 IS_0("i") JNZi(loop) code_label(exit_label) # exit_label: EXITi(0x00) buffer(8) data_label("stack") var("secret_code_sz", len(secret_code)) data_label("secret_code") buffer(len(secret_code)) var("secret_password_sz") data_label("secret_password") buffer(16) var("exchange_sz", 0) data_label("exchange") buffer(32) # Compiler text = code_segment text.extend(data_segment) if verbose: print "\n".join(text) # Phase 1. Calculate names. addr = 0 names = {} for line in text: if line[0] == ';': continue if line[0] != ' ': name = line.partition(':')[0] names[name] = addr else: addr = addr + 1 if verbose: print names raw_text = "\n".join(text) # Resolve names. for name in names: if verbose: print name, names[name], type(names[name]) name_re = re.compile(r'dw ' + name + '$', re.M) value = "%d" % names[name] raw_text = name_re.sub('dw ' + value, raw_text) text = raw_text.split("\n") if verbose: print "\n".join(text) # Phase 2. Compilation. addr = 0 comment = "" mem = [] for line in text: if line[0] == ';' or line[0] != ' ': comment = comment + line + ' ' else: value = int(line.strip().partition(" ")[2]) if verbose: print "%04X: %04X ; %s" % (addr, value, comment) mem.append(value) addr = addr + 1 comment = "" # Interpretation ip = names["ip"] exit_reg = names["exit_reg"] shift_reg = names["shift_reg"] carry_reg = names["carry_reg"] def nor(a, b): r = a | b r = r ^ 0xFFFF return r & 0xFFFF def norcpu(): while 1: i = mem[ip]; a = mem[i + 0] b = mem[i + 1] r = mem[i + 2] mem[ip] = i + 3 f = nor(mem[a], mem[b]) mem[r] = f mem[shift_reg] = ((f >> 15) & 1) | ((f & 0x7FFF) << 1) if verbose_cpu: print "%04X: %04X [%04X] %04X [%04X] -> %04X [%04X]" % \ (i, a, mem[a], b, mem[b], r, mem[r]) if r == exit_reg: break print "Starting from [%04X]" % mem[ip] encode_string(secret_code, "secret_code", secret_code_xor_mask, secret_code_add); encode_string(secret_password, "secret_password", secret_password_xor_mask, secret_password_add); mem_js = dump_js(mem) save_mem("norcpu-1-before.bin") mem_sz = len(mem) if len(mem) >= 0x10000: print "Too much code (%08X, %04X)" % (len(mem), len(mem) - 0x10000) sys.exit() # Inject plain password in the last moment (for testing). put_string(guess, "exchange") save_mem("norcpu-2-before-with-password.bin") if verbose: print "Original memory:" print dump(mem) start_time = time.time() norcpu() end_time = time.time() save_mem("norcpu-3-after.bin", mem_sz) if verbose: print "Memory after:" dump(mem) print print "Size: %X" % len(mem) print "Time: %d" % (end_time - start_time) print "Exit: %04X" % mem[exit_reg] exchange = names["exchange"] result_value = "" for i in range(0, mem[names["exchange_sz"]]): result_value = result_value + chr(mem[exchange + i] & 0xff) print "Result: [%s]" % result_value if len(result_value) == 0: print "ERROR: Wrong password" js = string.Template(open('template.html', 'r').read()) js = js.substitute( \ ip = names["ip"], exit_reg = names["exit_reg"], shift_reg = names["shift_reg"], exchange = names["exchange"], exchange_sz = names["exchange_sz"], mem_js = mem_js ) f = open("norcpu2.html", "w") f.write(js) f.close()