基於設備指紋零感驗證系統

做者: 我是小三
博客: http://www.cnblogs.com/2014asm/
因爲時間和水平有限,本文會存在諸多不足,但願獲得您的及時反饋與指正,多謝!html

工具環境: android 4.4.四、IDA Pro 7.0、jeb三、sklearn機器學習庫
目錄 :
爲何是零感驗證?驗證碼的發展史
實現原理與架構流程
SDK自身安全
環境與算法安全
服務端風控與安全
總結java

0x00:爲何是零感驗證?驗證碼的發展史。

爲何需要零感驗證?

爲何叫零感驗證:零的起源是來自於印度,它深受佛教大乘空宗的影響,意爲「空」,它表示「沒有」這個量,表明起點亦是終點。
與較傳統驗證碼相比,用戶無需輸入操做,只需按照正常邏輯登陸就可進行驗證,極大提高用戶體驗。
主要有三大優勢,分別是用戶體驗,風險識別,風險攔截。下面舉例說明。
先來看一個場景,在遊戲業務中,爲了下降用戶體驗門檻,在打開遊戲時能夠不要求用戶註冊,也就是說拿不到帳戶角度的用戶信息。這時識別用戶設備就很重要,不然在後端沒法分辨哪些是同一個用戶的數據。要在未登陸狀態時追蹤用戶。
用戶體驗:
零感驗證型驗證碼針對大多數的用戶可以無需思考,直接經過。不存在業務和流程的打斷,體驗流暢,對用戶體驗的提高毋庸質疑。
風險識別:
由於隨着機器學習的發展讓機器掌握人類具備的知識也再也不是難點,無知識型驗證碼再也不基於知識來挑戰機器,而是基於人類的固有行爲特徵以及操做的環境信息綜合進行風控決策,攻擊者難以批量的模擬出能夠欺騙風控引擎的正常人類的的操做。
風險攔截:
普通的驗證碼基於知識對機器發起挑戰,沒法作到對機器進行阻斷。由於知識的挑戰還須要兼顧人類的體驗,機器經過的機率只能作到無限的下降而沒法消除。而零感驗證型驗證碼基於後端的風控決策,能夠對不一樣風險的操做提出更高難度的驗證碼乃至阻斷,有更大空間對風險進行消除和攔截。android

驗證碼的發展史:

驗證碼是在2002年由提出者路易斯·馮·安(Luis von Ahn)和他的小夥伴在卡內基梅隆第一次提出了CAPTCHA(驗證碼)這個概念。該方式是指向請求的發起方提出問題,能正確回答的便是人類,反之則爲機器。這個程序基於這樣一個重要假設:提出的問題要容易被人類解答,而且讓機器沒法解答。
在當時機器計算能力弱小的的條件下,要識別扭曲的圖形,對於機器來講仍是一個很艱難的任務,而對於人來講,則相對能夠接受。yahoo在當時第一個應用了圖形化驗證碼這個產品,很快解決了yahoo郵箱上的垃圾郵件問題,所以圖形類驗證碼一直使用至今。
隨着圖片識別技術的發展與打碼平臺的出現,圖片驗證碼的開始沒落。圖片類驗證碼也愈來愈複雜,用戶體驗很是不友好
後來又出現了基於人類固有的生物特徵以及操做的環境信息綜合決策,來判斷是人類仍是機器。無知識型驗證碼最大特色即無需人類思考,從而不會打斷用戶操做,進而提供更好的用戶體驗。redis

0x02:實現原理與架構流程

零感驗證主要經過採集設備指紋、行爲特徵、訪問頻率、用戶登陸行爲、地理位置等信息進行模型分析與規類,有效的攔截惡意登陸、批量註冊,阻斷機器操做,攔截非正經常使用戶,較傳統驗證碼相比,用戶無需再通過思考或輸入操做,只需點擊登陸便可進行驗證。通過後臺鑒別爲正常的用戶,既爲企業提供了安全保障也讓用戶無感知經過,極大提高用戶體驗。
總體的框架流程以下:算法

0x03:技術細節分析

