Logo
Overview

Z3 for Reverse Engineering: Modeling, Debugging and Solving

December 27, 2025
6 min read

1. Basic Example

z3_basic.c
// cc -O2 -std=c11 -Wall -Wextra -o z3_basic z3_basic.c
#include <stdint.h>
#include <stdio.h>
#include <string.h>
static uint8_t rol8(uint8_t v, unsigned r) {
return (uint8_t)((v << r) | (v >> (8 - r)));
}
int main(void) {
char buf[128] = {0};
if (!fgets(buf, sizeof(buf), stdin)) {
return 1;
}
size_t len = strcspn(buf, "\n");
if (len != 24) {
puts("no");
return 1;
}
uint8_t x[24];
memcpy(x, buf, 24);
for (int i = 0; i < 24; i++) {
if (x[i] < 0x20 || x[i] > 0x7E) {
puts("no");
return 1;
}
}
uint32_t v = 0x13579BDFu;
for (int i = 0; i < 24; i++) {
v = (v + ((uint32_t)(x[i] ^ 0x5Au) * (uint32_t)(i + 3))) ^ (v >> 7);
v = v + 0x1234u;
}
if ((uint8_t)(x[3] + x[8] - x[15]) != 0xA4u) {
puts("no");
return 1;
}
if ((uint8_t)(rol8(x[10], 3) ^ x[1]) != 0x2Cu) {
puts("no");
return 1;
}
if (v != 0x135A0D76u) {
puts("no");
return 1;
}
puts("ok");
return 0;
}

Formula

uint32_t v = 0x13579BDF;
for (int i = 0; i < 24; i++) {
v = (v + (x[i] ^ 0x5A) * (i + 3)) ^ (v >> 7);
v = (v + 0x1234) & 0xFFFFFFFF;
}
if ((x[3] + x[8] - x[15]) & 0xFF != 0xA4) return 0;
if (((x[10] << 3) | (x[10] >> 5)) ^ x[1] != 0x2C) return 0;
if (v != 0x135A0D76) return 0;

위 Formula는 위 C 코드와 동일하게 맞춘 버전이다. 최종 상수는 Z3로 뽑은 v_target을 그대로 사용한다.

이런 유형은 식을 제대로 뽑으면 Z3가 대부분 풀어준다.

이걸 그대로 Z3로 옮기면 된다. 추가로 타입과 오버플로우에 주의하자.

  • uint8_t는 BitVec(8)
  • uint32_t는 BitVec(32)
  • & 0xFFFFFFFF는 32비트 오버플로우 유지
  • x[i]는 연산 전에 32비트로 확장

Basic 예시를 수식으로 정리하면 아래처럼 된다.

v0=vinitv_0 = v_{\text{init}} vi+1=(vi+(xic)(i+3))(vi7)v'_{i+1} = \left(v_i + (x_i \oplus c)(i+3)\right) \oplus (v_i \gg 7) vi+1=vi+1+d(mod232)v_{i+1} = v'_{i+1} + d \pmod{2^{32}}

여기서 vinitv_{\text{init}}0x13579BDF, cc0x5A, dd0x1234다.

추가 조건은 아래와 같이 정리된다.

(x3+x8x15)mod256=a(x_3 + x_8 - x_{15}) \bmod 256 = a rol8(x10,3)x1=b\text{rol}_8(x_{10}, 3) \oplus x_1 = b v24=tv_{24} = t

aa0xA4, bb0x2C, tt0x135A0D76이다.

그대로 Z3로 구현하자.

부분적으로 선형처럼 보이지만 전체는 비선형이다. XOR/ROL은 GF(2) 기준에서 선형(또는 affine)으로 볼 수 있지만, ADDITION은 CARRY 때문에 비선형이다. 따라서 선형 해법만으로는 풀기 어렵고 비트벡터 SMT가 필요하다.

Modeling

