from:http://www.debasish.in/2014/04/attacking-audio-recaptcha-using-googles.html
關于驗證碼和驗證碼破解的入門,請看:http://drops.wooyun.org/tips/141
什么是reCaptcha?
reCaptchas是由Google提供的基于云的驗證碼系統,通過結合程序生成的驗證碼和較難被OCR識別的圖片,來幫助Google數字化一些書籍,報紙和街景里的門牌號等。
reCaptcha同時還有聲音驗證碼的功能,用來給盲人提供服務。
中心思想:
用Google的Web Speech API語音識別來破解它自己的reCaptcha聲音驗證碼.
下面來看一下用來語音識別的API
Chrome瀏覽器內建了一個基于HTML5的語音輸入API,通過它,用戶可以通過麥克風輸入語音,然后Chrome會識別成文字,這個功能在Android系統下也有。如果你不熟悉這個功能的話這里有個demo:
https://www.google.com/intl/en/chrome/demos/speech.html
我一直很好奇這個語音識別API是如何工作的,是通過瀏覽器本身識別的還是把音頻發送到云端識別呢?
通過抓包發現,好像的確會把語音發送到云端,不過發送出去的數據是SSL加密過的。
于是我開始翻Chromium項目的源碼,終于我找到了有意思的地方:
http://src.chromium.org/viewvc/chrome/trunk/src/content/browser/speech/
實現過程非常簡單,首先從mic獲取音頻數據,然后發送到Google的語音識別Web服務,返回JSON格式的識別結果。 用來識別的Web API在這里:
https://www.google.com/speech-api/v1/recognize
比較重要的一點是這個API只接受flac格式的音頻(無損格式,真是高大上)。
既然知道了原理,寫一個利用這個識別API的程序就很簡單了。
#!bash
./google_speech.py hello.flac
源代碼:
#!python
'''
Accessing Google Web Speech API using Pyhon
Author : Debasish Mandal
'''
import httplib
import sys
print '[+] Sending clean file to Google voice API'
f = open(sys.argv[1])
data = f.read()
f.close()
google_speech = httplib.HTTPConnection('www.google.com')
google_speech.request('POST','/speech-api/v1/recognize?xjerr=1&client=chromium&lang=en-US',data,{'Content-type': 'audio/x-flac; rate=16000'})
print google_speech.getresponse().read()
google_speech.close()
研究了一下reCaptcha的語音驗證碼后,你會發現基本上有兩種語音驗證碼,一種是非常簡單的,沒有加入很多噪音,語音也很清晰。另外一種是非常復雜的,故意加了很多噪音,連真人很難聽出來。這種驗證碼里面估計加了很多嘶嘶的噪聲,并且用很多人聲作為干擾。
關于這個語音驗證碼的細節可以參考這里https://groups.google.com/forum/#!topic/recaptcha/lkCyM34zbJo
在這篇文章中我主要寫了如何解決前一種驗證碼,雖然我為了破解后一種復雜的驗證碼也做了很多努力,但是實在是太困難了,即使是人類對于它的識別率也很低。
用戶可以把recaptcha的語音驗證碼以mp3格式下載下來,但是Google語音識別接口只接受flac格式,所以我們需要對下載回來的mp3進行一些處理然后轉換成flac再提交。
我們先手工驗證一下這樣行不行:
首先把recaptcha播放的音頻下載成mp3文件。
然后用一個叫Audacity的音頻編輯軟件打開,如圖
把第一個數字的聲音復制到新窗口中,然后再重復一次,這樣我們把第一位數字的聲音復制成連續的兩個相同聲音。
比如這個驗證碼是76426,我們的目的是把7先分離出來,然后讓7的語音重復兩次。
最后把這段音頻保存成wav格式,再轉換成flac格式,然后提交到API。
#!bash
[email protected] ~/Desktop/audio/heart attack/final $ sox cut_0.wav -r 16000 -b 16 -c 1 cut_0.flac lowpass -2 2500
[email protected] ~/Desktop/audio/heart attack/final $ python send.py cut_0.flac
很好,服務器成功識別了這段音頻并且返回了正確的結果,下面就需要把這個過程自動化了。
在自動提交之前,我們需要了解一下數字音頻是處理什么原理。
這個stackoverflow的問題是個很好的教程:
http://stackoverflow.com/questions/732699/how-is-audio-represented-with-numbers
把一個wav格式的文件用16進制編輯器打開:
用Python WAVE模塊處理wav格式的音頻:
wave模塊提供了一個很方便接口用來處理wav格式:
#!python
import wave
f = wave.open('sample.wav', 'r')
print '[+] WAV parameters ',f.getparams()
print '[+] No. of Frames ',f.getnframes()
for i in range(f.getnframes()):
single_frame = f.readframes(1)
print single_frame.encode('hex')
f.close()
getparams()函數返回一個元組,內容是關于這個wav文件的一些元數據,例如頻道數量,采樣寬度,采樣率,幀數等等。
getnframes()返回這個wav文件有多少幀。
運行這個python程序后,會把sample.wav的每一幀用16進制表示然后print出來
[+] WAV parameters (1, 2, 44100, 937, 'NONE', 'not compressed')
[+] No. of Frames 937
[+] Sample 0 = 62fe ? ?<- Sample 1
[+] Sample 1 = 99fe ? <- Sample 2
[+] Sample 2 = c1ff ? ?<- Sample 3
[+] Sample 3 = 9000
[+] Sample 4 = 8700
[+] Sample 5 = b9ff
[+] Sample 6 = 5cfe
[+] Sample 7 = 35fd
[+] Sample 8 = b1fc
[+] Sample 9 = f5fc
[+] Sample 10 = 9afd
[+] Sample 11 = 3cfe
[+] Sample 12 = 83fe
[+] ....
從輸出文件中我們可以看到,這個wav文件是單通道的,每個通道是2字節長,因為音頻是16比特的,我們也可以用 getsampwidth()函數來判斷通道寬度,getchannels() 可以用來確定音頻是單聲道還是立體聲。
接下來對每幀進行解碼,這個16進制編碼實際上是小端序保存的(little-endian),所以還需要對這段python程序做一些修改,并且利用struct模塊把每幀的值轉換成帶符號的整數。
#!python
import wave
import struct
f = wave.open('sample.wav', 'r')
print '[+] WAV parameters ',f.getparams()
print '[+] No. of Frames ',f.getnframes()
for i in range(f.getnframes()):
single_frame = f.readframes(1)
sint = struct.unpack('<h', single_frame) [0]
print "[+] Sample ",i," = ",single_frame.encode('hex')," -> ",sint[0]
f.close()
修改完畢后再次運行,輸出內容差不多這樣:
[+] WAV parameters (1, 2, 44100, 937, 'NONE', 'not compressed')
[+] No. of Frames 937
[+] Sample 0 = 62fe -> -414
[+] Sample 1 = 99fe -> -359
[+] Sample 2 = c1ff -> -63
[+] Sample 3 = 9000 -> 144
[+] Sample 4 = 8700 -> 135
[+] Sample 5 = b9ff -> -71
[+] Sample 6 = 5cfe -> -420
[+] Sample 7 = 35fd -> -715
[+] Sample 8 = b1fc -> -847
[+] Sample 9 = f5fc -> -779
[+] Sample 10 = 9afd -> -614
[+] Sample 11 = 3cfe -> -452
[+] Sample 12 = 83fe -> -381
[+] Sample 13 = 52fe -> -430
[+] Sample 14 = e2fd -> -542
這樣是不是更明白了?下面用python的matplotlib畫圖模塊把這些數值畫出來:
#!python
import wave
import struct
import matplotlib.pyplot as plt
data_set = []
f = wave.open('sample.wav', 'r')
print '[+] WAV parameters ',f.getparams()
print '[+] No. of Frames ',f.getnframes()
for i in range(f.getnframes()):
single_frame = f.readframes(1)
sint = struct.unpack('<h', single_frame)[0]
data_set.append(sint)
f.close()
plt.plot(data_set)
plt.ylabel('Amplitude')
plt.xlabel('Time')
plt.show()
這個圖實際上就是聲音的波形圖
進一步自動化:
下面這段python程序通過音量不同把音頻文件分割成多個音頻文件,相當于圖片驗證碼識別中的圖片分割步驟。
#!python
'''
簡單的基于音量的音頻文件分割程序
作用:?
1. 簡單的降噪處理
2. 識別文件中的高音量部分
3. 根據高音量部分的數目把文件分割成獨立文件
?
'''
import wave
import sys
import struct
import os
import time
import httplib
from random import randint
ip = wave.open(sys.argv[1], 'r')
info = ip.getparams()
frame_list = []
for i in range(ip.getnframes()):
sframe = ip.readframes(1)
amplitude = struct.unpack('<h', sframe)[0]
frame_list.append(amplitude)
ip.close()
for i in range(0,len(frame_list)):
if abs(frame_list[i]) < 25:
frame_list[i] = 0
################################ Find Out most louder portions of the audio file ###########################
thresh = 30
output = []
nonzerotemp = []
length = len(frame_list)
i = 0
while i < length:
zeros = []
while i < length and frame_list[i] == 0:
i += 1
zeros.append(0)
if len(zeros) != 0 and len(zeros) < thresh:
nonzerotemp += zeros
elif len(zeros) > thresh:
if len(nonzerotemp) > 0 and i < length:
output.append(nonzerotemp)
nonzerotemp = []
else:
nonzerotemp.append(frame_list[i])
i += 1
if len(nonzerotemp) > 0:
output.append(nonzerotemp)
chunks = []
for j in range(0,len(output)):
if len(output[j]) > 3000:
chunks.append(output[j])
#########################################################################################################
for l in chunks:
for m in range(0,len(l)):
if l[m] == 0:
l[m] = randint(-0,+0)
inc_percent = 1 #10 percent
for l in chunks:
for m in range(0,len(l)):
if l[m] <= 0:
# negative value
l[m] = 0 - abs(l[m]) + abs(l[m])*inc_percent/100
else:
#positive vaule
l[m] = abs(l[m]) + abs(l[m])*inc_percent/100
########################################################
# Below code generates separate wav files depending on the number of loud voice detected.
NEW_RATE = 1 #Change it to > 1 if any amplification is required
print '[+] Possibly ',len(chunks),'number of loud voice detected...'
for i in range(0, len(chunks)):
new_frame_rate = info[0]*NEW_RATE
print '[+] Creating No. ',str(i),'file..'
split = wave.open('cut_'+str(i)+'.wav', 'w')
split.setparams((info[0],info[1],info[2],0,info[4],info[5]))
# split.setparams((info[0],info[1],new_frame_rate,0,info[4],info[5]))
#Add some silence at start selecting +15 to -15
for k in range(0,10000):
single_frame = struct.pack('<h', randint(-25,+25))
split.writeframes(single_frame)
# Add the voice for the first time
for frames in chunks[i]:
single_frame = struct.pack('<h', frames)
split.writeframes(single_frame)
#Add some silence in between two digits
for k in range(0,10000):
single_frame = struct.pack('<h', randint(-25,+25))
split.writeframes(single_frame)
# Repeat effect : Add the voice second time
for frames in chunks[i]:
single_frame = struct.pack('<h', frames)
split.writeframes(single_frame)
#Add silence at end
for k in range(0,10000):
single_frame = struct.pack('<h', randint(-25,+25))
split.writeframes(single_frame)
split.close()#Close each files
當這個文件被分割成多份之后我們可以簡單的把他們轉換成flac格式然后把每個文件單獨發送到Google語音識別API進行識別。
視頻已翻墻下載回來:
Solving reCaptcha Audio Challenge using Google Web Speech API Demo
現在我們已經解決了簡單的音頻驗證碼,我們再來嘗試一下復雜的。
這個圖片是用前面的程序畫出來的復雜語音驗證碼的波形圖:
從圖里我們可以看到,這段音頻中一直存在一個恒定的噪聲,就是中間橫的藍色的那條,對于這樣的噪聲我們可以用標準的離散傅里葉變換,通過快速傅里葉變換fast Fourier transform(掛在高樹上的注意了!)來解決。
回到多年前校園中的數字信號處理這門課,讓我們在純潔的正弦波 s(t)=sint(w*t)
上疊加一個白噪聲,S(t)=S(t+n)
, F為S的傅里葉變換,把頻率高于和低于w的F值設為0,噪聲就被這樣過濾掉了。
比如這張圖里,正弦波的頻譜域被分離了出來,只要把多余頻率切掉,再逆變換回去就相當于過濾掉部分噪音了。其實自己寫這樣的過濾器實在太蛋疼了,Python有不少音頻處理庫并且自帶降噪濾鏡。
但是就像識別圖形驗證碼一樣,噪音(相當于圖片里的干擾線和噪點)并不是破解語音驗證碼的難點,對于計算機來說,最難的部分還是分割,在復雜的語音驗證碼里,除了主要的人聲之外,背景中還有2,3個人在念叨各種東西,并且音量和主要的聲音差不多,無法通過音量分離,這樣的手段即使對于人類也很難識別的出。
我把目前的代碼放在了https://github.com/debasishm89/hack_audio_captcha
這些代碼還很原始,有很大改進的余地。
我把這個問題報給了Google安全團隊,他們說這個東西就是這樣設計的(苦逼的作者),如果系統懷疑對方不是人是機器的時候會自動提升到高難度驗證碼,目前Google不打算改進這個設計。