1.將圍繞以下總體技術架構圖來作分析:mongodb

2.刷單是怎麼實現的:

目前大部分APP開發中常須要獲取設備的硬件信息作爲識別設備基礎,以應對刷單,目前經常使用的幾個設備識別碼主要有IMEI、Android_id、IDFA、不過Android6.0以後須要權限才能獲取,並且這些硬件信息很容易被Hook篡改,可能並不靠譜,另外也能夠經過MAC地址或者藍牙地址,序列號等,以下:
IMEI : (International Mobile Equipment Identity) 、IDFA、MAC 或者藍牙地址
Serial Number(須要從新刷flash才能更新)
AndroidId ANDROID_ID是設備第一次啓動時產生和存儲的64bit的一個數,手機升級,或者被wipe後該數重置
以上是經常使用的設備識別碼,系統也提供了詳情的接口讓開發者獲取,可是因爲都是上層方法,很容易被Hook篡改,尤爲是有些專門刷單的,在手機Root/越獄以後,利用HOOK框架裏的一些插件很容易將獲取的數據給篡改,達到刷單的目的。
因此爲了能獲取相對準確的設備信息咱們須要採起相應的應對措施,能夠採用一些系統底層隱藏的接口來獲取設備信息,隱藏的接口不太容易被篡改,或者自定義一些相對安全的採集數據底層接口。
以下圖自行封裝的接中:數據庫

3.SDK自身安全:

做爲一個安全類的sdk產品,自身的安全與防護能力也是很重要的,否則被分析邏輯而後hook接口篡改參數就能夠作到必定程度的破解。
防逆向:經過上面介紹,採集數據主要經過本身封裝接口來實現,而後再混淆關鍵邏輯代碼,防止經過IDA,readelf等工具對so裏面的邏輯進行分析。未經混淆的代碼受到攻擊後,易暴露程序中關鍵算法、核心業務邏輯、數據結構和模塊的控制流佈局等敏感內容,代碼混淆方面主要功能以下:
控制流平坦化:在保證不改變源代碼功能的前提下,將C、C++、Objective-C等語言中的if、while、for、do等控制語句轉化爲switch分支選擇語句。
插入各類花指令與指令內聯:插入各類不會被執行的無效字節碼,使逆向分析工具進行字節碼解析時出錯。再將本身封裝的接口代碼指令內聯分散,增長分析難度。
控制流變換:對於跳轉控制條件和分支語句,在保持原程序邏輯關係的前提下,隨機肯定控制塊的執行順序,達到模糊程序控制邏輯、隱藏程序控制流的目的。
代碼完整性校驗:在混淆源碼時植入crc校驗,在程序執行時校驗同因子映射對應的代碼,保證代碼執行時的完整性,防止被下軟斷點調試與被HOOK風險。
字符串加密:對程序中字符串加密處理,對抗靜態邏輯分析。後端

4.環境與算法安全:

環境檢測:
模擬器與多開器由於易用可複製性在黑產刷單中被頻繁使用,如何準確的識別模擬器與多開器也是SDK的一個重要模塊。
模擬器檢測:系統屬性與硬件信息,好比:IMEI是否所有爲0000000000格式,系統庫存是否有模擬器特徵,文件系統差別的特徵,獲取cpu信息判斷是x86仍是ARM。
ARM與X86在架構上有很大區別,ARM採用的哈弗架構將指令存儲跟數據存儲分開,與之對應的,ARM的一級緩存分爲I-Cache(指令緩存)與D-Cahce(數據緩存),而X86只有一塊緩存,而模擬器採用的是模擬x86架構,若是咱們將一段可執行代碼動態映射到內存,在執行的時候,X86架構上動態修改這部分代碼後,指令緩存會被同步修改,而ARM修改的倒是D-Cahce中的內容,此時I-Cache中的指令並不必定被更新,這樣,程序就會在ARM與x86上有不一樣的表現,根據計算結果即可以知道到底是X86還在ARM平臺上運行。
測試代碼:主要就是將地址e2844001的指令add r4, r4,#1,在運行中動態替換爲e2877001的指令add r7, r7, #1,在ARM-V7架構上測試,ARM採用的是三級流水,PC值=當前程序執行位置+8。數組