from z3 import *
def rol8(v, r):
return ((v << r) | LShR(v, 8 - r)) & 0xFF
x = [BitVec(f'x{i}', 8) for i in range(24)]
def build_base_solver():
s = Solver()
# Printable로만 제한 (탐색 축소)
for i in range(24):
s.add(x[i] >= 0x20)
s.add(x[i] <= 0x7E)
v = BitVecVal(0x13579BDF, 32)
for i in range(24):
xi = ZeroExt(24, x[i])
v = (v + (xi ^ 0x5A) * (i + 3)) ^ LShR(v, 7)
v = (v + 0x1234) & 0xFFFFFFFF
return s, v
# 개별 조건 정의
sum_cond = (ZeroExt(24, x[3]) + ZeroExt(24, x[8]) - ZeroExt(24, x[15])) & 0xFF == 0xA4
rot_cond = (rol8(x[10], 3) ^ x[1]) == 0x2C
# 디버깅용: 조건 하나씩 체크
checks = [
("range", And([x[i] >= 0x20 for i in range(24)] + [x[i] <= 0x7E for i in range(24)])),
("sum", sum_cond),
("rot", rot_cond),
]
for name, cond in checks:
s_test, _ = build_base_solver()
s_test.add(cond)
print(name, s_test.check())
# sum + rot만 만족하는 해에서 v 값을 뽑는다.
s_mid, v_mid = build_base_solver()
s_mid.add(sum_cond)
s_mid.add(rot_cond)
if s_mid.check() == sat:
m_mid = s_mid.model()
v_target = m_mid.eval(v_mid).as_long()
print("v_target =", hex(v_target))
s_full, v_full = build_base_solver()
s_full.add(sum_cond)
s_full.add(rot_cond)
s_full.add(v_full == v_target)
if s_full.check() == sat:
m = s_full.model()
ans = bytes([m[x[i]].as_long() for i in range(24)])
print(ans)
else:
print("unsat")

위 코드에서 출력되는 v_target이 C 문제의 최종 상수다.

z3_basic

  • ZeroExt(24, x[i])를 써서 8비트 \rightarrow 32비트 확장
  • & 0xFFFFFFFF로 32비트 오버플로우 유지
  • Python int와 Z3 BitVec를 구분하자 \rightarrow BitVecVal

2. Advanced Example

  • (table + 혼합 라운드 + 누적 체크)

실제로 자주 나오는 테이블 기반 혼합 + 누적 체크 유형이다

입력: 32바이트
라운드:
1) 256바이트 테이블 T로 입력 바이트를 치환
2) 32비트 누적값 v 갱신 (xor/add/rol 혼합)
3) 중간마다 부분 체크
최종: v == 0x7A3C91F2

수식으로 정리하면 이런 형태다.

위 조건은

  1. 테이블 치환 때문에 역산이 어렵다.
  2. 누적값에 rol/xor/add가 섞여 있어 식이 폭발한다.
  3. 중간 체크가 여러 개여서 부분적으로 모순이 생기기 쉽다.

여기서도 선형/비선형이 섞인다. XOR/ROL은 선형에 가깝지만, ADDITION은 CARRY 때문에 비선형이고 테이블 치환은 거의 완전 비선형이다. 이 조합 때문에 단순 선형 해법이 잘 안 먹힌다.

Formula

차근차근 짜보자.

초기값은 0xC0FFEE00 같은 상수라고 가정한다.

vi+1=rol32(vi,5)+(T[xi](ic))+(vi3)v_{i+1} = \text{rol}_{32}(v_i, 5) + (T[x_i] \oplus (i \cdot c)) + (v_i \gg 3)

여기서 cc0x13 같은 라운드 상수다.

중간 체크

(T[x7]+T[x19]T[x23])mod256=m(T[x_7] + T[x_{19}] - T[x_{23}]) \bmod 256 = m

mm은 상수 값

Modeling

from z3 import *
def rol32(v, r):
return ((v << r) | LShR(v, 32 - r)) & 0xFFFFFFFF
# 테이블 예시
T = [i ^ 0xA5 for i in range(256)]
x = [BitVec(f'x{i}', 8) for i in range(32)]
s = Solver()
# Printable
for i in range(32):
s.add(x[i] >= 0x20)
s.add(x[i] <= 0x7E)
v = BitVecVal(0xC0FFEE00, 32)
for i in range(32):
# T[x[i]]를 만들기 위한 식
tval = Sum([If(x[i] == k, BitVecVal(T[k], 8), 0) for k in range(256)])
t32 = ZeroExt(24, tval)
v = rol32(v, 5) + (t32 ^ BitVecVal(i * 0x13, 32)) + LShR(v, 3)
v = v & 0xFFFFFFFF
# 중간 체크
def Tlookup(b):
return Sum([If(b == k, BitVecVal(T[k], 8), 0) for k in range(256)])
s.add((Tlookup(x[7]) + Tlookup(x[19]) - Tlookup(x[23])) & 0xFF == 0xB6)
s.add((x[5] ^ x[11] ^ x[29]) == 0x2D)
s.add(((x[2] + x[9] + x[17]) & 0xFF) == 0x90)
# 최종 조건
s.add(v == 0x7A3C91F2)
if s.check() == sat:
m = s.model()
ans = bytes([m[x[i]].as_long() for i in range(32)])
print(ans)
> python3 solve2.py
b'r0&,f~D0v!p/zl"9GI\\CV86`xz `;|c.'
  • Table Optimization

