有次幫某人發spam,找人來填調查。寫了個自動評論的指令碼,但是發出一定數量的評論之後就會遭遇驗證碼,於是決定破解之。
思路也是一般的轉化切割比對,成功率不是很高,不過重試幾次也是可以用的。
雖然已經控制好了頻率,不過最後還是被管理員發現了,直接封了帳號(好在是臨時註冊的),再註冊再封,後來乾脆封IP,
於是不得不給我的vps換了個ip(也好在是免費的),杯具。
閑話休說,言歸正題。
首先是需要取得驗證碼的樣本,以作訓練特徵之用。而要取得驗證碼,首先要類比登入的請求:
usr = 'xx' psw = 'oo'
resp = urllib2.urlopen('https://login.sina.com.cn/sso/login.php?username=%s&password=%s&returntype=TEXT' %
( usr, psw)) cookie = Cookie.SimpleCookie(resp.headers['set-cookie']) headers = { 'Referer': 'http://t.sina.com.cn', 'Cookie': cookie_header(cookie),
'User-Agent': 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.3pre) Gecko/20100405 Firefox/3.6.3plugin1', } def cookie_header(cookie):
ret = '' for v in cookie.values(): ret += "%s=%s; " % (v.key, v.value) return ret
headers就是後續的請求中,需要傳遞的參數了。
取回一些驗證碼樣本:
for i in xrange(100):
req_img = urllib2.Request('http://t.sina.com.cn/pincode/pin.php?lang=zh&r=%d&rule' % int(time() * 1000),
headers = headers) res_img = urllib2.urlopen(req_img) f = open('xinlang_pincode/%d.png' % i, 'wb') f.write(res_img.read()) f.close()
有一些驗證碼的回答是中文,中國首都什麼的,這些不處理,直接返回失敗。因為可以重複擷取重新識別,不成問題的。下面是處理算術問題驗證碼的方法:
先進行變換處理:
from PIL import Image, ImageFilter, ImageEnhance file = 'xinlang_pincode/0.png' im = Image.open(file) im = im.convert()
enhancer = ImageEnhance.Brightness(im) im = enhancer.enhance(2.0) #加亮,效果見圖1 enhancer = ImageEnhance.Contrast(im) im = enhancer.enhance(4)
#提高對比,效果見圖2 im = im.convert('1') #二值化,效果見圖3 im = im.filter(ImageFilter.MedianFilter) #中值去噪,效果見圖4 im.show()
#調用xv命令來顯示圖片,方便debug
圖1:
圖2:
圖3:
圖4:
這樣處理過之後,圖片背景中的色塊被過濾掉,雜點也被過濾掉,而數位形狀也沒有太大的損失。
下面是分解字元,也就是將每一個數字或者+-*等符號分解出來:
imim = im.load() WIDTH = 250 HEIGHT = 50 i = 0 has_start = False chars = [] while i < WIDTH: all_none = True for j in xrange(HEIGHT): if imim[i, j] != 255:
all_none = False if all_none: if has_start: end_x = i has_start = False char = im.crop((start_x, 0, end_x, HEIGHT))
char.show() #到這一步的效果見圖5 charchar = char.load() width = end_x - start_x y1 = 0 y2 = HEIGHT - 1 all_none = True
while all_none: for ii in xrange(width): if charchar[ii, y1] != 255: all_none = False y1 += 1 all_none = True while all_none:
for ii in xrange(width): if charchar[ii, y2] != 255: all_none = False y2 -= 1 char = char.crop((0, y1 - 1, width, y2 + 2))
char = char.resize((20, 20)) #將圖片縮放到統一的大小 char.show() #到這一步的效果見圖6 chars.append(char) else: if not has_start:
start_x = i has_start = True i += 1
圖5:字元被獨立分割開
圖6:字元上下兩邊的空白被去掉,且縮放到同一大小
這一步得到的chars是下面要用到的。
然後是訓練,也就是形成特徵庫。特徵庫規模越大,識別率也越高。不過訓練起來也挺累的,有幾十上百條也就好了。至少0到9和+-*=等幾個字元的特徵都要有:
file = open('xinlang.img', 'a') for c in chars: nstr = '' im_loaded = c.load() for x in range(20): for y in range(20): if im_loaded[x, y] == 255:
nstr += '0' else: nstr += '1' c.show() n = raw_input('? ') file.write(nstr+':'+n+'\n') file.close()
這裡的特徵,就是直接把每一個像素的資訊,用0和1組成的字串表示。
訓練的結果是一個文字檔,記錄了對應的特徵和字元,用於下面的比對。
比對函數:
pattern = [] for l in open('xinlang.img', 'r').read().split('\n'): pattern.append(l.split(':')) del pattern[-1] def what(img): im = img.load() nstr = ''
for x in xrange(20): #產生靶心圖表像的特徵字串 for y in xrange(20): if im[x, y] == 255: nstr += '0' else: nstr += '1' minmin = 400
res = None for p in pattern: cur = 0 for i in xrange(400):
if nstr[i] != p[0][i]: #比對每一個像素,如果不相同,則增加差異值 cur += 1 if cur < = minmin: #記錄下差異值最小時所對應的字元
minmin = cur res = p[1] return res
最後測試一下:
for c in chars: print what(c),
結果:
可以看到18+18這些字元可以成功識別。那為什麼=和?識別不了呢?因為我沒有訓練這兩個字元,而=和?都和數字2的特徵最接近 -_,-|
這個驗證碼還是挺好破解的,因為字元之間間距很大,而且沒有旋轉,沒有扭曲,不需要多少變換就能得到可用的結果。像google的那種,就完全沒法可想了。