8410:       e92d41f0        push    {r4, r5, r6, r7, r8, lr}
8414:       e3a07000        mov     r7, #0
8418:       e1a0800f        mov     r8, pc      // 本平臺針對ARM7,三級流水  PC值=當前程序執行位置+8
841c:       e3a04000        mov     r4, #0
8420:       e2877001        add     r7, r7, #1
842c:       e1a0800f        mov     r8, pc
8430:       e248800c        sub     r8, r8, #12   // PC值=當前程序執行位置+8
8434:       e5885000        str     r5, [r8]
8438:       e354000a        cmp     r4, #10
843c:       aa000002        bge     844c <out>

若是是在ARM上運行,e2844001處指令沒法被覆蓋,最終執行的是add r4,#1,而在x86平臺上,執行的是add r7,#1
多開檢測:目前市面上的多開App的原理相似,都是以新進程運行被多開的App,並hook各種系統函數,使被多開的App認爲本身是一個正常的App在運行。目前主流的多以開平行空間、VirtualApp等形式來作。
檢測files目錄路徑:咱們知道App的私有目錄是/data/data/包名/或/data/user/用戶號/包名,經過Context.getFilesDir()方法能夠拿到私有目錄下的files目錄。在多開環境下,獲取到目錄會變爲/data/data/多開App的包名/xxxxxxxx或/data/user/用戶號/多開App的包名/xxxxxxxx。
uid檢測::在Android系統中會爲每個apk分配的一個應用的標誌,每個APP對應一個uid。由於虛擬化並無真正的安裝應用,所以uid一定是和宿主一致的。咱們的檢測方法就是若是知足同一uid下的兩個進程對應的包名,在"/data/data"下有兩個私有目錄,則該應用被多開了。
進程模塊檢測:讀取/proc/self/maps,多開App會加載一些本身的so到內存空間,經過對各類多開App的包名的匹配,若是maps中有多開App的包名的東西或模塊,那麼當前就是運行在多開環境下。獲取自身加載的模塊路徑。
算法安全:
算法代碼虛擬化:虛擬算法指令隨機化、非線性化操做,進行隨機映射編碼,隱藏相關內容,增強防護黑客對算的法攻擊。好比咱們SDK中的用戶登陸行爲算法被逆向清楚就可能被模擬了。
什麼是代碼虛擬化?
虛擬機的代碼保護也能夠算是代碼混淆技術的一種。代碼混淆的目的就是防止代碼被逆向分析,可是全部的混淆技術都不是徹底不能被分析出來,只是增長了分析的難度或者加長了分析的時間,雖然這些技術對保護代碼頗有效果,可是也存在着反作用,好比會或多或少的下降程序效率,這一點在基於虛擬機的保護中格外突出,因此大多基於虛擬機的保護都只是保護了其中比較重要的部分。因此咱們對關鍵算法部分進行保護。
虛擬機的保護技術中,一般自定義的字節碼與native指令都存在着映射關係,也就是說一條或多條字節碼對應於一條native指令。至於爲何須要多條字節碼對應同一條native指令,這樣就能夠作到每次都不能的算法指令,實際上是爲了增長虛擬機保護被破解的難度,這樣在對被保護的代碼進行轉換的時候就能夠隨機生成出多套字節碼不一樣,但執行效果相同的程序,致使逆向分析時的難度增長。
大體代碼以下圖:緩存

用戶登陸行爲:
舉一個簡單的示例:
好比算用戶按鍵行爲時間差,假設第i個按鍵按下與彈起的時間分別爲Downi與UPi;第二步,計算第i個字符的按鍵時長Downi與間隔時長UPTi,計算方法爲Downi=UPi-Downi,即按下與彈起的時間之差。這只是行爲的一小部分。
設備惟一性:
在移動互聯網時代新零售、電商、遊戲、獲取設備可信的惟一id是一個常見的業務需求,可是在app推廣拉新過程當中,長期存在着新用戶免費的業務邏輯。可觀的利潤使得大量的黑產蜂擁而至。刷機,hook篡改,設備農場等手段層出不窮,無所不用其極。因此上面咱們作的各類反逆向手段就是爲了保證SDK自身的安全性,這樣才能更好地保證全獲取設備惟一的方案安全。
零感驗證產品工做流程大體以下,事前->事中->過後->結果反饋