위 방식은 If 256개를 합치는 형태라 느릴 수 있다. 보통은 아래 방법을 쓴다.

  • Z3 Array로 테이블을 만들고 Select 사용
Arr = Array('Arr', BitVecSort(8), BitVecSort(8))
arr_init = Arr
for k in range(256):
arr_init = Store(arr_init, BitVecVal(k, 8), BitVecVal(T[k], 8))
tval = Select(arr_init, x[i])

이렇게 바꾸면 속도가 개선된다.

3. Fault

자주 하는 실수를 모아보았다.

  1. signed/unsigned 혼동 char가 signed인지 unsigned인지 확인해야 한다. 보통 리버싱에서는 uint8_t로 보는 게 안전하다. signed 연산이면 SignExt가 필요하다.

  2. 연산 순서 (a + b) ^ ca + (b ^ c)는 완전히 다르다. 어셈블리 기준으로 그대로 따라야 한다.

  3. 8비트 연산 승격 C에서는 uint8_t도 연산 시 int로 승격된다. 즉 실제 바이너리는 32비트로 계산 후 다시 잘리는 경우가 많다. 이걸 놓치면 모델이 틀어진다.

4. Debugging

  1. 조건을 잘게 쪼개기
s.push()
s.add(조건1)
print(s.check())
s.pop()

이걸 반복하면 어느 조건에서 꼬였는지 나온다.

XOR/ROL만 남기면 선형처럼 풀릴 수도 있지만, ADDITION/테이블이 섞이면 비선형이 되어 SMT가 필요해진다.

  1. checks로 단독 검사하기

checks 배열도 디버깅에 포함된다. 중요한 건 이미 모든 제약이 들어간 Solver에 추가로 넣지 말고, 매번 새 Solver를 만들어 단독으로 검사해야 한다는 점이다.

checks = [
("range", And([x[i] >= 0x20 for i in range(24)] + [x[i] <= 0x7E for i in range(24)])),
("sum", sum_cond),
("rot", rot_cond),
]
for name, cond in checks:
s_test, _ = build_base_solver()
s_test.add(cond)
print(name, s_test.check())

rangeunsat이면 입력 범위 자체가 모순이다. sum이나 rotunsat면 해당 식이 잘못 뽑혔거나 타입/오버플로우가 틀린 경우다.
이 단계에서 어디가 꼬이는지 바로 좁힐 수 있다.

  1. 중간값 검증

IDA/ghidra에서 계산한 중간값이 있으면 그대로 넣어본다.

s.add(v == 0x1234ABCD)

여기서 unsat가 뜨면 모델링이 틀렸다.

  1. 모델 출력 후 직접 검증
m = s.model()
print([m[x[i]].as_long() for i in range(24)])

이 값으로 실제 바이너리에 넣어서 통과하는지 확인한다.

5. Optimization

  1. 범위 제한: 입력이 대문자/숫자만 허용되면 바로 줄인다.
  2. 불필요 조건 제거: 결과에 영향 없는 조건은 빼라.
  3. 표현식 캐싱: 같은 표현은 변수로 만들어 재사용.
  4. 부분 solve: 먼저 절반만 풀고 나머지를 고정.

Z3는 제약이 명확하면 빠르다. 모호하면 느리다.

z3는 만능은 아니다. 그래도 수식만 정확히 만들면 대부분 풀린다. 결국 실력 차이는 식을 뽑는 능력과 타입, 오버플로우 감각에서 갈린다고 생각한다. 감이 잡히면 난제도 풀린다.

6. 실습

✨ 챌린지 공짜 다운로드 ✨

