1. 程式人生 > >Python 自動化加固流程

Python 自動化加固流程

這裡的加固指的是那種需要把檔案上傳到第三方網站上, 等它加固完成之後再下載下來的場景.這裡就以梆梆加固為例, 通過Python指令碼將這個過程自動化起來.既然牽扯到第三方的網站, 那麼這個指令碼絕大多數的操作都跟網路請求相關. 簡單設計一下指令碼的結構, 先封一個基類出來對外提供一些基礎的網路操作, 方面以後擴充套件使用.子類根據實際的業務場景對外提供功能方法.

根據梆梆加固的業務流程來拆解指令碼需要做的步驟.大致可以分為五步, 分別是登入, 上傳檔案開始加固, 檢測加固狀態, 下載已加固檔案, 刪除應用.下面就詳細介紹每個步驟如果分析如何實現.

登陸梆梆

使用梆梆加固肯定是需要登陸的, 所以要自動化這個流程首當其衝得就是要模擬登陸, 拿到cookie之後儲存起來, 這樣之後請求介面的時候帶著有登陸資訊的cookie就不會被攔截登入了. 在使用urllib2預設的opener是不支援cookie相關操作的, 所以在父類的初始化方法中初始化cookie庫, 構建出支援cookie自定義的opener, 並將其裝載為全域性的opener.

cookie_jar = cookielib.LWPCookieJar()
self.__cookie_support = urllib2.HTTPCookieProcessor(cookie_jar)
opener = urllib2.build_opener(
    self.__cookie_support, urllib2.HTTPHandler)
urllib2.install_opener(opener)

接下來分析一下梆梆的登入流程, 使用chrome抓取登入介面.從圖中可以看到登入介面的url以及post資料的格式. 這裡的賬號和密碼是明文的, randomID經過測試可以設定一個固定的值.

知道了需要什麼資料直接就可以實現子類中的登入方法了.首先將賬號密碼和其他固定的引數先構建出來, 再呼叫父類封裝好的post方法並接受response的返回資訊, 並打印出來.如果返回資料中有”code”:0則代表登入成功.

def login(self, user_name, user_password):
    print 'prepare to login bangbang website...'
    login_params = {'account': user_name,
                    'password': user_password,
                    'randomID'
: '0.6358602885605047', 'randomCode': '', 'rememberChecked': 'false', } result = self.post(self.__login_url, login_params, self.__header) print result if "\"code\":0" in result: self.__logined = True print 'login succeed!' else: print 'login failed!'

根據列印的結果來看是登入成功的. 返回的資料中除了code代表請求狀態之外, 其他的資料這裡都用不到. 由於父類已經初始化了cookielib, 此時使用者的cookie資訊已經儲存起來了.

{
    "code":0,
    "enumlastupdate":0,
    "modelId":0,
    "msg":"https://dev.bangcle.com/Apps/index"
}
login succeed!

上傳檔案開始加固

登入成功之後就要上傳apk檔案讓梆梆對其加固. 先在web端走一下流程, 發現可以分為兩步, 第一步先上傳檔案然後跳轉到一個確認加固頁面, 第二步點選確認加固.通過抓包可以看到檔案上傳的介面請求.

  1. 上傳檔案

    經過測試, 該介面的返回資料如下.這個msg欄位的資料很可疑, 我們繼續往下看.

    {
        "code":0,
        "modelId":0,
        "msg":"583142"
    }

    upload介面請求完成之後緊跟著又請求了一個addapk的介面, 請求引數中多了一個appId的欄位, 而這個值跟post介面返回的msg是一模一樣的, 看來這個msg就是appId.這個值要存起來, 後續還會用到.最後這個介面的資料返回是重定向的確認加固頁面.

  2. 開始加固

    接下來在確認加固頁面點選加固按鈕之後又抓到了一個介面請求.從這個介面的引數中可以看到又多了一個不確定的引數, 是一個token. 找了一下也沒有請求其他的介面了, 最後在addapk的返回資料(確定加固頁面的原始碼)中找到了這個token欄位. 既然找到了token在哪就好處理了, 直接用正則把它的值摘出來就可以了.

    $.ajax( {
        data : {
            authenticityToken:'e085497b0562a74216f57b51544eb3646a4ba981'
        },
        type : "post",
        url : '/home/hideheadertip',
        success : function(data) {
        }
    });

    介面請求成功後返回如下資料, 梆梆後臺開始對apk進行加固.這個時候web端應用列表中可以看到剛才上傳的應用.

    {
        "code":0,
        "modelId":0,
        "msg":"新增成功!"
    }

抓包分析完了流程之後就可以開始用指令碼實現了.這個階段比登入要複雜不少, 牽扯到了三個介面的呼叫, 並且介面之間的資料還都是互相依賴的.

  1. 上傳檔案

    父類中的上傳檔案介面是通過poster實現的, 首先通過poster註冊一個opener, 並將之前的cookie相關資訊新增進該opener中.

    opener = poster.streaminghttp.register_openers()
    opener.add_handler(self.__cookie_support)

    再使用poster的編碼方法將dir引數資料編碼出包含Content-Length和Content-Type的頭資訊以及包含檔案流的資料引數, 併發出請求.

    post_data, post_header = multipart_encode(upload_data)
    print post_header
    request = urllib2.Request(upload_url, post_data, post_header)
    return urllib2.urlopen(request).read()

    回到子類的業務流程. 首先判斷一下當前是否是登入狀態, 然後構建檔案上傳的引數, 請求上傳介面, 根據返回值判斷介面成功狀態.

    if self.__logined is False:
        print 'should be logined!'
        return
    
    upload_params = {'name': file_name,
                     'file': open(file_name, "rb")}
    result = self.upload(self.__upload_url, upload_params)
    print result
    json_result = json.loads(result)
    if 0 != json_result['code']:
        print 'upload apk failed!'
        return
    print 'upload apk succeed!'

    講上傳介面返回的appId儲存起來請求addapk介面, 並且從請求結果中將token資訊摘出來儲存起來.

    self.__app_id = json_result['msg']
    add_params = {}
    add_params['appId'] = self.__app_id
    result = self.get(self.__add_app_url, add_params)
    self.__auth_token = re.findall(r"authenticityToken:'(.+?)'", result)[0]
  2. 開始加固

    在第一步中已經拿到了appId和token, 將它們和預設引數拼接起來呼叫complete介面完成檔案的上傳並且開始加固.

    complete_params = {'appId': self.__app_id,
                       'authenticityToken': self.__auth_token,
                       'appCategoryId': '',
                       'isCeping': 'true',
                       'isReinforce': 'true'
                       }
    result = self.post(self.__complete_app_url, complete_params, self.__header)

