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


作者: 我是小三
博客: http://www.cnblogs.com/2014asm/
由於時間和水平有限,本文會存在諸多不足,希望得到您的及時反饋與指正,多謝!

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

0x00:為什么是零感驗證?驗證碼的發展史。

為什么須要零感驗證?

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

驗證碼的發展史:

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

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

零感驗證主要通過采集設備指紋、行為特征、訪問頻率、用戶登錄行為、地理位置等信息進行模型分析與規類,有效的攔截惡意登錄、批量注冊,阻斷機器操作,攔截非正常用戶,較傳統驗證碼相比,用戶無需再經過思考或輸入操作,只需點擊登錄即可進行驗證。經過后台鑒別為正常的用戶,既為企業提供了安全保障也讓用戶無感知通過,極大提升用戶體驗。
整體的框架流程如下:

0x03:技術細節分析

1.將圍繞如下整體技術架構圖來做分析:

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感性趣可以閱讀這篇文章:https://www.cnblogs.com/2014asm/p/10884489.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樣例。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM