<span id="7ztzv"></span>
<sub id="7ztzv"></sub>

<span id="7ztzv"></span><form id="7ztzv"></form>

<span id="7ztzv"></span>

        <address id="7ztzv"></address>

            原文地址:http://drops.wooyun.org/papers/1524

            from:http://www.debasish.in/2014/04/attacking-audio-recaptcha-using-googles.html

            0x00 背景


            關于驗證碼和驗證碼破解的入門,請看:http://drops.wooyun.org/tips/141

            什么是reCaptcha?

            reCaptchas是由Google提供的基于云的驗證碼系統,通過結合程序生成的驗證碼和較難被OCR識別的圖片,來幫助Google數字化一些書籍,報紙和街景里的門牌號等。

            enter image description here

            reCaptcha同時還有聲音驗證碼的功能,用來給盲人提供服務。

            0x01 細節


            中心思想:

            用Google的Web Speech API語音識別來破解它自己的reCaptcha聲音驗證碼.

            enter image description here

            下面來看一下用來語音識別的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的音頻編輯軟件打開,如圖

            enter image description here

            把第一個數字的聲音復制到新窗口中,然后再重復一次,這樣我們把第一位數字的聲音復制成連續的兩個相同聲音。

            比如這個驗證碼是76426,我們的目的是把7先分離出來,然后讓7的語音重復兩次。

            enter image description here

            最后把這段音頻保存成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 
            

            enter image description here

            很好,服務器成功識別了這段音頻并且返回了正確的結果,下面就需要把這個過程自動化了。

            在自動提交之前,我們需要了解一下數字音頻是處理什么原理。

            這個stackoverflow的問題是個很好的教程:

            http://stackoverflow.com/questions/732699/how-is-audio-represented-with-numbers

            把一個wav格式的文件用16進制編輯器打開:

            enter image description here

            用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()
            

            enter image description here

            這個圖實際上就是聲音的波形圖

            進一步自動化:

            下面這段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

            現在我們已經解決了簡單的音頻驗證碼,我們再來嘗試一下復雜的。

            這個圖片是用前面的程序畫出來的復雜語音驗證碼的波形圖:

            enter image description here

            從圖里我們可以看到,這段音頻中一直存在一個恒定的噪聲,就是中間橫的藍色的那條,對于這樣的噪聲我們可以用標準的離散傅里葉變換,通過快速傅里葉變換fast Fourier transform(掛在高樹上的注意了!)來解決。

            回到多年前校園中的數字信號處理這門課,讓我們在純潔的正弦波 s(t)=sint(w*t)上疊加一個白噪聲,S(t)=S(t+n), F為S的傅里葉變換,把頻率高于和低于w的F值設為0,噪聲就被這樣過濾掉了。

            enter image description here

            比如這張圖里,正弦波的頻譜域被分離了出來,只要把多余頻率切掉,再逆變換回去就相當于過濾掉部分噪音了。其實自己寫這樣的過濾器實在太蛋疼了,Python有不少音頻處理庫并且自帶降噪濾鏡。

            但是就像識別圖形驗證碼一樣,噪音(相當于圖片里的干擾線和噪點)并不是破解語音驗證碼的難點,對于計算機來說,最難的部分還是分割,在復雜的語音驗證碼里,除了主要的人聲之外,背景中還有2,3個人在念叨各種東西,并且音量和主要的聲音差不多,無法通過音量分離,這樣的手段即使對于人類也很難識別的出。

            我把目前的代碼放在了https://github.com/debasishm89/hack_audio_captcha

            這些代碼還很原始,有很大改進的余地。

            0x02 結論


            我把這個問題報給了Google安全團隊,他們說這個東西就是這樣設計的(苦逼的作者),如果系統懷疑對方不是人是機器的時候會自動提升到高難度驗證碼,目前Google不打算改進這個設計。

            <span id="7ztzv"></span>
            <sub id="7ztzv"></sub>

            <span id="7ztzv"></span><form id="7ztzv"></form>

            <span id="7ztzv"></span>

                  <address id="7ztzv"></address>

                      亚洲欧美在线