檢測加固狀態

在web端開始加固之後會自動跳轉到應用列表頁面, 這時會請求getappinfos介面獲取當前app的資訊. 請求引數沒什麼特別的, 只要登入了這些引數都可以固定起來.

而返回資料中就有很多欄位了, 看下圖中黑色框起來的欄位, 先通過appId找到我們剛才上傳的app, 然後根據自己的理解reinforceStatus這個欄位代表了加固狀態, 8代表的是安檢中.

過一會等加固完成了之後再重新整理一下web頁, 重新找到app資訊. 這次可以看到狀態變成10代表加固完成.

既然有欄位可以判斷加固的狀態, 那就可以通過指令碼輪詢這個介面直到加固完成. 根據以下流程圖完成這部分邏輯並記錄加固狀態.

首先做一些初始化工作:判斷是否登入, 是否上傳成功, 構建預設請求引數. 建立一個60次的迴圈.

check_params = {
    'pageSize': '8',
    'page': '1',
    'param': ''
}
check_count = 0

while check_count <= 60:
    check_count += 1

在迴圈中請求getappinfos介面. 反序列化返回資料到物件中, 並根據code狀態決定是否跳過這次迴圈進入下一次迴圈中.

result = self.get(self.__check_status_url, check_params)
json_result = json.loads(result)

if 0 != json_result['code']:
    print 'check app reinforce status failed!'
    continue

遍歷json物件中的results陣列, 這裡的每一個item就代表一個應用. 從item中的id欄位也就是前面的appId資訊過濾出我們正在操作的應用. 再根據上面對欄位的分析, 判斷reinforceStatus是否完成加固. 如果加固還沒有完成則休眠10s繼續輪詢.

for result_item in json_result['results']:
    if str(result_item['id']) != self.__app_id:
        continue

    if result_item['reinforceStatusCode'] == '10':
        print 'reinforce app reinforce succeed!'
        self.__reinforced = True
        return

if self.__reinforced is False:
    print 'sleeping 10s...'
    time.sleep(10)

下載加固後文件

在web端等加固完成之後就可以下載加固過的apk包. 這裡抓到了兩個介面呼叫. 第一個是請求引數w為appId的downapkcode介面, 返回資料中有很多欄位, 先不管看第二個介面.

{
    "code":0,
    "info":{
        "code":"d7076e3810ad628875af97b58b6fc9953f95122e",
        "model_id":583145,
        "sid":"355d0d0f167b4ffbaf8fa8be27de4fa724238509",
        "user_id":620615
    },
    "modelId":0,
    "msg":""
}

第二個呼叫的就是下載檔案的介面downloadfile, 該介面的引數除了id是appId, type固定之外都是通過第一個介面返回值確定的.

套路都瞭解了之後就先判斷當前是否是登入狀態, 是否加固完成. 然後根據appId請求介面解析結果, 判斷狀態之後獲得sid, user_id和code欄位值.

download_code_params = {
    'appId': self.__app_id,
    'hideDownloadNotice': 'false'
}
result = self.post(self.__download_code_url,
                   download_code_params, self.__header)
print result
json_result = json.loads(result)

集齊引數之後就可以呼叫父類的download方法, 將檔案下載到指定的路徑下.

download_params = {
    'id': self.__app_id,
    'sid': json_result['info']['sid'],
    'user_id': json_result['info']['user_id'],
    'code': json_result['info']['code'],
    'type': '1'
}
self.download(self.__download_apk_url, download_params, downlaod_path)

刪除應用

為什麼走完上面的流程之後要把應用從梆梆應用列表中刪除呢? 因為根據目前梆梆的業務流程, 相同版本的同一個應用只能加固一次.如果要把加固指令碼持續整合起來可能每天都要加固一次.所以我們要每次加固完成之後就把應用刪除掉, 方便下次加固使用.

刪除應用同樣抓包, 定位到引數為appId和token的介面delById. 這些引數資訊我們都有儲存, 直接拿來呼叫就行了.

同樣先判斷是否登入是否上傳成功, 然後拼裝引數執行post, 最後根據返回結果輸出最終狀態就完成整個指令碼的最後一步.

delete_params = {
    'authenticityToken': self.__auth_token,
    'appId': self.__app_id
}
result = self.post(self.__delete_app_url, delete_params, self.__header)

彙總

BangBangHttp類實現了之後在外部就可以通過物件方便得使用加固的功能.也可以將賬號, 密碼, 輸入和輸出這四個屬性作為外部引數傳遞進來, 豐富使用場景.

bang_bang = BangBangHttp()
bang_bang.login('account', 'pwd')
bang_bang.upload_apk('test.apk')
bang_bang.check_reinforce_status()
bang_bang.download_apk('reinforced.apk')
bang_bang.delete_app()