經過SDK採集設備硬件參數、系統配置、網絡環境、傳感器、信號等多維度的設備信息,服務器後臺模型算法對採集的數據進行自動分析計算、生成做弊風險、僞造風險、應用風險、設備屬性等多個維度的風險標籤。
若是對惟一ID感性趣能夠閱讀這篇文章:http://www.javashuo.com/article/p-czhoowoa-dd.html

5.服務端風控與安全:

上面提到的惟一id的生成策略風控等全部的生成規則都是放到服務器處理的,這樣反做弊的策略也能夠最快時間響應。
服務器端總體架構以下圖:

服務器端用Spring Boot框架開發的服務,服務器端會有需要實時計算的功能,要將任意維度的歷史數據(可能半年或更久)實時統計出結果,因爲數據的維度數量不固定的,選取統計的維度也是隨意的,因此不能在關係數據庫中建幾個索引就能搞定的,須要利用空間換時間,來下降時間複雜度。目前採起的方案是redis加mongodb,redis中數據結構sortedset,是個有序的集合,集合中只會出現最新的惟一的值。利用sortedset的自然優點,作頻數統計很是有利。mongodb自己的聚合函數統計維度,支持不少好比:max,min,sum,avg,first,last,標準差,採樣標準差,複雜的統計方法能夠在基礎聚合函數上創建。redis性能優於mongodb,因此使用場景較多的頻數計算默認在redis中運行,可是redis爲了性能犧牲了不少空間,數據重複存儲,會佔用不少內存。基本代碼以下:

