Программа, устойчивая к редактированию
Клиент заказывает Программисту Складыватель. Складыватель читает два целых числа и печатает их сумму. Программист пишет:
a, b = map(int, input().split())
print(a + b)
Клиент доволен, но он опасается, что программа могла исказиться при передаче. Например, если +
случайно заменился на -
, программа продолжить работать, но результаты будут неверные. Клиент может понять что программа напечатала число, целое число он узнает, но проверить результат не может. В случае замены знаков, Клиент будет получать неправильные результаты и не узнает об этом. А это плохо.
Искажением считается удаление одного символа, или добавление одного символа, или замена одного символа.
Клиент отличает целые числа от другого текста. Если программа напечатает не число, Клиент это поймёт. Нельзя допустить чтобы искажённая программа напечатала единственное целое число, которое не равно сумме аргументов.
Вопрос: можно ли написать Складыватель, устойчивый к искажению?
Вопрос *: Для какого максимального количества искажений k можно написать k-устойчивый Складыватель?
P.S. Если искажённый Складыватель падает с ошибкой, это не проблема. Клиент увидит что есть ошибка и попросит Программиста переслать Складыватель ещё раз. Опасны только те искажения, которые Клиент не может отличить от правильной работы Складывателя.
Десятые сутки
dofi4ka, 22 искажения
dofi4ka – абсолютный лидер!
Наиболее полное решение. Указана верхняя граница устройчивости программы на Питоне – 23 символа, приведён соответствующий пример. Построена программа, которую нельзя сломать изменив только 22 символа. Хотя это не доказано формально (потому что доказать это крайне сложно!), никто не смог привести контрпример.
Вторые сутки
dofi4ka, 22 искажения
dofi4ka обновил своё решение. Это явный лидер, который, кажется, не ломается за 22 изменения. Напомню, что 23 изменения ломают любую программу на Питоне. То есть, это оптимальное решение для Питона.
Если вы захотите состязаться с ним, рекомендую посмотреть как можно решить задачу без exec
. Или решить её на другом языке.
eri, 2 изменения
eri обновил своё решение. Сейчас два изменения, но потенциал есть.
Итоги первой ночи
CrazyElf, 1 искажение
Ответ CrazyElf был первым. Была высказана неожиданная идея о том что Складывать может восстанавливаться после искажений. К сожалению, сам складыватель пока можно испортить одним искажением. Ждём улучшений.
Serge3leo, ? искажений
10 часов назад. Тоже затронута тема восстановения от ошибок. Код нуждается в доводке.
dofi4ka, 6 или 22 (?) искажения
10 часов назад. Код зашифрован в base85. Очень сильный ход! Было заявлено 22 (двадцать два) искажения. Проверка показала, что программу можно сломать шестью искажениями, что тоже очень не плохо. Это рекорд!
Приведена верхняя оценка устойчивости программы на Питоне: любую программу можно исказить добавив 23 символа.
Несколько минут назад dofi4ka обновил ответ. Снова заявлены 22 искажения! Требуется проверка.
Pak Uula, ? искажений
5 часов назад. Поднята тема самокоррекции программы. Нужно больше практики.
eri, 1 искажение
15 минут назад. Анализируется CRC кода программы. Это новая идея. Уверен, что её можно усовершенствовать.
Ответы (9 шт):
Есть одна мысль для одного изменения сделать набор функций с запасом и применить голосование. Если у нас три сумматора и возможна только одна ошибка, то простое большинство голосов за какой-то из вариантов позволит нам исправить ошибку.
from collections import Counter
def sum1(a, b):
return a - b
def sum2(a, b):
return a + b
def sum3(a, b):
return a + b
def summator(a, b):
return next(iter(next(iter(Counter(map(lambda x: x(a, b), [sum1, sum2, sum3])).most_common()))))
a, b = map(int, input().split())
print(summator(a, b))
Одна из функций испорчена (любая), но результат при этом получается корректный. Дальше нужно думать, сколько нужно копий функций, чтобы можно было правильно проголосовать ими, если ошибок станет больше.
А так то вообще можно перейти на функциональный стиль и просто избавиться от знаков арифметики. Но, думаю, вопрос был не в этом. )
a, b = map(int, input().split())
print(int.__add__(a, b))
Вариант, на предмет одиночного искажения программы:
import statistics
def sum_one(a, b, err=0):
return a + b if err != 1 else a - b
def sum_two(a, b, err=0):
return a + b if err != 2 else a - b
def sum_three(a, b, err=0):
return a + b if err != 3 else a - b
def robust_summator(abfile_one, abfile_two, abfile_three, err=0):
return statistics.median((sum_one(*abfile_one(), err),
sum_two(*abfile_two(), err),
sum_three(*abfile_three(), err)))
# Вариант использования с имитацией одиночных ошибок
for a in range(-10, 11):
for b in range(-10, 11):
abfile = lambda: (a, b) # имитатор чтения из входного файла
for err in range(1, 4):
assert(robust_summator(abfile, abfile, abfile) ==
robust_summator(abfile, abfile, abfile, err))
Но, имеет ли смысл рассматривать одиночное искажение программы в отрыве от одиночного искажения исполняющей системы (чтения файла, int.__add__(self, other)
, statistics.median()
и т.п.)?
P.S. Насчёт k искажений, думаю, здесь применимы оценки для блочных кодов исправляющих или корректирующих ошибки - 2k + 1 и 2k.
Верхняя планка
Нам нужно соблюсти три условия чтобы программа выполнилась и дала неправильный ответ:
- принять что-то в stdin
- вывести любое целое число в stdout
- закончить исполнение без ошибок
Кратчайший код, подходящий под эти три условия:
input();exit(print(0))
Этот код можно вполне вставить в начало файла за 23 искажения. Выходит, если k >= 23, то создать устойчивый Складыватель невозможно.
k < 23
Простейшая защита
Давайте подумаем как защитить код хотя бы от одного искажения
Очевидно что для неправильного ответа достаточно лишь вставить куда-нибудь унарный минус. И пока у нас есть голые print
мы вообще никак не защитимся от унарных операторов. Как вариант, вместо голого print
можно использовать exec
, а код Складывателя хранить в строчке, которую будем проверять чек-суммой перед запуском:
from hashlib import sha256
code = b"print(sum(map(int,input().split())))"
if sha256(code).hexdigest() == "f5fd7c191c13e9865867511719e2164c82a8bc132f67696449be642372c48996":
exec(code)
На exec
никак не повлияешь унарным оператором, а изменение переменной code
сразу же повлияет на её чек-сумму!
Выходит что-то вроде двух-факторной аутентификации, нужно одновременно и изменить строчку с кодом и сломать проверку на чек-сумму. Но как вы уже поняли это всего 2 искажения:
from hashlib import sha256
code = b"print(-sum(map(int,input().split())))"
+
if sha256(code).hexdigest() != "f5fd7c191c13e9865867511719e2164c82a8bc132f67696449be642372c48996":
r
exec(code)
Получить устойчивый Складыватель можно!
Другой подход к защите и нижняя планка
Строчку с кодом можно закодировать, например в base85, и тогда просто добавить унарный оператор не выйдет
from base64 import b85encode
b85encode(b"print(sum(map(int,input().split())))")
>>> b'aB^vGbSQImZ76MFa42bRbS!CZaCLMjDK2wxY-w~TDJdx_'
Теперь просто декодируем и запускаем
from base64 import b85decode
exec(b85decode(b'aB^vGbSQImZ76MFa42bRbS!CZaCLMjDK2wxY-w~TDJdx_'))
Чек-сумма защищает всего от 1 искажения так что смысла от неё здесь особо нет
Попытаемся сломать и получим аж 17 искажений!
from base64 import b85decode
input();print(0)#exec(b85decode(b'aB^vGbSQImZ76MFa42bRbS!CZaCLMjDK2wxY-w~TDJdx_'))
+++++++++++++++++
k >= 16
Слабое место программы сейчас - это количество строчек, а именно всего одна. Из-за этого можно выйти без ошибок используя не exit
, а обычной комментарий. Решение же безумно простое, растянем литерал строки на несколько строк:
from base64 import b85decode
exec(b85decode(b'aB^vGbS'
b'QImZ76M'
b'Fa42bRb'
b'S!CZaCL'
b'MjDK2wx'
b'Y-w~TDJ'
b'dx_'))
Теперь нужно комментировать каждую строчку и предыдущий подход потребует целых 23 искажения
просто закомментировать первую и последнюю строчку с литералом base85 не выйдет: мы получим IndentationError
k >= 22
Итог
Нижняя и верхняя планка вместе дают нам ограничение k = [22; 23), то есть мы получили 22-устойчивый Складыватель! Большую устойчивость получить невозможно так как любой Python-код можно обезвредить за 23 искажения
UPD
ниже в коментариях успели пару раз сломать мой 22-устойчивый Складыватель. Я действительно немного переоценил base85 и недооценил вашу фантазию. Что ж, вот вам моя новая версия ;)
from base64 import b85decode
exit(
exec(
b85decode(
b85decode(
b85decode(
b85decode(
b85decode(
b85decode(
b"QA18cOk-6=c5z4~Uv*+(LU>GMT1IG4J6CUfQz1(-YISO4R5MX1N-!ooD_Uh`c~VhlJzi}{WG!i8S3q=NOIIs!drML^KUjH9QZO@iSw=!oXHH&aRykKremh@cR%ka(QY3LpUR7XyS0hSwC;"
)
)
)
)
)
)
)
)
0/0
0/0
0/0
Жду пока кто-нибудь сломает его менее чем за 23 искажения)
UPD2
Ждать пришлось не долго :O
Но я только сейчас догадался что функцию exit()
можно уместить прямо в закодированной строке. Даже не представляю что вы теперь придумаете
from base64 import b85decode
exec(
b85decode(
b85decode(
b85decode(
b85decode(
b85decode(
b85decode(
b'Q8;s0OIK@HPg+)Sd^BN8QB@&PPiSCFD^D#dKPp%|SV3k(USeQoR!l#AIAURFV|`M3P(W5zKu<F(VRv+WLs}tpKY3R>d?+wiOFbk}N;E++OjT4-elS!vAzC|5OLS&zPe@-deMw?GD@IyHdnPMZN=svGS3XC3Zc$k>Ei^|)WMu'
)
)
)
)
)
)
)
0/0
0/0
0/0
UPD3
Моя идея с шестью b85decode
и тремя 0/0
показала себя очень хорошо, однако теперь всё ещё есть слабое место - исходная строчка.
Прежде чем показать новое решение давайте посмотрим как вообще можно сломать то что уже имеется:
- Заменить одну из используемых функций
b"...",{'int':len}
вexec
(12 искажений) - Срезать конец строки и добавить в исходник единичку
b"..."[:-2]+b'+1))'
(13 искажений) - Никто не указал, но всё так же можно вставить унарный минус:
b"...".replace(b'(s',b'(-s')
(22 искажения)
Третий раз вставлять свою лесенку с b85decode
я не буду, кому надо: замените старый литерал на новый:
b'Q8;r{Qc77PWqVD0Z%8I#Bw2MzV_$wYKTUFdH8@&iSXLxZLvl7NPi$33Ax=z1c1cWCZDe^`V^SqPR%=RNdrf^pLS{{JN-0WBKudRaOnpQ<KUhjZD?LbIeMnJHEk<-HPCh6}LSk5OEq7y5PFibZc4<^$T469UW>7?BLM>HbbZ2{ES8-}~Q+aqnH)47wJStX6cq30kQ6_gpSw>-gHbHlKb|_RecYQ`sCSZANOLbykFiuH%C|^=GC0So(Za!soO;IFxR8?R&A$v?wD}G8+WLi#IVM8!0PE22GG)`J$QhjhnY->YJL3THAb7OKiCTLG?Awg74Pg7B5LQpbvIZ8K3Eq+yVP)T7;U?hD#M`l_)V_HQ?PbgzGa4L3XPJ3}oPd;IKXJJ}4NHb$sD_22NeQI`YP(@O0BTyz_SA0W!SABa*cXKFxSZXyuWlm33Ykp8AF<NO^W<XAHN;X1CUrAacH*RH7Q%YJ;Luf%!PD*V<LRn{1O+ZstC~HwdS660zPc3+TUR85&D^*7-DMKSzYa@LtM<{+oI8bD0Q&dcIKsRqwcu{L}MrbQmc2;aRBQi`@X?$i<VKFcyP-9~wK2B~`Bv?lzJ#=AFD^O8*SVuxJNl`^-H9$%-aV2d~LTpV(QX?=wX-!RDVJJr=LSk}bHDFLaN_=!}N>L+Cc~Me0c6@hAenV?JV{uGpDo;aqI95}1c2zxBKuL94OMGTWcSb&6Loh;8aaep&C@nWpQer79Qeu30e0NVfIc-`-ZYXA0Q7dRhIAbt+aYI5^Uw2<laU*jnVqtMORZ%urP;EtRes_3SD?VvEM`ukmPf=NHXdz8Ja3d*KY;|Z!SVd}AMoe!cHg-#MVlg>TWM6tRWp8|NOiVLXFePJpX>xK+Ktp~lQXwdNdt+2bNL5N9L`h3bb3;{cS}J!#c~K@lD_BZ-c5Eb3L~~ChSUi4ADp+oEBu7wbd?j93X;CUQLVHa>Vn|{kS!qLlBsFYPG<9HXNMk>AMpjElPBmmeHF!W{RaR3lR(x7mWlA(bLv>0pZh3iFJ$X|+RZwL&I8!liay>{UVk<>NKx$VzRbYK;O;=8SZBbM(epg3UZ*e0qVl!%0LQQU7G(}}FL`zRlDMC;'
А остальным, я думаю, куда интереснее будет посмотреть что именно там закодировано:
from hashlib import sha256
assert int("123456") == 123456
assert sum([1, 2, 3, 4, 5, 6]) == 21
assert sha256(b"print((((sum(map(int,input().split()))))))").hexdigest() == "78085bffda94487c3ca59f8da61cf5636fb990d3b79f07034d0d1b0ae598ed8f"
print((((sum(map(int,input().split()))))))
exit()
Первые два assert проверяют функции int
и sum
, которые раньше можно было просто заменить на len
и min
или max
соответственно
Третий assert - защита от .replace()
. Работает он очень просто: если вы придумаете как заменить несколько символов в предпоследней строке чтобы программа перестала работать, то в любом случае вы заденете и этот assert с чек-суммой
exit()
теперь находится на отдельной строке, а внутри print
есть несколько лишних пар скобочек. Скобочки эти разумеется не лишние: теперь если вы захотите срезать последнюю часть кода и вставить что-то своё в print
, то вместо [:-2]+b'+1))'
теперь придётся дописать [:-11]+b'+1))));exit()'
, а это уже ровно 23 искажения
Жду с нетерпеньем результатов вашей фантазии по уничтожению моего решения :)
UPD4
Мой подход к защите от срезов выше оказался неправильным. Скобочки можно спокойно обрезать с обеих сторон, чего я, оказывается, не учёл.
Теперь обрезав конец, вам придётся обратно добавлять кучу скобочек, а обрезав начало вы столкнётесь с миллионом NameError
Так же здесь добавлены дополнительные тесты
e, p, s, m, i, inp = exit, print, sum, map, int, input
assert i("123") == 123
assert i("321") == 321
assert s([1, 2, 3, 0, 0, 0]) == 6
assert s([3, 4, 5, 0, 0, 0]) == 12
assert s(m(i, "123456")) == 21
assert sha256(b"(e(p(s(m(i,inp().split())").hexdigest() == "d61f448980322eb85402b196cb190fd9c8b061bc28d944fb1e8704f5062b5623"
((((((((((((((((((((e(p(s(m(i,inp().split()))))))))))))))))))))))))
Закодированная 6 раз b85 строка:
b'Q8;r~Yf5-FL^V!vbvSEOGk!}?RZ&uRc2XpFD11mlPBckKC|^laR5UFmV@F75BVkc1X+2O&c29R<Qg<tSJY-QRd3j-XK{qW<YEULsNjM}hDP(RVZA(xjXfSa_X=impPds@kUq&q?RBTQ^b#!=HDNJNvT1F~$Qc@*+JAO?~Q8;a4eo|K>M=E}GWKcVCa5+<VeKku^MPXGWSvygDVpvK(RXkR6eJdkQN@Qm;P-c8=Yf?6RZ&^cASxr7-b3Hj`P-!qZcSK-ES$RWDb$cOUH6eCHRxo{TNmDaUJ90sHD^oC0Bw}NBL{M!jBUof#EoxC^b2cSWV@7>`Lw!s~BuQ9bby!V4BrrQrCOdXZLs2P6R7hocWPDb8K{7E-Z9Qc+PfTMnI8kCkY-me$B{@?{HE>=kNJVx|b4+g`cr#8wNj5)MPe)KiWl}{nbXP`nOm<UKJtb0NSZFv!WlCjsAy#j3RWVLpS9WAlA!2nvT0(tvepqQpCNft*NmM^eFknP|Omcd6Lr6kQJ#tTNQ)($$NMu=6RZ%%jZBSw=QYd6#cz$d~J4b9`Svf#fI9NVqJ}FIdOe=LlcQHVFS3X*KXGkkFOLsz3Vp>p8EmC1$WpQk2CR$}CGjvf#WKmF9JTiJyPdiCQPGf#$C_zzKV{1T0Y)M&VMr?aeD^xTgDLqYaQ8pu3M{PwwP-{y_eMn(4b}>&YH&$q4R#7WqVkCDla8g<&U}{TXYba4oQDk&gM=ellIZ|Uidu~!TX=^l1O>j0pPG4+JdqE*nY&&5ga79B^RbOgzQ6oM|HBnf3JaJb|a5qk3Swt&%LRcwkMqznWU^h@mYgJH9PIqr$P(prKXHr-rIb~rZVqrH?CMGC9R!uQ{Oh#y4H7!G5H!Dz4W<+RmQ+IxGYfO7fQ+Ht~M>scHMNv0MStMyxK1_08Lqkh`XE}CdQ$bosM<z5$CPykvcu7xbUPv%uBWyf<NI_6kNm@p4d1_WtBuhh0U{hmOQDb_2O-pk-L{me2S3X8dS!Go%Q6_F>OiDLtI9Nh;M|WXLBWqDIN;7_Ods8@AMpk7_G9i0NDMDX4OMYNcD?)TrB}-5!KY3A6WI|S6LU=1AWJWz}IcZjKcSTK6I6FOTPHuBMJ!5xjJW^L^c|v?-eL+}tS575sIAVBhKR#MGLNI+uD>OY|P$@ibOjk~GeQ;uUY#~EPVI)37PHK8yBV&3<X<<||NqS35cRp%PL{2<VUSwW<eNIj-J#<J;K0jwqSVuB<R77A}cPd1CGgEy}Wg}xkP(fxPWKK&-V`flfIAJ_eV?B96R5EWcQ&TfpP;pL6dU|7GGFDe6MQvhHUr;SEIcQTMM|gc%ML=>xWOHIOW@S!1LuOb<K3-5#HE%{WReMlZc2{v_K`>T)Mr<@nd?Z+RR4{aNO;RCuVpU6iabZYTJ~=p7N_;zhBT^whO>jd|O-?IDXkIlYOmuuSBUwC9N<C3%Hf?52KzB$aOEfe-XJR#EBwAB`P;5C<Q+9eaMlEY(PE1f@D{@6}OF~~+M@}VSPDgJ|Ge~v'
Жду плоды вашей фантазии :)
Само-корректирующая программа? Даже во времена оны, когда компьютеры были скудными, и вовсю баловались с программами, которые печатают себя сами, на это забили.
Решением является доверенный Запускатель у Клиента, про который известна его целостность. Причем эта целостность обеспечивается внесистемными средствами: прошивка в Биос, проверка целостности ОЗУ (за счёт избыточных битов, радиационно-устойчивая память).
Такой Запускатель проверит целостность Складывателя при запуске и сообщит Клиенту о проблемах с кодом. Ничего лучше контрольных сумм пока не придумали.
UPD. По опыту работы с авиацией, как гражданской, так и военной.
Обновление ПО - это вот прям больное-пребольное место. В военной авиации пошли на принцип: работает, и ничего не трогай. Как на заводе залили, так и будет стоять. Любая правка ПО эквивалентна созданию новой модификации, и обновляется вся система целиком. Модели присваивается новый индекс, проводятся государственные испытания, затем борта при плановом обслуживании проходят модернизацию. Никаких "самопроверяющих" программ и обновлений кусками.
В гражданской авиации оно чуть попроще. Можно делать обновления для отдельных модулей без нового индекса и новой модификации всей линейки, но! на каждое обновление пишется просто Эверест бумаг. Описание изменений, обоснование необходимости изменений, обоснование безопасности изменений, результаты юнит тестов, результаты модульных тестов, результаты интеграционных тестов... Бинарники заливаются только с доверенных носителей с обязательным логированием "кто, что, когда" и обязательной проверкой криптографической контрольной суммы.
Вообще в системах критических по надёжности никаких искусственных интеллектов и само-исправлений ошибок. Только доказанные алгоритмы, только валидированные бинарники, только хардкор. Я с ужасом ожидаю те времена, когда конкурентная борьба вынудит системщиков ставить на борт "искусственный интеллект", который работает хрен знает как и приходит к хрен знает каким выводам.
Можно проверить байткод программы, но её тогда нужно запускать на определенной версии python.
def main():
a, b = map(int, input().split())
print(a + b)
import zlib
import sys
pcrc=None
if sys.version_info[:2]==(3,10):
pcrc = 1147345271
if sys.version_info[:2]==(3,12):
pcrc = 2947152858
crc = zlib.crc32(main.__code__.co_code)
assert crc == pcrc, f"код поврежден: {crc} != {pcrc}"
main()
Защита байткода выглядит красиво, но сама защита не защищенна.
А вот так можно защитить и защиту тоже:
import zlib
with open(__file__,'rb') as f:
thisfile = f.read()
crc = zlib.crc32(thisfile[:-9]).to_bytes(4,'big').hex()
if crc != thisfile[-9:-1].decode():
raise Exception(f"код поврежден: {crc}")
else:
del thisfile
a, b = map(int, input().split())
print(a + b)
0xa9fd28f7
Тут есть что оптимизировать, но сам
Я понимаю, что, вероятно, это не совсем искомый способ, но формально...
from hashlib import sha256
import requests
r = requests.get('https://raw.githubusercontent.com/im-shung/Problem-Solving/1d1565cd9d13997afd6fa5099210a2e695053442/BOJ/%EA%B8%B0%ED%83%80/1000_A%2BB.py')
exec(r.content if sha256(r.content).hexdigest() == "cb7af1a82698865433dfe854895ff32efbb7c992b7a438703187dbdabf384182" else "")
*Я не являюсь владельцем кода на гитхаб, но предполагаю, что Программист использует «доверенное» расположение файла
По сути это просто вариация «простейшей защиты» от dofi4ka
В ответ на последнюю (на текущий момент) версию UPD2 от dofi4ka - я решил пойти с другой стороны и сломать программу простым переопределением методов.
Самым коротким будет переопределение sum (название из трёх букв, всего один параметр на входе, вернуть нужно число) т.е. добавив "def sum(a):return 0" + перевод строки в самое начало скрипта. Что даёт чуть меньшую границу для минимального количества искажений в 20 символов.
Так же можно поступить и с методом map, заменив его на что-то вроде: def map(*a):return{} - в таком случае мы получаем 21 символ.
П.с. извиняюсь, что не в комментариях к ответу - карма не позволяет(((
Изначально хотел просто проверить возможность перебора файла посимвольно на предмет "разрешённых" или "запрещённых" символов. В результате получилось нечто чуть более сложное. Ломается за две подмены (если следовать моей логике), но интересно посмотреть на одну) Концептуально, не сильно отличается от вариантов, что уже есть выше, разве что этот более "человекочитаемый"
is_ok = False
with open(__file__,'r') as f:
for l in f.read():
if l in {'a', 't', '4', ':', '\n', 'g', 'c', '_', 'd', 'i', '(', 'k', '=', 'n', 'e', "'", 's', '.', 'x', ')', '*', 'm', 'u', 'f', 'h', '0', 'b', 'r', '"', 'o', 'l', 'E', ' ', '1', ',', '{', '}', '\\'}: is_ok = not is_ok
elif l not in {'p', 'w', '+', 'F'}: raise Exception
a, b = map(int, input().split())
if is_ok: print(a + b)
В целом, его можно улучшить, но свою задумку я реализовал)
Работал в банке. Читая это всё был даже удивлён.
Первое, что сразу пришло на ум: программа не должна быть текстовым скриптом! Делайте .exe! Исправляйте .exe потом, товарищи-хакеры, если кишка не тонка! Пример дан на Питоне. Там такое исправление превратится в кошмар, потому что "библиотеки поддержки" раз в сто больше самой программы. Попробуйте порыться в такой свалке.
Второе. Создаётся собственная библиотека шифрования с открытым ключом. У нас в банке такая библиотека писалась своим отделом безопасности. Программа-дешифровщик и ключ отправляется заказчику. Он расшифровывает .exe и пробует работать. Битый файл работать не будет. Ключи периодически меняются, отправляются заказчику НЕ вместе с дешифратором или зашифрованной программой, а отдельными путями. Ничто не мешает в этом случае и продублировать целостность с помощью CRC.