z3_chall.c
#include <stdio.h>
#include <stdint.h>
#include <string.h>
static uint32_t lcg_next(uint32_t *s) {
*s = (*s * 1664525u) + 1013904223u;
return *s;
}
static uint32_t rotl32(uint32_t x, unsigned r) {
r &= 31u;
return (x << r) | (x >> ((32u - r) & 31u));
}
static uint64_t rotl64(uint64_t x, unsigned r) {
r &= 63u;
return (x << r) | (x >> ((64u - r) & 63u));
}
static uint32_t read_le32(const uint8_t *p) {
return (uint32_t)p[0]
| ((uint32_t)p[1] << 8)
| ((uint32_t)p[2] << 16)
| ((uint32_t)p[3] << 24);
}
static void gen_perm(uint8_t perm[48]) {
uint32_t s = 0xA3C59AC3u;
for (int i = 0; i < 48; i++) {
perm[i] = (uint8_t)i;
}
for (int i = 47; i > 0; i--) {
s = lcg_next(&s);
uint32_t j = s % (uint32_t)(i + 1);
uint8_t tmp = perm[i];
perm[i] = perm[j];
perm[j] = tmp;
}
}
static void gen_sbox(uint8_t sbox[24]) {
uint32_t s = 0xC3D2E1F0u;
for (int i = 0; i < 24; i++) {
s = lcg_next(&s);
sbox[i] = (uint8_t)s;
}
}
static void gen_keys(uint32_t keys[18]) {
uint32_t s = 0x1BADB002u;
for (int i = 0; i < 18; i++) {
s = lcg_next(&s);
keys[i] = s;
}
}
static void gen_tbox(uint32_t tbox[16]) {
uint32_t s = 0xB16B00B5u;
for (int i = 0; i < 16; i++) {
s = lcg_next(&s);
tbox[i] = s;
}
}
static uint32_t feistel_f(uint32_t x, uint32_t k, const uint32_t tbox[16]) {
uint32_t y = x + k;
uint32_t z = rotl32(x ^ k, y & 31u);
uint32_t m = (x * 0x045d9f3bu) ^ rotl32(y, 7);
uint32_t t = tbox[(x ^ k) & 0xFu];
uint32_t f = z + m + t + 0x9e3779b9u;
f ^= (f >> 16);
f += rotl32(f, 3);
return f;
}
static int check_ascii(const uint8_t *buf, size_t len) {
for (size_t i = 0; i < len; i++) {
if (buf[i] < 0x20 || buf[i] > 0x7e) {
return 0;
}
}
return 1;
}
static int check_stream(const uint8_t *buf) {
static const uint8_t stream_expected[48] = {
0x24, 0xd9, 0x4e, 0x85, 0x4f, 0x7d, 0x68, 0xb7,
0x08, 0x22, 0x1e, 0x4d, 0xd7, 0xdf, 0x99, 0x3d,
0xac, 0xfe, 0xd4, 0x06, 0x68, 0xfd, 0x21, 0x68,
0x42, 0xe3, 0x4d, 0x71, 0xec, 0x7d, 0xe6, 0xf4,
0x5a, 0x18, 0x9e, 0xef, 0x9c, 0x5b, 0x0d, 0x3e,
0x60, 0x78, 0x91, 0xfe, 0xc1, 0x93, 0x63, 0xf5
};
uint32_t s = 0xD00DFEEDu;
for (int i = 0; i < 48; i++) {
s = lcg_next(&s);
uint8_t ks = (uint8_t)(s >> 24);
if ((uint8_t)(buf[i] ^ ks) != stream_expected[i]) {
return 0;
}
}
return 1;
}
static int check_perm_sbox(const uint8_t *buf) {
static const uint8_t perm_expected[24] = {
0xe3, 0xc0, 0xe9, 0x57, 0x6c, 0x97, 0xa9, 0xac,
0x27, 0xee, 0x52, 0x13, 0x52, 0x01, 0x15, 0x22,
0xe4, 0x02, 0x4e, 0x09, 0x70, 0xb0, 0x08, 0x9c
};
uint8_t perm[48];
uint8_t sbox[24];
gen_perm(perm);
gen_sbox(sbox);
for (int i = 0; i < 24; i++) {
uint8_t v = (uint8_t)(buf[perm[i]] ^ sbox[i]);
if (v != perm_expected[i]) {
return 0;
}
}
return 1;
}
static int check_feistel(const uint8_t *buf) {
static const uint32_t expL[6] = {
0xc808b416u, 0xe9100223u, 0x5031e4ccu,
0x610df482u, 0x22c9a088u, 0x05f105c9u
};
static const uint32_t expR[6] = {
0xa217b24bu, 0xd5881d02u, 0x9f8c09e4u,
0x9605e10au, 0xe4fb1a56u, 0xb20a9001u
};
uint32_t keys[18];
uint32_t tbox[16];
gen_keys(keys);
gen_tbox(tbox);
for (int blk = 0; blk < 6; blk++) {
const uint8_t *p = buf + (blk * 8);
uint32_t L = read_le32(p);
uint32_t R = read_le32(p + 4);
for (int r = 0; r < 3; r++) {
uint32_t k = keys[(blk * 3) + r];
uint32_t f = feistel_f(R, k, tbox);
uint32_t newL = R;
R = L ^ f;
L = newL;
}
if (L != expL[blk] || R != expR[blk]) {
return 0;
}
}
return 1;
}
static int check_hash(const uint8_t *buf) {
const uint64_t expected = 0x8a7ad627100d088aULL;
uint64_t h = 0x0123456789ABCDEFULL;
for (int i = 0; i < 48; i++) {
h ^= (uint64_t)(buf[i] + i);
h *= 0x100000001B3ULL;
h = rotl64(h, 17);
h ^= (h >> 23);
}
return h == expected;
}
static int check_sums(const uint8_t *buf) {
const uint32_t sum_even_expected = 0x0000d028u;
const uint32_t sum_odd_expected = 0x0000e5e8u;
const uint8_t xor_expected = 0x60u;
uint32_t sum_even = 0;
uint32_t sum_odd = 0;
uint8_t xor_all = 0;
for (int i = 0; i < 48; i++) {
if ((i & 1) == 0) {
sum_even += (uint32_t)buf[i] * (uint32_t)(i + 1);
} else {
sum_odd += (uint32_t)buf[i] * (uint32_t)(i + 1);
}
xor_all ^= buf[i];
}
return sum_even == sum_even_expected
&& sum_odd == sum_odd_expected
&& xor_all == xor_expected;
}
int main(void) {
char input[128];
if (!fgets(input, sizeof(input), stdin)) {
return 1;
}
size_t len = strcspn(input, "\r\n");
input[len] = '\0';
if (len != 48) {
puts("wrong");
return 0;
}
const uint8_t *buf = (const uint8_t *)input;
if (!check_ascii(buf, len)) {
puts("wrong");
return 0;
}
if (check_stream(buf) &&
check_perm_sbox(buf) &&
check_feistel(buf) &&
check_hash(buf) &&
check_sums(buf)) {
puts("correct");
} else {
puts("wrong");
}
return 0;
}
  • 체커 흐름을 보면 입력은 48바이트 ASCII여야 한다.
  • LCG 기반 키스트림 XOR로 각 바이트를 고정한다.
  • 24개 위치에 대해 permutation + S-box XOR 제약을 추가한다.
  • 6개 블록에 대해 3라운드 Feistel을 적용하고 L/R 결과를 맞춘다.
  • 롤링 해시와 선형 합/ XOR 조건으로 최종 문자열을 확정한다.