/**
     * @param event          事件
     * @param condDimensions 條件維度數組,注意順序
     * @param enumTimePeriod 查詢時間段
     * @param aggrDimension  聚合維度
     * @return
     */
    public int addQueryHabit(Event event, String[] condDimensions, EnumTimePeriod enumTimePeriod, String aggrDimension) {
        if (event == null || ArrayUtils.isEmpty(condDimensions) || enumTimePeriod == null || aggrDimension == null) {
            logger.error("參數錯誤");
            return 0;
        }
        Date operate = event.getOperateTime();
        String key1 = String.join(".", String.join(".", condDimensions), aggrDimension);
        String[] key2 = new String[condDimensions.length];
        for (int i = 0; i < condDimensions.length; i++) {
            Object value = getProperty(event, condDimensions[i]);
            if (value == null || "".equals(value)) {
                return 0;
            }
            key2[i] = value.toString();
        }
        String key = event.getScene() + "_sset_" + key1 + "_" + String.join(".", key2);

        Object value = getProperty(event, aggrDimension);
        if (value == null || "".equals(value)) {
            return 0;
        }

        int expire = 0;
        String remMaxScore = "0";
        if (!enumTimePeriod.equals(EnumTimePeriod.ALL)) {
            //若是須要過時,則保留7天數據,知足時間段計算
            expire = 7 * 24 * 3600;
            remMaxScore = dateScore(new Date(operate.getTime() - expire * 1000L));
        }

        Long ret = runSha(key, remMaxScore, String.valueOf(expire), dateScore(operate), value.toString(), dateScore(enumTimePeriod.getMinTime(operate)), dateScore(enumTimePeriod.getMaxTime(operate)));
        return ret == null ? 0 : ret.intValue();
    }


    /**
     * 計算sortedset的score
     *
     * @param date
     * @return
     */
    private String dateScore(Date date) {
        return new SimpleDateFormat("yyyyMMddHHmmss").format(date);
    }


    private Object getProperty(Event event, String field) {
        try {
            return PropertyUtils.getProperty(event, field);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }


    /**
     * 事件入庫
     *
     * @param event
     */
    public void insertEvent(Event event) {
        mongoDao.insert(event.getScene(), Document.parse(JSON.toJSONString(event), new DocumentDecoder()));
    }

    /**
     * 可疑事件入庫
     *
     * @param event 事件bean
     * @param rule  觸發的規則詳情
     */
    public void insertRiskEvent(Event event, String rule) {
        Document document = Document.parse(JSON.toJSONString(event), new DocumentDecoder());
        document.append("rule", rule);
        mongoDao.insert(riskEventCollection, document);
        logger.warn("可疑事件,event={},rule={}", JSON.toJSONString(event), rule);
    }

    public int count(Event event, String[] condDimensions, EnumTimePeriod enumTimePeriod) {
        if (event == null || ArrayUtils.isEmpty(condDimensions) || enumTimePeriod == null) {
            logger.error("參數錯誤");
            return 0;
        }

        Document query = new Document();
        for (String dimension : condDimensions) {
            Object value = getProperty(event, dimension);
            if (value == null || "".equals(value)) {
                return 0;
            }
            query.put(dimension, value);
        }

        query.put(Event.OPERATETIME, new Document("$gte", enumTimePeriod.getMinTime(event.getOperateTime())).append("$lte", enumTimePeriod.getMaxTime(event.getOperateTime())));

        return mongoDao.count(event.getScene(), query);
    }

    /**
     * db.applogin.aggregate(
     * [
     * {$match:{mobile:"13900009725", operateTime: { $gte: new Date(1467213873277) }}},
     * {$group:{_id:null,_array:{$addToSet: "$operateIp"}}},
     * {$project:{_num:{$size:"$_array"}}}
     * ]
     * )
     **/
    private int distinctCountWithMongo(Event event, String[] condDimensions, EnumTimePeriod enumTimePeriod, String aggrDimension) {
        if (event == null || ArrayUtils.isEmpty(condDimensions) || enumTimePeriod == null || aggrDimension == null) {
            logger.error("參數錯誤");
            return 0;
        }

        Document query = new Document();
        for (String weido : condDimensions) {
            Object value = getProperty(event, weido);
            if (value == null || "".equals(value)) {
                return 0;
            }
            query.put(weido, value);
        }

        query.put(Event.OPERATETIME, new Document("$gte", enumTimePeriod.getMinTime(event.getOperateTime())).append("$lte", enumTimePeriod.getMaxTime(event.getOperateTime())));

        return mongoDao.distinctCount(event.getScene(), query, aggrDimension);
    }

    private int distinctCountWithRedis(Event event, String[] condDimensions, EnumTimePeriod enumTimePeriod, String aggrDimension) {
        return addQueryHabit(event, condDimensions, enumTimePeriod, aggrDimension);
    }

    /**
     * 計算頻數,有2種方式,這裏考慮性能,採用redis方式
     *
     * @param event
     * @param condDimensions
     * @param enumTimePeriod
     * @param aggrDimension
     * @return
     */
    public int distinctCount(Event event, String[] condDimensions, EnumTimePeriod enumTimePeriod, String aggrDimension) {
        return distinctCountWithRedis(event, condDimensions, enumTimePeriod, aggrDimension);
    }

    public List distinct(Event event, String[] condDimensions, EnumTimePeriod enumTimePeriod, String aggrDimension) {
        if (event == null || ArrayUtils.isEmpty(condDimensions) || enumTimePeriod == null || aggrDimension == null) {
            logger.error("參數錯誤");
            return null;
        }

        Document query = new Document();
        for (String dimension : condDimensions) {
            Object value = getProperty(event, dimension);
            if (value == null || "".equals(value)) {
                return null;
            }
            query.put(dimension, value);
        }

        query.put(Event.OPERATETIME, new Document("$gte", enumTimePeriod.getMinTime(event.getOperateTime())).append("$lte", enumTimePeriod.getMaxTime(event.getOperateTime())));
        return mongoDao.distinct(event.getScene(), query, aggrDimension);
    }

初始密鑰與虛擬機算法code

@RequestMapping(value = "/initreq", method = RequestMethod.POST)
    public Result<String> initreq(@RequestBody String init) {
        String ret = null;
        String base64tmp = null;
        String tempaes = null;
        byte[] base64outbufer;
        String m_base64outbufer;
        byte[] aseout = {0x00};
        String aeskey = "tdf5df0kljhlnhbd";     //aeskey
        String pub_key ="-----BEGIN PUBLIC KEY-----\n" +
                "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCRwBcxeI0LTFJrBevaMSV2B5mj\n" +
                "WF51b/VAmAb76L1IVQJx1JjCSI25G3P5omdPzS7Mbe2rlyHwOWjS3A6V6YiEYtwh\n" +
                "JcAM7Z+gbwzCbjPSd/N+ONrmCwJcmj5xQky1prvtZhfxRRdd89fHm8yZ9JKO/kpX\n" +
                "R/v2BSDl+q89aQmxmwIDAQAB\n" +
                "-----END PUBLIC KEY-----";//rsa公鑰
        String requestId = null;          //本次請求ID
        String m_data = null;
        String vcode = null;
        logger.info("init:"+init);
        base64tmp = init.replace("datainfo=", "");
        try {
            base64tmp = java.net.URLDecoder.decode(base64tmp, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        //base64tmp = base64tmp.replace("%3D%3D", "==");
        //base64tmp = base64tmp.replace("\r", "");
        //base64tmp = base64tmp.replace("\n", "");
        logger.info("post:"+base64tmp);
        logger.info("post:"+base64tmp.length());
        Result r = Result.success();
        try {
            if (StringUtils.isEmpty(base64tmp)) {
                throw new RCRuntimeException(CodeMap.PARAM_ERROR);
            }
           /* Event event = EventFactory.build(tempaes);

            if ("ON".equals(configService.query("SWITCH_RC"))) {
                kieService.execute(event);
            }*/

            //base64解密
            base64outbufer = Base64.decode(base64tmp.getBytes());
            //去掉先後固定字符
            String strbase64 = new String(base64outbufer);
            logger.info("strbase64_1:"+strbase64);
            //返回算法隨機bycode碼,與密鑰

            requestId = ParamAlgorithm.GetRequestId();
            logger.info("requestId:"+requestId);
            r.setrequestId(requestId);

            /*
            保證每個手機生成算法都不同,
            除了ID的安全外還有算法的安全來作保證,
            黑產大量收集了ID也是沒用的,還需要把算法分析清楚才能進行攻擊
            算法每一次都是不同的,隨機變化
             */
            vcode = ParamAlgorithm.GetVcode();
            logger.info("vcode:"+vcode);
            //r.setVcode(vcode);

            m_data = ParamAlgorithm.GetData(aeskey, pub_key, vcode);
            logger.info("m_data:"+m_data);
            r.setData(m_data);

        } catch (RCRuntimeException e) {
            r = Result.fail();
            r.setRetCode(e.getId());
        } catch (Exception e) {
            logger.error("業務風控初始化失敗!", e);
            r = Result.fail();//清空數據
        }
        return r;
    }

生成惟一ID

@RequestMapping(value = "/GenID", method = RequestMethod.POST)
    public Result<String> GenID(@RequestBody String devinfo) {
        String ret = null;
        String base64tmp = null;
        byte[] base64outbufer;
        String m_base64outbufer;
        String requestId = null;          //本次請求ID
        String deviceid = null;
        String m_data = null;
        String imei = null;
        String android = null;
        String mac = null;
        String disksn = null;
        byte[] aseout = {0x00};
        logger.info("post:"+base64tmp);
        base64tmp = devinfo.replace("datainfo=", "");
        base64tmp = base64tmp.replace("%3D%3D", "==");
        base64tmp = base64tmp.replace("\r", "");
        base64tmp = base64tmp.replace("\n", "");
        logger.info("post:"+base64tmp);
        logger.info("post:"+base64tmp.length());
        Result r = Result.success();
        try {
            if (StringUtils.isEmpty(base64tmp)) {
                throw new RCRuntimeException(CodeMap.PARAM_ERROR);
            }
            //解析參數,生成惟一ID
            //base64解密
            m_base64outbufer = Base64.sdecode(base64tmp);
            //去掉先後固定字符
            String strbase64 = new String(m_base64outbufer);
            strbase64 = new String(strbase64.getBytes(),"UTF-8");;
            logger.info("strbase64_1:"+strbase64);
            strbase64 = strbase64.replace("seid;","").trim();
            strbase64 = strbase64.replace("wdfw","").trim();
            logger.info("strbase64_2->"+strbase64);
            //逆轉
            strbase64 = ParamAlgorithm.Reverse(strbase64.toCharArray(), strbase64.length());
            logger.info("Reverse->"+strbase64);
            base64outbufer = Base64.decode(strbase64.getBytes());
            String tempaes= new String(base64outbufer);
            logger.info("tempaes->"+tempaes);
            /*Event event = EventFactory.build(tempaes);
            if ("ON".equals(configService.query("SWITCH_RC"))) {
                kieService.execute(event);
            }*/
            requestId = ParamAlgorithm.GetRequestId();

部分風控使用規則,使用drools規則引擎管理風控規則,這樣原則上能夠動態配置規則。好比1分鐘內某帳號的登陸次數,能夠用來分析盜號等,頻數統計,好比1小時內某ip上出現的帳號,能夠用來分析黃牛黨等,某時間段,能夠是多個維度組合,利用統計方法統計結果維度的值,以判斷 ip爲例,規則以下:

rule "login_ip"
    salience 98
    lock-on-active true
    when
        event:LoginEvent()
    then
        int count  = dimensionService.distinctCount(event,new String[]{LoginEvent.OPERATEIP},EnumTimePeriod.LASTHOUR,LoginEvent.MOBILE);
        if(event.addScore(count,20,10,1)){
            dimensionService.insertRiskEvent(event,"近1小時內同ip出現多個mobile,count="+count);
        }
        count = dimensionService.count(event,new String[]{LoginEvent.OPERATEIP},EnumTimePeriod.LASTMIN);
        if(event.addScore(count,20,10,1)){
             dimensionService.insertRiskEvent(event,"近1分鐘同ip登錄頻次,count="+count);
        }
end

再總結下服務端流程:設備信息->黑白名單->風控規則->閾值預警->保存事件。

0x04:總結

最後再來總結下零感驗證系統總體流程,以下圖所示:

該方案能給業務方便的同時還能保證業務的相對安全,相比較於傳統圖形驗證碼,安全用戶無感知經過,提高體驗,下降流失。
在風控方面結合了設備指紋、行爲特徵、訪問頻率、登陸行爲等特徵,有效的攔截惡意登陸、批量註冊,阻斷機器操做,攔截非正經常使用戶。
若是黑產對對手機的imei和mac等信息作了更改行爲,可是經過其它維度的信息檢測,還能夠識別出是原來的用機,而不認爲是一個新的設備。
提升做弊成本。世上沒有完美產品與解決方案,攻防的本質是成本的較量,可是黑產行爲必定是要講投入產出比例的。其實咱們沒有辦法從根本上屏蔽做弊行爲,可是若是讓想要做弊黑產提升不可接受的時間成本和資金成本,那咱們就成功了。
目前主要以黑白名單,ip,設備指紋爲主,行爲、能夠擴展更多維度信息,好比地域運營商,ip地域運營商,ip出口類型,徵信等,維度越多,行爲用戶行爲特徵、能夠創建規則越多,風控越精準;
擴展風控規則,針對須要解決的場景問題,用戶行爲特徵、添加特定規則,分值也應根據自身場景來調整。
將用戶的行爲軌跡綜合考慮,創建複合場景的規則條件。減小漏報和誤報。這將是個漫長打磨的過程。給出一個demo樣例。

最後感謝看完本文,歡迎掃碼關注公衆號:

相關文章
相關標籤/搜索