무작위 대입으로 니모닉을 복구할 수 있을까?
![](https://framerusercontent.com/images/IjmFirbmjZsTE07kdE9KmsvTWFA.png)
Yuu | GMB LABS
|
General
2025-02-07
Disclaimer : 본 아티클에 언급된 내용은 GMB LABS 개인의 의견으로 GMB LABS의 공식 입장과는 무관합니다.
본 아티클에 포함된 어떠한 내용도 투자 조언이 아니며, 투자 조언으로 해석되어서도 안 됩니다.
Disclaimer : 본 아티클에 언급된 내용은 GMB LABS 개인의 의견으로 GMB LABS의 공식 입장과는 무관합니다.
본 아티클에 포함된 어떠한 내용도 투자 조언이 아니며, 투자 조언으로 해석되어서도 안 됩니다.
![](https://framerusercontent.com/images/erB4ckRzAVqH9VAm7hgaYDOrJ9s.png)
밈 코인 열풍과 DEX 거래의 급격한 성장으로
이제는 “니모닉” 이라는 단어는 암호화폐 생태계에서 어렵지 않은 개념이 되었습니다.
해당 글에서는 니모닉의 일부를 잊어 버렸을 경우
무작위 대입(Brute force)을 통하여 복구할 수 있는가를 다루려고 합니다.
니모닉과 계층적 결정에 대해 자세히 알고 싶다면!
https://steemit.com/kr-dev/@modolee/mastering-ethereum-4-wallet
니모닉을 통한 이더리움 지갑 생성은 다음과 같이 코드를 구성할 수 있습니다.
python 의존성 파일을 먼저 설치합니다.
pip install mnemonic eth-account
랜덤한 니모닉 12개로 지갑을 생성하는 코드를 먼저 구성합니다.
from mnemonic import Mnemonic
from eth_account import Account
def generate_eth_address():
# BIP39 영단어 목록을 사용하여 니모닉 생성
mnemo = Mnemonic("english")
# 12단어 니모닉 생성
mnemonic = mnemo.generate()
print(f"니모닉: {mnemonic}")
# 니모닉으로부터 이더리움 계정 생성
Account.enable_unaudited_hdwallet_features()
account = Account.from_mnemonic(mnemonic)
# 주소 출력
print(f"이더리움 주소: {account.address}")
print(f"개인키: {account.key.hex()}")
return mnemonic, account.address
if __name__ == "__main__":
generate_eth_address()
니모닉: awesome eight gasp decline entire cycle innocent permit until beef sunny return
이더리움 주소: 0xc7a4D73E9266B0E7a274Cd15fBC413F157b1d37A
개인키: 1c5e293177cd88783022a01b3a6ba3c542916e4c79fd52ab57301da601e1a2cb
![](https://framerusercontent.com/images/xIUdu6A0MrZZDW0sooyFPfPr9M.png)
다음 예제를 이용하여 니모닉을 한 개 분실했을 경우를 가정하겠습니다.
2048개를 무작위 대입으로 하는 코드를 구성
from mnemonic import Mnemonic
from eth_account import Account
from tqdm import tqdm
# BIP39 영단어 목록을 사용
mnemo = Mnemonic("english")
words = mnemo.wordlist
def find_missing_word():
incomplete_mnemonic = "sudden chronic alcohol pig path plug left symbol range inject promote"
target_address = "0x394c38284fbb796c8afb1ddb96c837e86919628b"
incomplete_words = incomplete_mnemonic.split()
print(f"시도할 단어 수: {len(words)}")
# HD Wallet 기능 활성화
Account.enable_unaudited_hdwallet_features()
for missing_word in tqdm(words):
try:
# 완성된 니모닉 문구 생성
test_mnemonic = " ".join(incomplete_words + [missing_word])
# 니모닉이 유효한지 확인
if not mnemo.check(test_mnemonic):
continue
test_account = Account.from_mnemonic(test_mnemonic)
# 주소 확인 (대소문자 구분 없이)
if test_account.address.lower() == target_address.lower():
print(f"니모닉: {test_mnemonic}")
print(f"이더리움 주소: {test_account.address}")
print(f"개인키: {test_account.key.hex()}")
return missing_word
except Exception as e:
continue
print("\n일치하는 단어를 찾지 못했습니다.")
return None
if __name__ == "__main__":
find_missing_word()
시도할 단어 수: 2048
완성된 니모닉: sudden chronic alcohol pig path plug left symbol range inject promote hunt
주소: 0x394C38284FBb796C8afb1Ddb96c837E86919628b
개인키: a7536f3ff38c78718dcdffbdac183f148f545e79693dc0982e1f6e370bbfc699
네 글자라는 힌트를 대입할 경우
from mnemonic import Mnemonic
from eth_account import Account
from tqdm import tqdm
# BIP39 영단어 목록을 사용
mnemo = Mnemonic("english")
# 4글자인 단어만 필터링
words = [word for word in mnemo.wordlist if len(word) == 4]
def find_missing_word():
incomplete_mnemonic = "sudden chronic alcohol pig path plug left symbol range inject promote"
target_address = "0x394c38284fbb796c8afb1ddb96c837e86919628b"
incomplete_words = incomplete_mnemonic.split()
print(f"시도할 4글자 단어 수: {len(words)}")
# HD Wallet 기능 활성화
Account.enable_unaudited_hdwallet_features()
for missing_word in tqdm(words):
try:
# 완성된 니모닉 문구 생성
test_mnemonic = " ".join(incomplete_words + [missing_word])
# 니모닉이 유효한지 확인
if not mnemo.check(test_mnemonic):
continue
test_account = Account.from_mnemonic(test_mnemonic)
# 주소 확인 (대소문자 구분 없이)
if test_account.address.lower() == target_address.lower():
print(f"니모닉: {test_mnemonic}")
print(f"이더리움 주소: {test_account.address}")
print(f"개인키: {test_account.key.hex()}")
return missing_word
except Exception as e:
continue
print("\n일치하는 단어를 찾지 못했습니다.")
return None
if __name__ == "__main__":
find_missing_word()
시도할 4글자 단어 수: 442
니모닉: sudden chronic alcohol pig path plug left symbol range inject promote hunt
이더리움 주소: 0x394C38284FBb796C8afb1Ddb96c837E86919628b
개인키: a7536f3ff38c78718dcdffbdac183f148f545e79693dc0982e1f6e370bbfc699
실제 실행 시간은 모두 1초 내외입니다.
![](https://framerusercontent.com/images/WXkPY78RrdAJ3ifZ0uu6Ae4hg.png)
다수의 니모닉을 분실한 경우로 보입니다.
니모닉의 첫 글자와 자릿수를 알고 있으니 이를 토대로 분석해 보기로 합니다.
첫 글자와 자릿수로 해당하는 영단어를 확인하는 예제 코드
from mnemonic import Mnemonic
# BIP39 영단어 목록을 사용
mnemo = Mnemonic("english")
words = mnemo.wordlist
def find_words_by_pattern():
# s로 시작하는 5글자
s_five = [word for word in words if word.startswith('s') and len(word) == 5]
print("\ns로 시작하는 5글자 단어들:")
print(f"개수: {len(s_five)}")
print("단어들:", ', '.join(s_five))
if __name__ == "__main__":
find_words_by_pattern()
상단의 니모닉
o로 시작하는 3글자 단어들:
개수: 6
단어들: oak, off, oil, old, one, own
c로 시작하는 5글자 단어들:
개수: 57
단어들: cabin, cable, canal, candy, canoe, cargo, carry, catch, cause, chair, chalk, chaos, chase, cheap, check, chest, chief, child, chunk, churn, cigar, civil, claim, clean, clerk, click, cliff, climb, clock, close, cloth, cloud, clown, clump, coach, coast, color, comic, coral, couch, cover, crack, craft, crane, crash, crawl, crazy, cream, creek, crime, crisp, cross, crowd, cruel, crush, curve, cycle
m으로 시작하는 5글자 단어들:
개수: 23
단어들: magic, major, mango, maple, march, match, medal, media, mercy, merge, merit, merry, metal, mimic, minor, mixed, model, month, moral, motor, mouse, movie, music
c로 시작하는 4글자 단어들:
개수: 34
단어들: cage, cake, call, calm, camp, card, cart, case, cash, cave, chat, chef, city, clap, claw, clay, clip, clog, club, code, coil, coin, come, cook, cool, copy, core, corn, cost, cram, crew, crop, cube, cute
가능한 조합의 갯수
6 * 57 * 23 * 57 * 34 = 15,244,308
하단의 니모닉
s로 시작하는 5글자 단어들:
개수: 92
단어들: salad, salon, sauce, scale, scare, scene, scout, scrap, scrub, sense, setup, seven, shaft, share, shell, shift, shine, shock, shoot, short, shove, shrug, siege, sight, silly, since, siren, skate, skill, skirt, skull, sleep, slice, slide, slush, small, smart, smile, smoke, snack, snake, sniff, solar, solid, solve, sorry, sound, south, space, spare, spawn, speak, speed, spell, spend, spice, spike, split, spoil, spoon, sport, spray, staff, stage, stamp, stand, start, state, steak, steel, stick, still, sting, stock, stone, stool, story, stove, stuff, style, sugar, sunny, super, surge, swamp, swarm, swear, sweet, swift, swing, sword, syrup
s로 시작하는 7글자 단어들:
개수: 37
단어들: sadness, satisfy, satoshi, sausage, scatter, science, section, segment, seminar, service, session, shallow, sheriff, shuffle, sibling, similar, situate, slender, soldier, someone, spatial, special, sponsor, squeeze, stadium, stomach, student, stumble, subject, success, suggest, supreme, surface, suspect, sustain, swallow, symptom
b로 시작하는 5글자 단어들:
개수: 36
단어들: bacon, badge, basic, beach, begin, below, bench, birth, black, blade, blame, blast, bleak, bless, blind, blood, blush, board, bonus, boost, brain, brand, brass, brave, bread, brick, brief, bring, brisk, broom, brown, brush, buddy, build, burst, buyer
m으로 시작하는 7글자 단어들:
개수: 16
단어들: machine, mandate, mansion, maximum, measure, mention, message, million, minimum, miracle, mistake, mixture, monitor, monster, morning, mystery
가능한 조합의 갯수
92 * 37 * 36 * 16 = 1,960,704
멀티 시그 지갑 주소를 탐색
![](https://framerusercontent.com/images/29bkoyyMO7mZP3EovMIJ3h4inRE.png)
getOwners를 통해서 멀티시그의 Onwers를 가져올 수 있습니다
0x219B588fbd48B3678864B582f5DDaABC8e8db104
0x6239598a18EA5D413073dc63f5d34cFE0998561b
하단의 니모닉을 찾는 코드를 구성
from mnemonic import Mnemonic
from eth_account import Account
from tqdm import tqdm
import itertools
# BIP39 영단어 목록을 사용
mnemo = Mnemonic("english")
words = mnemo.wordlist
def find_missing_words():
# 기본 니모닉 (알려진 부분)
base_mnemonic = "member throw creek omit pizza valid way balance"
target_address = "0x219B588fbd48B3678864B582f5DDaABC8e8db104".lower()
targer_address2 = "0x6239598a18EA5D413073dc63f5d34cFE0998561b".lower()
# 조건에 맞는 단어들 필터링
s_five = [word for word in words if word.startswith('s') and len(word) == 5]
s_seven = [word for word in words if word.startswith('s') and len(word) == 7]
b_five = [word for word in words if word.startswith('b') and len(word) == 5]
m_seven = [word for word in words if word.startswith('m') and len(word) == 7]
print(f"\n시도할 조합 수:")
print(f"s로 시작하는 5글자 단어: {len(s_five)}개")
print(f"s로 시작하는 7글자 단어: {len(s_seven)}개")
print(f"b로 시작하는 5글자 단어: {len(b_five)}개")
print(f"m으로 시작하는 7글자 단어: {len(m_seven)}개")
print(f"총 조합 수: {len(s_five) * len(s_seven) * len(b_five) * len(m_seven)}")
# HD Wallet 기능 활성화
Account.enable_unaudited_hdwallet_features()
# 모든 가능한 조합 시도
total = len(s_five) * len(s_seven) * len(b_five) * len(m_seven)
for words_combo in tqdm(itertools.product(s_five, s_seven, b_five, m_seven), total=total):
try:
# 완성된 니모닉 문구 생성
test_mnemonic = base_mnemonic + " " + " ".join(words_combo)
# 니모닉이 유효한지 확인
if not mnemo.check(test_mnemonic):
continue
test_account = Account.from_mnemonic(test_mnemonic)
# 주소 확인 (대소문자 구분 없이)
if test_account.address.lower() == target_address or test_account.address.lower() == targer_address
print(f"니모닉: {test_mnemonic}")
print(f"이더리움 주소: {test_account.address}")
print(f"개인키: {test_account.key.hex()}")
return
except Exception as e:
continue
print("\n일치하는 니모닉을 찾지 못했습니다.")
if __name__ == "__main__":
find_missing_words()
총 조합 수: 1960704
니모닉: member throw creek omit pizza valid way balance space swallow brief maximum
이더리움 주소: 0x6239598a18EA5D413073dc63f5d34cFE0998561b
개인키: 73eb42231d1a50f38e641c30b6c285c98703d96fddbe5055b85d7ab233cf503b
분실된 니모닉과 지갑 주소의 매칭을 모르기 때문에 경우의 수가 적은 하단의 니모닉을 먼저 찾도록 합니다.
M1 Pro 경우 14분정도 소요 되었습니다.
상단 니모닉을 찾는 구성
from mnemonic import Mnemonic
from eth_account import Account
from concurrent.futures import ProcessPoolExecutor
import itertools
import time
from dataclasses import dataclass
from typing import List, Tuple, Optional
import os
import signal
import sys
from tqdm import tqdm
@dataclass
class SearchConfig:
base_mnemonic: str
target_address: str
word_filters: List[Tuple[str, int]]
class MnemonicFinder:
def __init__(self, config: SearchConfig):
self.config = config
self.mnemo = Mnemonic("english")
self.total_combinations = 0
self.processed_combinations = 0
self.found = False
self.result = None
def get_filtered_words(self) -> List[List[str]]:
"""각 위치에 대한 필터링된 단어 목록 생성"""
words = self.mnemo.wordlist
filtered_words = []
total_words = 0
for prefix, length in self.config.word_filters:
word_list = [w for w in words if w.startswith(prefix) and len(w) == length]
count = len(word_list)
total_words += count
print(f"{prefix}로 시작하는 {length}글자 단어: {count}개")
filtered_words.append(word_list)
return filtered_words
def process_chunk(self, chunk: List[Tuple[str, ...]]) -> Optional[Tuple[str, str, str]]:
"""주어진 조합 청크를 처리"""
# 각 워커 프로세스에서 HD Wallet 기능 활성화
Account.enable_unaudited_hdwallet_features()
processed_count = 0
for combo in chunk:
if self.found:
return None
test_mnemonic = f"{self.config.base_mnemonic} {' '.join(combo)}"
processed_count += 1
try:
# 니모닉 검증
if not self.mnemo.check(test_mnemonic):
continue
account = Account.from_mnemonic(test_mnemonic)
current_address = account.address.lower()
if current_address == self.config.target_address.lower():
return (test_mnemonic, account.address, account.key.hex())
except Exception as e:
print(f"에러 발생: {str(e)}")
continue
self.processed_combinations += 1
return None
def chunk_combinations(self, combinations: List[Tuple[str, ...]], chunk_size: int):
"""조합을 청크로 나누기"""
for i in range(0, len(combinations), chunk_size):
yield combinations[i:i + chunk_size]
def search(self) -> Optional[Tuple[str, str, str]]:
"""니모닉 검색 실행"""
filtered_words = self.get_filtered_words()
combinations = list(itertools.product(*filtered_words))
self.total_combinations = len(combinations)
print(f"총 조합 수: {self.total_combinations:,}개")
num_processes = os.cpu_count()
print(f"프로세스 수: {num_processes}")
# 청크 크기 계산
chunk_size = max(100, self.total_combinations // (num_processes * 10))
chunks = list(self.chunk_combinations(combinations, chunk_size))
processed = 0
with tqdm(total=self.total_combinations) as pbar:
with ProcessPoolExecutor(max_workers=num_processes) as executor:
for result in executor.map(self.process_chunk, chunks):
if result:
self.found = True
return result
processed += chunk_size
pbar.update(chunk_size)
if self.found:
break
return None
def main():
def signal_handler(signum, frame):
print("검색을 중단합니다...")
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
# 메인 프로세스에서 HD Wallet 기능 활성화
Account.enable_unaudited_hdwallet_features()
config = SearchConfig(
base_mnemonic="camp deal area rigid hidden priority jacket",
target_address="0x219B588fbd48B3678864B582f5DDaABC8e8db104".lower(),
word_filters=[
('o', 3), # o로 시작하는 3글자 단어
('c', 5), # c로 시작하는 5글자 단어(1)
('m', 5), # m으로 시작하는 5글자 단어
('c', 5), # c로 시작하는 5글자 단어(2)
('c', 4), # c로 시작하는 4글자 단어
]
)
finder = MnemonicFinder(config)
result = finder.search()
if result:
mnemonic, address, private_key = result
print(f"니모닉: {mnemonic}")
print(f"이더리움 주소: {address}")
print(f"개인키: {private_key}")
else:
print("일치하는 니모닉을 찾지 못했습니다.")
if __name__ == "__main__":
main()
니모닉: camp deal area rigid hidden priority jacket own click merge crowd claw
이더리움 주소: 0x219B588fbd48B3678864B582f5DDaABC8e8db104
개인키: ec441b3aba12d30299772f1669337b452ad0bb774346e7d2f88798565edcb1d0
CPU 혹은 GPU 병렬 처리를 활용하여 속도를 더욱 높일 수 있습니다. (예제는 CPU 병렬 처리)
M1 Pro의 경우 30분 정도 소요 되었습니다.
결론
![](https://framerusercontent.com/images/iPkM8AGlnFtLKDG9IMmlJKm6oU.png)
필자의 보안 지식 수준
- 단어 1개를 분실한 경우
경우의 수 : 2048
예상 복구시간 :1초 미만
- 단어 2개를 분실한 경우
경우의 수 : 2048 * 2048 = 4,194,304
예상 복구 시간 : 10분 내외
- 단어 3개를 분실한 경우
경우의 수 : 2048 * 2048 * 2048 = 8,589,934,592
예상 복구 시간 : 1일 내외
- 단어 12개 분실한 경우
경우의 수 : 2048^12 = 5,444,517,870,735,015,936,000,000,000,000,000,000,000
예상 복구 시간 : 불가능
이번 사례는 몇 가지 특수한 조건이 적용되었기 때문에
무작위 대입을 통하여 비교적 빠른 시간 내에 니모닉을 복구해 볼 수 있었으나,
일반적인 상황에서는 사실상 대부분 복구가 어렵다고 보는 것이 맞습니다.
- 니모닉 문구는 절대 온라인에 저장하지 말 것
- 부분적이라도 니모닉을 공개하지 말 것
- 가능하다면 하드웨어 지갑 ( Ledger 등... )을
따라서 니모닉을 분실했을 때 복구하는 방법을 찾는 것보다는
애초에 안전하게 보관하는 것이 가장 중요한 대비책입니다.
Latest
©2021-2024. GMB LABS
©2021-2024. GMB LABS
©2021-2024. GMB LABS