Сразу оговорюсь, что обычно я не занимаюсь компьютерной безопасностью и не интересуюсь, а занимаюсь алгоритмами и структурами данных - в прикладном применении это оптимизация быстродействия, высокопроизводительные вычисления типа CUDA, AVX512, многопоточность, что применяется например для майнеров криптовалют. Так я влез в криптоанализ, ибо области, получается, соприкасаются. Был у меня заказ от человека, который хотел очень быстро на видеокартах перемножать 256-битные числа в 512-битные произведения. Я конечно сделал как он хотел, но вот пришла идея: так а зачем перемножать бесчисленное количество чисел, если в принципе можно разложить на множители 512-битное число имея текущие технологии? Об этом дальше и речь.
Дано:
512-битное число
Нету P=NP, но существуют быстрые BSAT солверы типа kissat и cryptominisat5 (второй устанавливается на Ubuntu типа
sudo apt install cryptominisat libcryptominisat5-dev
, первый выигрывает соревнования, но ставится из исходников)Linux машина (далее инструкции будут для Ubuntu 24.04)
Программа CBMC (устанавливается
sudo apt install cbmc
)Допустим вы собрали из исходников и установили kissat, доступный т.о. как команда по имени
Надо:
Найти два 256-битных числа, чьё произведение (т.е. если их перемножить в длинной арифметике) дают заданное 512-битное число.
Решение.
Во-первых, как мы знаем например из университетского курса криптологии (или даже школьных олимпиадных алгоритмов), не существует полиномиального (т.е., грубо говоря, быстрого) алгоритма факторизации больших чисел (т.е. разложения числа на множители). На этом держится криптография (например, RSA) и может быть некоторые криптовалюты, мало ли.
Однако, факторизация в свою очередь держится на предположении что P != NP, т.е. якобы не существует быстрого алгоритма решения задачи выполнимости булевых формул (над чем я сейчас и работаю, поэтому взялась такая статья). Дело в том что любую программу, в том числе и программу произведения двух длинных чисел, можно переписать как булеву формулу в КНФ (конъюнктивной нормальной форме), распределив по булевым переменным биты состояния программы в каждый момент времени от
0, ..., t-1, t, t+1, ..., T
. Что успешно и делает вышеназванная CBMC для программ на языке C.Таким образом, если как-то быстро решить выполнимость булевой формулы (эвристиками?), то и большое число будет разложено на искомые делители. Что успешно и делает kissat за 3.5 часа работы на старом и пыльном ноутбучном процессоре 13th Gen Intel(R) Core(TM) i9-13980HX.
CBMC далее по решению булевой формулы строит контрпример. Т.е. в C-программе мы утверждаем, будто разложить нельзя, а CBMC в контрпримере нам говорит "нет, и вот как" - далее следуют присвоенные значения каждой недетерменированной переменной программы. Это и есть наши делители.
Подробности.
Вот программа на C, файл mul256cbmc.c
, которая перемножает два недетерменированных 256-битных числа в искомое 512-битное число (захардкожено - можете сменить на что вам угодно).
#include <stdint.h>
#include <string.h>
#include <assert.h>
/* CBMC nondeterministic generator: “nondet_*” is recognized and each call yields
an unconstrained fresh value. */
unsigned long long nondet_unsigned_long_long(void);
/* 256x256 -> 512 (little-endian 64-bit limbs) */
static void mul256_u64le(const uint64_t a[4], const uint64_t b[4], uint64_t r[8]) {
for (int i = 0; i < 8; ++i) {
r[i] = 0;
}
#if defined(__SIZEOF_INT128__)
for (size_t i = 0; i < 4; ++i) {
__uint128_t carry = 0;
for (size_t j = 0; j < 4; ++j) {
__uint128_t t = (__uint128_t)a[i] * (__uint128_t)b[j];
t += (__uint128_t)r[i + j];
t += carry;
r[i + j] = (uint64_t)t;
carry = t >> 64;
}
/* bounded ripple: at most 4 remaining limbs */
for (size_t step = 0, k = i + 4; step < 4 && carry != 0; ++step, ++k) {
__uint128_t t = (__uint128_t)r[k] + carry;
r[k] = (uint64_t)t;
carry = t >> 64;
}
}
#else
# error "This file expects unsigned __int128; if unavailable, ask for the 64-bit fallback."
#endif
}
static int eq_u512(const uint64_t a[8], const uint64_t b[8]) {
for (int i = 0; i < 8; ++i) if (a[i] != b[i]) return 0;
return 1;
}
int main(void) {
/* Make p1, p2 nondeterministic */
uint64_t p1[4], p2[4];
for (int i = 0; i < 4; ++i) {
p1[i] = nondet_unsigned_long_long();
p2[i] = nondet_unsigned_long_long();
}
/* Expected is the fixed 512-bit product of:
secp256k1 prime (p_secp) *
P-256 prime (p_p256)
p_secp = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
p_p256 = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
*/
const uint64_t expected[8] = {
0x00000001000003d1ULL, 0xfffffc2f00000000ULL,
0xfffffffffffffffeULL, 0x000003cffffffc2eULL,
0xfffffffefffffc2fULL, 0x00000000ffffffffULL,
0x0000000000000000ULL, 0xffffffff00000001ULL
};
uint64_t prod[8];
mul256_u64le(p1, p2, prod);
/* FAIL IFF p1*p2 == expected */
__CPROVER_assert(!eq_u512(prod, expected), "p1*p2 must not equal expected");
return 0;
}
Вот файл cbmc-kissat.sh
скрипт-адаптер чтобы CBMC решал булеву формулу топовым солвером kissat. После сохранения файла в текущую директорию, не забудьте выполнить `chmod +x cbmc-kissat.sh` чтобы ОС могла выполнять этот файл.
#!/usr/bin/env bash
set -euo pipefail
# Config
solver=${KISSAT_BIN:-kissat}
ts=$(date +%Y%m%d-%H%M%S)
user="${USER:-u}"
# Figure out CNF input mode (arg path vs stdin)
if [[ $# -ge 1 && -r "$1" ]]; then
cnf_in="$1"
base="$(basename "$cnf_in")"; base="${base%.cnf}"
src="arg"
else
base="stdin"
src="stdin"
fi
# Workdir for artifacts (readable names)
workdir="${CBMC_KISSAT_WORKDIR:-/tmp}/${user}/cbmc-kissat/${base}_${ts}_$$"
mkdir -p "$workdir"
cnf_copy="$workdir/formula.cnf"
raw_log="$workdir/kissat.stderr" # kissat stats
sol_out="$workdir/kissat.stdout" # kissat model/output (what CBMC needs)
# Prepare CNF file for kissat and for your inspection
if [[ "$src" == "arg" ]]; then
cp -f "$cnf_in" "$cnf_copy"
else
cat > "$cnf_copy"
fi
# Run kissat, forward stdout DIRECTLY to CBMC, and tee both streams to files.
# Important: CBMC reads ONLY stdout; do not print anything else to stdout here.
# kissat prints "s SATISFIABLE"/"s UNSATISFIABLE" and "v ..." model lines to stdout.
# Stats go to stderr; we capture them separately.
"$solver" "$cnf_copy" > >(tee "$sol_out") 2> >(tee "$raw_log" >&2)
rc=$?
# Helpful pointers (stderr only; won't confuse CBMC)
{
echo "[cbmc-kissat] workdir: $workdir"
echo "[cbmc-kissat] cnf: $cnf_copy"
echo "[cbmc-kissat] stdout: $sol_out (tail -f to watch model)"
echo "[cbmc-kissat] stderr: $raw_log (stats)"
} >&2
# Propagate kissat's exit code (10=SAT, 20=UNSAT)
exit $rc
Далее запускаем CBMC следующим образом, считая что вы уже установили CBMC и kissat:
cbmc mul256cbmc.c --c11 --64 \
--no-standard-checks \
--no-built-in-assertions \
--drop-unused-functions \
--reachability-slice \
--slice-formula \
--property main.assertion.1 \
--unwind 9 --trace --external-sat-solver ./cbmc-kissat.sh | tee cbmc-trace.txt
Получаем из этого вывод на экран и в файл cbmc-trace.txt
.
Проверяем что kissat начал решать задачу:
ls /tmp/$USER/cbmc-kissat/
# находим вглубине директорий файл kissat.stdout, и далее например
tail -f /tmp/serge/cbmc-kissat/external-sat30893.2AZwWy_20250818-133907_30909/kissat.stdout
Далее ждём несколько часов, в зависимости от производительности вашего компьютера в одном ядре процессора (да, к сожалению kissat - однопоточный).
Далее kissat завершается чем-то типа такого:
c ---- [ profiling ] ---------------------------------------------------------
c
c 11334.31 91.22 % search
c 5683.98 45.75 % stable
c 5650.32 45.48 % focused
c 1089.66 8.77 % simplify
c 1059.92 8.53 % probe
c 896.59 7.22 % vivify
c 110.43 0.89 % sweep
c 45.68 0.37 % reduce
c 21.33 0.17 % substitute
c 18.08 0.15 % factor
c 15.42 0.12 % eliminate
c 15.03 0.12 % walking
c 8.31 0.07 % transitive
c 3.41 0.03 % congruence
c 2.89 0.02 % subsume
c 1.76 0.01 % backbone
c 0.99 0.01 % preprocess
c 0.26 0.00 % fastel
c 0.09 0.00 % parse
c 0.05 0.00 % lucky
c 0.01 0.00 % extend
c =============================================
c 12425.11 100.00 % total
c
c ---- [ statistics ] --------------------------------------------------------
c
c chronological: 798548 1 % conflicts
c conflicts: 70322440 5659.70 per second
c congruent: 14702 7 % variables
c decisions: 503054267 7.15 per conflict
c eliminated: 77788 38 % variables
c factored: 660 0 % variables
c fast_eliminated: 588 1 % eliminated
c fast_strengthened: 175 0 % per strengthened
c fast_subsumed: 2577 1 % per subsumed
c iterations: 79 0 % variables
c propagations: 115164469837 9268685 per second
c reductions: 2232 31506 interval
c reordered: 118 595953 interval
c rephased: 109 645160 interval
c restarts: 2193054 32 interval
c strengthened: 332326 0 % checks
c substituted: 15998 8 % variables
c subsumed: 206142 0 % checks
c switched: 178 395070 interval
c vivified: 6679374 18 % checks
c walks: 36 1953401 interval
c
c ---- [ glue usage ] --------------------------------------------------------
c
c focused glue 1 used 243800208 clauses 71.95% accumulated 71.95% tier1
c focused glue 2 used 29184284 clauses 8.61% accumulated 80.56%
c focused glue 3 used 18037928 clauses 5.32% accumulated 85.89%
c focused glue 4 used 12070423 clauses 3.56% accumulated 89.45%
c focused glue 5 used 8128688 clauses 2.40% accumulated 91.85% tier2
c
c stable glue 1 used 315710656 clauses 59.86% accumulated 59.86% tier1
c stable glue 2 used 46150963 clauses 8.75% accumulated 68.61%
c stable glue 3-6 used 99177292 clauses 18.81% accumulated 87.42%
c stable glue 7 used 9677189 clauses 1.83% accumulated 89.25%
c stable glue 8 used 7056415 clauses 1.34% accumulated 90.59% tier2
c
c ---- [ resources ] ---------------------------------------------------------
c
c maximum-resident-set-size: 248938496 bytes 237 MB
c process-time: 3h 27m 5s 12425.11 seconds
c
c ---- [ shutting down ] -----------------------------------------------------
c
c exit 10
И тогда CBMC выводит долгожеланную трассировку.
Далее с помощью этого Python скрипта (файл parse_cbmc_trace.py
) парсим трассировку и проверяем:
#!/usr/bin/env python3
import sys, re
def parse_cbmc_trace(text: str):
"""
Parse CBMC plain trace lines like:
p1[0l]=18446744073709551615ul (...)
p2[3l]=0xFFFFFFFFFFFFFFFF (...)
Returns two lists of limbs (little-endian), each index -> 64-bit limb.
"""
# Collect latest assignments seen for each limb (in case of reassignments)
p = {1: {}, 2: {}}
# Accept index with or without trailing 'l' and numbers in dec or hex
pat = re.compile(r'\bp([12])\[(\d+)l?\]\s*=\s*(0x[0-9a-fA-F]+|\d+)', re.I)
for line in text.splitlines():
m = pat.search(line)
if not m:
continue
which = int(m.group(1)) # 1 or 2
idx = int(m.group(2)) # limb index
val_s = m.group(3) # number literal (dec or hex)
val = int(val_s, 0) # auto base
p[which][idx] = val & ((1 << 64) - 1)
def limbs_to_list(d):
if not d:
return [0, 0, 0, 0]
max_idx = max(d.keys())
arr = [0] * (max_idx + 1)
for i, v in d.items():
arr[i] = v
return arr
p1_limbs = limbs_to_list(p[1])
p2_limbs = limbs_to_list(p[2])
return p1_limbs, p2_limbs
def limbs_to_int(limbs):
n = 0
for i, limb in enumerate(limbs):
n |= (int(limb) & ((1 << 64) - 1)) << (64 * i)
return n
def fmt_hex(n: int) -> str:
if n == 0:
return "0x0"
nbytes = (n.bit_length() + 7) // 8
return "0x" + n.to_bytes(nbytes, "big").hex()
def main():
data = sys.stdin.read()
if not data:
print("Usage: cbmc ... --trace | python3 parse_cbmc_trace.py", file=sys.stderr)
sys.exit(2)
p1_limbs, p2_limbs = parse_cbmc_trace(data)
p1 = limbs_to_int(p1_limbs)
p2 = limbs_to_int(p2_limbs)
prod = p1 * p2
print("p1 =", fmt_hex(p1))
print("p2 =", fmt_hex(p2))
print("p1 * p2 =", fmt_hex(prod))
if __name__ == "__main__":
main()
Запускаем его следующим образом:
python3 parse_cbmc_trace.py < cbmc-trace.txt
И получаем что-то типа такой картинки:

Думаю за какую недельку вычислений kissat возьмёт и используемый сейчас широко RSA-2048, а если не возьмёт, то ждём моих следующих статей, где я собираюсь рассказать про:
Синтез микропрограмм на основе решения булевых формул. Смысл в том, что решив булеву формулу, можно создать программу на примитивном языке (8 команд и однобитные регистры), но всё ещё полном по Тьюрингу. Преимущество такого подхода состоит в том, что алгоритм сразу получается доказанным как теорема, и что можно синтезировать программы, до которых не додумались человеческие умы. Вот только в 192 ГБ памяти на моём ноутбуке поместится разве что формула для микропрограммы из всего лишь нескольких тысяч шагов.
Быстрые решения булевых формул - тут всё ясно должно быть теоретически.
Комментарии (41)
SadOcean
18.08.2025 19:47Ничего не понятно, но если звучит так, как звучит, это весьма сильно (и может даже довольно опасно)
anoldman25
18.08.2025 19:47Дело в том что любую программу, в том числе и программу произведения двух длинных чисел, можно переписать как булеву формулу в КНФ (конъюнктивной нормальной форме)
Мне кажется, что эта формула ( КНФ ) будет очень длинной. По сути дела она будет содержать все возможные ответы.
rserge Автор
18.08.2025 19:47Да, формулы получаются немаленькие, но некоторые эвристики их берут. В данном случае, для RSA-512 получается формула из 202657 переменных и 1127580 выражений.
brugger
18.08.2025 19:47О, это похоже на нормальность числа Пи )) В нём, якобы, обязательно будет искомая комбинация символов, твоя дата рождения, твой номер телефона, все сочинения Пушкина и тд ) Такое вот следствие из гипотезы о нормальности числа π, правдоподобно, но без математического доказательства(пока что)
snuk182
18.08.2025 19:47Intel i9 13го поколения полномощный - старый и пыльный?
muxa_ru
18.08.2025 19:47Полагаю, что статья написана не для маленького пика посещаемости в первые часы, а на годы вперёд. Это разговор с вечностью!
Спустя 10-15 лет, Ваша придирка будет выглядет смехотворно.
rserge Автор
18.08.2025 19:47Так это ноутбучный проц, только 36 МБ кеша, а пыльный потому что ноут давно в чистку не сдавал. В итоге частота далека от заявленных 5.6 ГГц, а ноут с Ryzen 9955HX3D работает в разы быстрее на том же одном ядре, особенно когда данные в кеш вмещаются.
m0tral
18.08.2025 19:47Какая разница, 13е поколение когда старым стало, вам, извините, 20лет? что пару лет - это сравнимо старости?
Okeu
18.08.2025 19:47Так это ноутбучный проц, только 36 МБ кеша
у 13900K тоже 36МБ. Да и разница в производительности у них не прям чтоб уж пропасть)
GidraVydra
18.08.2025 19:47https://yurichev.com/news/20220210_RSA/
3 года назад RSA 512 за 4 дня ломали на Ryzen 5 3600@64 GB, прямой факторизацией числа на CADO-NFS. Тут же машина с несколько раз производительнее. Да и сама программа факторизации взята более эффективная.
Не вижу принципиальной разницы, учитывая, что сложность факторизации двух произвольно взятых 512-битных чисел может на порядок(ки) различаться.
rserge Автор
18.08.2025 19:47Для начала разберёмся, какой алгоритм использует CADO-NFS: https://chatgpt.com/share/68a3e312-c7c8-8000-abc5-9f7f1cf7bb44 . В кратце, числовое решето.
Этот алгоритм легко распределяется на много потоков и/или машин, поэтому 6-ядерный десктопный Ryzen 3600 должен быть помощнее, чем одноядерный ноутбучный 13980HX.
В моей статье используется совершенно другой алгоритм, а именно выполнимость булевых формул.
Эффективность факторизации трудно оценивать. Тот алгоритм работает за его алгоритмическую сложность, и нахождения числа - вероятностно, как я понимаю. Здесь же в статье используется алгоритм, чья алгоритмическая сложность в худшем случае - невообразимое число даже на квантовых компьютерах. Но эвристики делают его практичным.
Да, для разных чисел может существенно отличаться время работы солвера.
GidraVydra
18.08.2025 19:47Да, для разных чисел может существенно отличаться время работы солвера.
Это-то и вызывает вопросы. У вас и 512-битное число, и оба числа его разложения, таковы, что почти все старшие разряды либо f либо 0, причем большими блоками подряд. Сравните это с числами из моей ссылки - они намного ближе к нормальным (в математическом смысле). Поэтому разница во времени работы солвера скорее всего обусловлена разницей в нормальности выбраных случайных чисел, а не алгоритмическим преимуществом
tempart
18.08.2025 19:47чем одноядерный ноутбучный 13980HX
Откуда у вас такая интересная спецификация процессора? У него 24 ядра (и 32 потока, что немаловажно)
GidraVydra
18.08.2025 19:47ТС почему-то считает, что kissat не поддерживает multithread (или как минимум multicore). Я лично в этом сомневаюсь, но чтобы это проверить, нужен линуксоид.
krote
18.08.2025 19:47>Думаю за какую недельку вычислений kissat возьмёт и используемый сейчас широко RSA-2048
как бы 512 бит от 2048 отличается далеко не в 4 раза по сложности!rserge Автор
18.08.2025 19:47Да, нормально так отличается формула - в 10-15 раз больше переменных и выражений для RSA-2048:
RSA-2048
tbl
18.08.2025 19:47Если RSA-512 разламывается за 3,5 часа, то RSA-2048 будет разламываться на той же машине за ~1 500 000 лет, а RSA-4096 - за ~20 000 000 000 000 лет
Более точные оценки скажу, если предоставите время хотя бы для 3-4 значений битности менее 512
rserge Автор
18.08.2025 19:47Смотрите ответ выше - формула для RSA-512 стала в 10-15 раз больше чем для RSA-512. Конечно, это вопрос алгоритма решения выполнимости булевых формул - с текущим экспоненциальным алгоритмом и ускоряющими эвристиками, мы никогда не знаем, займёт это неделю или больше чем всё время Вселенной. Плюс для разных чисел сложность может существенно отличаться.
ky0
18.08.2025 19:47За разложение на множители 2048-битного числа полагается 200 килобаксов, не проходите мимо и обязательно напишите потом статью :)
rserge Автор
18.08.2025 19:47спасибо Вам большое что сообщили, я не знал. Сейчас прямо вычисляю, но наверное другое число 2048-битное.
old_bear
18.08.2025 19:47А все остальные, кто дочитал до этого коммента теперь вычисляют именно то число по вашему методу... :D
rserge Автор
18.08.2025 19:47вот только вроде как конкурс закрыли в 2007-м году: https://en.wikipedia.org/wiki/RSA_numbers . Или есть теперь другие конкурсы по факторизации?
old_bear
18.08.2025 19:47Ну так может они как я - пошли по прямой ссылке из предыдущего коммента, а про то что конкурс уже закрыли не доскролили наверх страницы.
P.S. Если что, я не вычисляю.
MetodX
18.08.2025 19:47Зачем тебе это...?
Так, ради публикации или признания?
За такие решения будет наблюдение.
И кто сказал что не существует полиномиального алгоритма факторизации больших чисел...
rserge Автор
18.08.2025 19:47И признание, и публикация конечно же важны. К наблюдению мне не привыкать. Вот именно что для дальнейшего развития ИИ очень важны быстрые алгоритмы выполнимости булевых формул, это добавило бы логику к ИИ, который сейчас (чисто нейронные сети) - с большего статистический и решать задачи может только в одно-два действия. Факторизация - это лишь способ продемонстрировать теперешние возможности SAT солверов. Главное всё-таки создать ИИ с логикой, а не чисто болтолога. Тогда автоматическое доказательство теорем позволит ИИ делать изобретения - типа межзвёздного двигателя.
MetodX
18.08.2025 19:47Кидай задачу, помогу.
Но не для тщеславия твоего или чего-то...
Если есть что заработать на этом почестному, помогу с математикой и возможно с алгоритмом тоже.
@MetodXKZ
rserge Автор
18.08.2025 19:47да тут речь не о тщеславии, а чтоб элементарно брали на работу, а то меня уже лет 15 толком никуда не берут, типа я ничего не умею так выставляют. Приходишь на собеседование, а там так загнобят, что хочется то ли плакать, то ли лица бить...
incotus
18.08.2025 19:47есть сильное подозрение, что любой специализированный алгоритм типа elliptic curve или GNFS на этом же примере и оборудовании отработает гораздо быстрее.
rserge Автор
18.08.2025 19:47посмотрите выше в комментариях - там решето уже обсуждалось (GNFS), вроде получилось медленнее для 512 бит, а 876 бит им вообще не взяли факторизацию.
incotus
18.08.2025 19:47именно на вашем примере, у вас p1 маленькое по модулю, поэтому elliptic curve его должно найти за секунды. было бы странно если бы специализированный алгоритм проигрывал general-purpose алгоритму
Jsasha
18.08.2025 19:47Подскажите, какие есть способы превратить алгоритм в булеву формулу? Или она только возникает в неявном виде внутри cbms, и никак её не вытащить?
VladimirKonkin
18.08.2025 19:47Так она сохранятся в
"$workdir/formula.cnf". И там её можно посмотреть
cnf - значит КНФ дальше число дизъюнктов и аргументов, дальше дизъюнкты ("0" - конец дизъюнкта, "-" - инверсия аргумента).
eigrad
18.08.2025 19:47Когда дело касается криптографии необходимо консультироваться со специалистом. Описанный алгоритм имеет мало общего с RSA, а разложение случайного 512-битного числа на простые множители будет проходить меньше чем за секунду на одном ядре любого современного процессора при использовании тривиальных алгоритмов из "школьного" курса.
primepix
18.08.2025 19:47Заголовок не совсем точно отражает содержание. Автор факторизует произведение двух известных простых чисел (secp256k1 prime и P-256 prime), а не настоящий RSA-ключ. Это существенно разные задачи по сложности.
Прогноз про RSA-2048 ("за какую недельку вычислений kissat возьмёт") не учитывает экспоненциальный рост сложности факторизации. Переход от 512 к 2048 битам означает увеличение времени вычислений в миллиарды раз, а не в 4 раза. SAT-формула для RSA-2048 будет содержать миллионы переменных.
Также есть неточности в терминологии - "ломание RSA" в криптографии обычно означает нахождение способа расшифровки без приватного ключа, а не просто факторизацию числа. Автор смешивает теоретическую сложность задач с практическими возможностями конкретных алгоритмов.
pharrell
Ничего не понял, но звучит легендарно!