실제로는 48바이트를 Z3 BitVec으로 모델링하고 모든 검사를 제약으로 넣어 풀어야 한다.

z3_chall.py
from z3 import *
A = 1664525
C = 1013904223
def lcg(x):
return (x * A + C) & 0xFFFFFFFF
def gen_perm():
state = 0xA3C59AC3
perm = list(range(48))
for i in range(47, 0, -1):
state = lcg(state)
j = state % (i + 1)
perm[i], perm[j] = perm[j], perm[i]
return perm
def gen_sbox():
state = 0xC3D2E1F0
sbox = []
for _ in range(24):
state = lcg(state)
sbox.append(state & 0xFF)
return sbox
def gen_keys():
state = 0x1BADB002
keys = []
for _ in range(18):
state = lcg(state)
keys.append(state)
return keys
def gen_tbox():
state = 0xB16B00B5
tbox = []
for _ in range(16):
state = lcg(state)
tbox.append(state)
return tbox
perm_expected = [
0xe3, 0xc0, 0xe9, 0x57, 0x6c, 0x97, 0xa9, 0xac,
0x27, 0xee, 0x52, 0x13, 0x52, 0x01, 0x15, 0x22,
0xe4, 0x02, 0x4e, 0x09, 0x70, 0xb0, 0x08, 0x9c
]
stream_expected = [
0x24, 0xd9, 0x4e, 0x85, 0x4f, 0x7d, 0x68, 0xb7,
0x08, 0x22, 0x1e, 0x4d, 0xd7, 0xdf, 0x99, 0x3d,
0xac, 0xfe, 0xd4, 0x06, 0x68, 0xfd, 0x21, 0x68,
0x42, 0xe3, 0x4d, 0x71, 0xec, 0x7d, 0xe6, 0xf4,
0x5a, 0x18, 0x9e, 0xef, 0x9c, 0x5b, 0x0d, 0x3e,
0x60, 0x78, 0x91, 0xfe, 0xc1, 0x93, 0x63, 0xf5
]
expL = [0xc808b416, 0xe9100223, 0x5031e4cc, 0x610df482, 0x22c9a088, 0x05f105c9]
expR = [0xa217b24b, 0xd5881d02, 0x9f8c09e4, 0x9605e10a, 0xe4fb1a56, 0xb20a9001]
hash_expected = 0x8a7ad627100d088a
sum_even_expected = 0x0000d028
sum_odd_expected = 0x0000e5e8
xor_expected = 0x60
b = [BitVec(f"b{i}", 8) for i in range(48)]
s = SolverFor("QF_BV")
for i in range(48):
s.add(UGE(b[i], 0x20), ULE(b[i], 0x7e))
state = 0xD00DFEED
for i in range(48):
state = lcg(state)
ks = (state >> 24) & 0xFF
s.add(b[i] ^ BitVecVal(ks, 8) == BitVecVal(stream_expected[i], 8))
perm = gen_perm()
sbox = gen_sbox()
for i in range(24):
s.add(b[perm[i]] ^ BitVecVal(sbox[i], 8) == BitVecVal(perm_expected[i], 8))
keys = gen_keys()
tbox = gen_tbox()
tbox_arr = K(BitVecSort(4), BitVecVal(0, 32))
for i, v in enumerate(tbox):
tbox_arr = Store(tbox_arr, BitVecVal(i, 4), BitVecVal(v, 32))
def rotl32_var(x, r):
r = r & BitVecVal(31, 32)
inv = (-r) & BitVecVal(31, 32)
return (x << r) | LShR(x, inv)
def feistel_f(x, k):
y = x + k
z = rotl32_var(x ^ k, y)
m = (x * BitVecVal(0x045d9f3b, 32)) ^ RotateLeft(y, 7)
idx = Extract(3, 0, x ^ k)
t = Select(tbox_arr, idx)
f = z + m + t + BitVecVal(0x9e3779b9, 32)
f = f ^ LShR(f, 16)
f = f + rotl32_var(f, BitVecVal(3, 32))
return f
def pack_le32(bs):
return Concat(bs[3], bs[2], bs[1], bs[0])
for blk in range(6):
off = blk * 8
L = pack_le32(b[off:off+4])
R = pack_le32(b[off+4:off+8])
for r in range(3):
k = BitVecVal(keys[blk * 3 + r], 32)
f = feistel_f(R, k)
L, R = R, L ^ f
s.add(L == BitVecVal(expL[blk], 32))
s.add(R == BitVecVal(expR[blk], 32))
h = BitVecVal(0x0123456789ABCDEF, 64)
for i in range(48):
h = h ^ (ZeroExt(56, b[i]) + BitVecVal(i, 64))
h = h * BitVecVal(0x100000001B3, 64)
h = RotateLeft(h, 17)
h = h ^ LShR(h, 23)
s.add(h == BitVecVal(hash_expected, 64))
sum_even = BitVecVal(0, 32)
sum_odd = BitVecVal(0, 32)
xor_all = BitVecVal(0, 8)
for i in range(48):
if i % 2 == 0:
sum_even = sum_even + ZeroExt(24, b[i]) * (i + 1)
else:
sum_odd = sum_odd + ZeroExt(24, b[i]) * (i + 1)
xor_all = xor_all ^ b[i]
s.add(sum_even == BitVecVal(sum_even_expected, 32))
s.add(sum_odd == BitVecVal(sum_odd_expected, 32))
s.add(xor_all == BitVecVal(xor_expected, 8))
if s.check() != sat:
print("unsat")
raise SystemExit(1)
m = s.model()
candidate = "".join(chr(m.eval(b[i]).as_long()) for i in range(48))
print(candidate)
lia = SolverFor("QF_LIA")
x = [Int(f"x{i}") for i in range(48)]
for i in range(48):
lia.add(x[i] == ord(candidate[i]))
lia.add(x[i] >= 0x20, x[i] <= 0x7e)
lia.add(Sum([x[i] * (i + 1) for i in range(0, 48, 2)]) == sum_even_expected)
lia.add(Sum([x[i] * (i + 1) for i in range(1, 48, 2)]) == sum_odd_expected)
assert lia.check() == sat
xor_val = 0
for ch in candidate:
xor_val ^= ord(ch)
assert xor_val == xor_expected
  • Top 3 Solver (Discord @badabodaa)
@Greedun RubiyaLab 2025.12.27
@Gunter RubiyaLab 2025.12.27