• 목록
  • 아래로
  • 위로
  • 1
  • Hanam09
  • 조회 수 114

안녕하세요. Hanam09라고 하옵니다.

 

오늘은 Python3을 이용해 네이버 로그인을 해보는 코드를 만들어 봅시다.

 

뭐 로그인하는거 별거 없다고 생각하실 수도 있습니다만, 네이버 로그인은 다른 사이트하고는 다른 독특한 방식으로 로그인 합니다.

 

image.png.jpg

<그림 1> 크고 아름다운 네이버의 로그인 제출 값

 

여기에서 보니 우리가 작성한 ID와 패스워드는 눈 씻고 찾아봐도 보이지 않습니다. 근데 네이버는 어떻게 우리가 입력한 아이디와 비밀번호를 알 수 있을까요? 바로 네이버가 우리의 아이디와 패스워드를 암호화 하고 서버에서 개인키를 통해 풀기 때문입니다.

 

image.png.jpg

<그림 2> Naver의 common200807v2.js 中 encryptIdPwSplit 함수

 

이렇게 가지고 있는 RSA 공개키값 e와 N으로 공개키를 설정하고 그 키로 사용자의 아이디와 비밀번호, 세션키를 암호화하게 됩니다.

이는 다음 카카오 또한 비슷한 방식을 취하고 있습니다. 그럼 이걸가지고 로그인 하는것이 끝이냐? 아닙니다. 네이버는 이거 말고도 또 다른 특징이 있습니다. 이걸 말하기 전에, 왜 네이버의 로그인 페이지는 HTTPS가 적용되어 있는데도 불구하고 이렇게 귀찮게 이용자의 ID와 패스워드를 암호화할까요? 

 

제가 아는 한, 그 이유는, 5년 전만해도 암호화 비용을 이유로 HTTPS를 적용하지 않았기 때문입니다. 여기서 암호화 비용이라 함은 TLS 인증서가 아닌 (RSA 또는 ECC)+(AES 또는 3DES) 알고리즘을 이용해 데이터를 암호화하는데 들어가는 서버의 부담을 말합니다. 지금은 웹 브라우저 벤더사들과(특히 구글) Let's Encrypt 덕분에 HTTPS 열풍(?)이 불고 무로 DV 인증서 발급 방법을 많은 사람들이 알게되면서 현대의 웹 사이트들 대부분이 서버에 TLS인증서를 적용하여 암호화 통신을 지원하고 있지만, 그때 당시만 해도 대한민국의 많은 사이트들이 TLS인증서를 적용하지 않았습니다. 지금은 다행히도 이게 NAVER 등을 비롯한 각종 대한민국 포털사이트들에게까지도 (사실은 웹 브라우저 벤더 회사의 압박때문에..) 퍼져서 이제는 HTTPS를 적용하지 않은 사이트를 보기 힘들정도 입니다. (개인적으로 정말 좋은 변화 였다고 생각합니다.) 아무튼, 저 과거의 소스를 이용한 RSA의 암호화 방식이 지금까지 이어져 온것 입니다.

 

본론으로 돌아와서, js 소스코드를 알았으니 이걸 그대로 파이썬으로 옮겨서 정보를 암호화하고 그대로 로그인 하면 되겠네요...?

 

아쉽지만, 아닙니다. 아까 네이버 로그인 방식에 또 다른 특징이 있다고 했잖아요? 그게 뭐냐면, 자동 로그인 방지를 위한 네이버의 야심작(?), bvsd 라는 사람 행동 탐지기(?)입니다. 아까 보여드린 첫번째 사진의 name 값 중에 bvsd라는것이 있었죠? 저게 뭘 의미하는지는 모르겠지만, 저게 약간이라도 이상하면 캡챠가 우리를 반겨줍니다. 

 

결론부터 말하자면, 이 bvsd의 값은 로그인 과정에서 있었던 마우스 움직임, 터치, browser fingerprint, 기기 움직임 등을 기록하여 서버에 같이 보내서 이게 사람이 로그인 하는건지 판단합니다. 미심쩍으면 캡챠로 다시 체크하고요. 한번 봅시다.

 

image.png.jpg

<그림 3> bvsd1.3.4.min.js 의 bvsd_data 값 생성 부분

 

딱 봐도 보이죠? 각 부분의 역할은 자명합니다. 이것이 바로 bvsd_data 부분인데요. 이 값이 압축 및 인코딩되어 다음과 같이 bvsd 필드의 encData 부분의 값으로 변합니다.

 

 

{"uuid":"2cdaa639-2385-4eb8-86c0-6976b63d8cba-0","encData":"N4IghiBcIEwMYBMxgGwGYCcBaGaAcArFgCwCmARnlninAAxYoYDsK56Cec5YWdIAGhDkoIAIwA6NBOKCQcKADMwAGwDOpIQigBtUAEtR+7UIiQdAXSEjIoM5etQ6AXyELoAC1IqVAeznant5+cqRKqhpCiuHqpK4GogAOAO5y9lbCUHa6GTYubqIBhUJhkAAuAE4Arpog0ZDKsc4Zpdm24FAEzBLMjpAoBD0FkMQSGK6Z7WZdQ5MDs+6j4xP1bWsdkBhbfVsYw7sTNm27O9vyUAeHWRvHZzYn55tbV1MXd297j5cTAObX9iA6AAfZhA4joIFoNCyITAtBAsTEIFYMRyYFIxFA-iwoEYeFiGAYZEwGBorEI+EkMkwBFiZFoMkoBHArAMnFMolUnF0pk4MnwvD0skEXFCnEYJnMEU4VE4zB0FDA0FiAiknHAjVkpEorFa5F07GAoGCnWG4EE-W67l0DFWo0ilmyo0qhF281Mg1kwWO6m2s1AxVoOms5iCxF4fmWp3A70Ir1AmnRhGsYm0pMGt2Q-WJjB6lHwkl5sT43M47ViYEwf3lumFsvIokwJPa5Wlo3a-Ew9upglF3F64EoAcBvV0rvo10ESN0qc4kU08dgyejsHToGz7t0tn2hOr+tblfbie14dHrOV4XktXdi9yq+XuF95uW68TnWvpc659f-l4Og05hKTPDVMyDQkWFdLACGA8lq3pBM83xRC43rHAENQgtkI-bUkWw+DFw7dDu1ZIi30TLDIxIs9pXhM9CLwtC6PghiaQYulv1Y4dv3Yp9eNQnj63NLi+zgoT+JQm8JInMTiIEo14Rk4EHSkpdFITREYDwZgiUVZF0AojCVKA0j11TFiVPLIyezzcjUM4ucoz4+SzMvH87zQ8yPxokzKXs4jMLsizmJsoKPNcvy4RchySI-XzQoiz8kOi2znLQ78AuIlK3wysiTPo1ycs-BLtQSuLSVyKBEWIYY0BtLQLjQXoQFKGACDoAg8E0qInAmAArSriAIIQPFEZhSAIMQ8FIMACAoDqBjERQUAQYgwEUAhFBgMhFDwRq8CdQxXmgABZXwAC99B8MAAHpBjoAACAAKAB1fQADsEF8ZI1HugA5AAVe6KwkOgAG57tet6UGIcGAA9oYASnugBBRJEhUUhnooABpfQyluxqpBQJ7sYACX+46ABkBHulR9AAa1Ie6AHFSDgenfCRgBhDwKl8ABbUhrowSQ6BkYhmBgCQCWIe6AGU1rACp9AJ7o0CHPpmreuR3C2+rIDwEpKu6kYhD+cwxBQP8BArNqMhGi2rboG26ArDJDqg4hnZAfrIDEIR6eNkAVCDs6g-50Qqje+m3q+7WhG16BIbQa9-GgKOY7jtIAR5vnBfugAFAARAAxQuVCqH53sgSAC98CoyjAcgMfuovfDgKpBbesp7pL+v+bAMoa7ANG6bgQf9F8N7rthrAfl8XwfgxrA4F5gXSCwRIEEUAA-LfoiEXP18L0v7oANX0UhklICoa+H0f9HHspJ+n-e9+3uRfongA3Jmubp0g3c76QBHujR+E8p4zywG9MAcAVA7wEKAseEDp6z0SDAuBO8QAZESKIOAbNWrqwwOQUg-56AoBgIoEWtVlB4AwKQ8gCAMDwDkAAR1EBQUgCB0CS3IK1DAa00CkAGH+RUK1mAIDQMgMACARBCAqKIZmC8l5MwAJJvTgBIJ66iyjeARjvZGv1maUwAKLaO7t4GmOjvCPQAEpIwAKqkyLizCoI8PCPx+hQh6Rd9AVDZmUNARcxBiHut-NQAB9AgESHqJEidE52rcgkhJwCgCQVZpZ0DFoBLaCM5BqBiJEEAZRCm1CqKUoQ38KkgFSA0CItRYa6Gdo0SILTSAZAAJ66BAMjZWqg5C9P0Koe6AAhFQsDA5CEGcMr+FQ+apEPqofQ5BlZyC5mAfmKyhlrI2VssA91jqDxGofAWj95ZgDej9Y6cs1lTzUL4cZBSTlVGVjfW5LzL4VD+lfOQrN65VwgEIUm3hf7P3HnIVR-NEiwJKUISmVQ4DGH2VzO5DywhwoRUi85lz7oOLeo-XwCB0UgGOo-Pm9zFA9wVjiuWN99AHxJXLFmvgygeIUEIa5hclGssfnITl1Kfq0uVgy-ldKGUF1UBPWO91KbvRZR0xIxLaXzyZgXZW3c5DKt8EzOWcBlaJFhSALVTMHGqM1aQFVuLVEyv0D8DwhrjVWvlqQfmyyHkmCNRa7VTq5YdM2Q8uQ-0wAeAFoCkA-19CCyeeGyNpAfq-SvvdWxob47hv8eQBFXge7XLkGfG+SAYFyEhj8BA70fgFIsM4ZwQA"}

<코드 1> form의 bvsd 필드

 

정말 딱 봐도 난해합니다(...) 이런 값은 여기에서 이렇게 처리되어 나옵니다

image.png.jpg

<그림 4> LZString.compressToEncodedURIComponent 함수를 이용한 압축과정

 

어쨌든, 이러한 동작 감시 시스템이 있는데 우리는 어떻게 네이버에게 프로그램을 사람처럼 속일 수 있을까요?

 

정말 많은 시도를 해봤는데, 답은 구글 크롬의 자동완성과 탭키를 활용해서 거의 "있을법한" 상황을 만들어서 로그인 과정에서 최대한 사람의 손(마우스와 키보드 사용)을 안 닿게하면 됩니다. 왜요? 구글 자동완성과 탭키 많이 쓰잖아요? 

 

한번 구현해 봅시다.

 

준비물: python3

http 요청을 위한 requests 모듈

정보 암호화를 위한 rsa 모듈

bvsd data 인코딩에 필요한 lzstring 모듈

bvsd에 사용될 uuid 모듈

bvsd 등 json 정보를 stringify 하게 만들어줄 json 모듈

마지막으로, 요즘 새로추가된 dynamic_key를 가져오기 위한 파싱에 필요한 beautifulsoup4 모듈

일단, 최근에 네이버가 로그인 방식을 한번 더 바꿨더군요.. 이 때문에 PC 웹상에서는 처음에 제공된 dynamic_key를 통해 rsa 공개키를 가져와야 해서 이 글에서는 모바일 페이지를 통해 key를 가져올겁니다. (모바일 페이지에서는 rsa 키와 dynamic_key사 한번에 제공되요.)

 

차근 차근히 소스코드를 보시면 어느정도 이해가 되실겁니다.

 

from rsa import encrypt, PublicKey
from requests import Session
from lzstring import LZString
from uuid import uuid4
from json import dumps
from bs4 import BeautifulSoup


class Naver(Session):
    def __init__(self) -> None:
        super().__init__()
        self.headers = {
            "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
        }
        self.base = "https://nid.naver.com/nidlogin.login"
        self.bvsd = {
            "uuid": None,
            "encData": None
        }
        self.default = {
            "localechange": "",
            "dynamicKey": None,
            "encpw": None,
            "svctype": "262144",
            "smart_LEVEL": "-1",
            "bvsd": None,
            "encnm": None,
            "locale": "en_US",
            "url": "https:///m.naver.com/aside/",
            "nvlong": "on",
            "appSchemeView": "true",
            "id": "",
            "pw": ""
        }

        self.state_footprint = {
            "a": None, # uuidWithCaptchaSequence
            "b": "1.3.4", # bvsdVersion
            "c": False, # deviceTouchable
            "d": [{ # keyboardLogs
                "a": ["0,d,TAB,9"], # keyStrokeLog
                "b": { # inputIntervalLog
                    "a": None, # valueTimelineList
                    "b": 0 # timelineListIndex
                },
                "c": "", # initialValue
                "d": None, # CompleteValue
                "e": False, # secureMode
                "f": False, # hideValueMode
                "i": "id" # inputFieldId
            }, {
                "a": ["0,d,TAB,"], # keyStrokeLog
                "b": { # inputIntervalLog
                    "a": ["0,"], # valueTimelineList
                    "b": 0 # timelineListIndex
                },
                "c": "", # initialValue
                "d": "", # CompleteValue
                "e": True, # secureMode
                "f": False, # hideValueMode
                "i": "pw" # inputFieldId
            }],
            "e": { # deviceOrientation
                "a": {  # firstOrientation
                    "a": 53.9, # Alpha
                    "b": 65.7, # Beta
                    "c": 4.8 # Gamma
                },
                "b": { # currentOrientation
                    "a": 53.9, # Alpha
                    "b": 65.7, # Beta
                    "c": 4.8 # Gamma
                }
            },
            "f": { # deviceMotion
                "a": { # first
                    "a": { # firstAcceleration
                        "a": 999, # X
                        "b": 999, # Y
                        "c": 999 # Z
                    },
                    "b": { # firstAccelerationIncludingGravity
                        "a": 999, # X
                        "b": 999, # Y
                        "c": 999 # Z
                    }
                },
                "b": { # current
                    "a": { # currentAcceleration
                        "a": 999, # X
                        "b": 999, # Y
                        "c": 999 # Z
                    },
                    "b": { # currentAccelerationIncludingGravity
                        "a": 999, # X
                        "b": 999, # Y
                        "c": 999 # Z
                    }
                }
            },
            "g": { # mouseMove
                "a": [], # mouseActiveLogs
                "b": 0, # timelineListIndex
                "c": 0, # pageXDifference
                "d": 0, # pageYDifference
                "e": -1, # totalInterval
                "f": 0 # errorCount
            },
            "h": "7e518ea5eb58651f6d4af5f24ef83781", # fingerprintHash
            "i": { # browserFingerprintComponents
                "a": self.headers["user-agent"],
                "b": "en",
                "c": 24,
                "d": 8,
                "e": 1,
                "f": 4,
                "g": [1680, 1050],
                "h": [1680, 1010],
                "i": -540,
                "j": 1,
                "k": 1,
                "l": 1,
                "z": 1,
                "m": "unknown",
                "n": "Win32",
                "o": "unknown",
                "aa": ["Chrome PDF Plugin::Portable Document Format::application/x-google-chrome-pdf~pdf", "Chrome PDF Viewer::::application/pdf~pdf", "Native Client::::application/x-nacl~,application/x-pnacl~"],
                "p": "bb84491f8f0ca552e32aa5b90b350297",
                "q": "ebed6372b259af3e658060d47d3aaadb",
                "r": "Google Inc. (Intel)~ANGLE (Intel, Intel(R) UHD Graphics 620 Direct3D11 vs_5_0 ps_5_0, D3D11-26.20.100.7324)",
                "s": False,
                "t": False,
                "u": False,
                "v": False,
                "w": False,
                "x": [0, False, False],
                "y": ["Arial", "Arial Black", "Arial Narrow", "Calibri", "Cambria", "Cambria Math", "Comic Sans MS", "Consolas", "Courier", "Courier New", "Georgia", "Helvetica", "Impact", "Lucida Console", "Lucida Sans Unicode", "Microsoft Sans Serif", "MS Gothic", "MS PGothic", "MS Sans Serif", "MS Serif", "Palatino Linotype", "Segoe Print", "Segoe Script", "Segoe UI", "Segoe UI Light", "Segoe UI Semibold", "Segoe UI Symbol", "Tahoma", "Times", "Times New Roman", "Trebuchet MS", "Verdana", "Wingdings"]
            },
            "j": 134 # fingerprintProcessingDuration
        }
    
    def form(self):
        return self.default
    
    def new_bvsd_data(self):
        return self.bvsd
    
    def new_bvsd_footprint(self):
        return self.state_footprint
    
    def encode_bvsd_data(bvsd_data):
        return LZString.compressToEncodedURIComponent(dumps(bvsd_data))

    def fill_bvsd(self, bvsd, id):
        bvsd_uuid = str(uuid4())+"-0" # 로그인 할때 사용되는 bvsd uuid 생성
        bvsd["uuid"] = bvsd_uuid

        bvsd_data = self.new_bvsd_footprint()
        bvsd_data["a"] = bvsd_uuid
        bvsd_data["d"][0]["b"]["a"] = ["0,{}".format(id)]
        bvsd_data["d"][0]["d"] = id

        bvsd["encData"] = Naver.encode_bvsd_data(bvsd_data)

        return bvsd
    
    def get_finalize(response_text: str):
        if response_text.find("location") > -1:
            return {
                "url": response_text.split('("')[1].split('")')[0],
                "result": True
            }
        else:
            return {
                "result": False
            }

    def login(self, NAVER_ID: str, NAVER_PW: str) -> bool:
        def download_keys():
            DOM = BeautifulSoup(self.get(self.base, params={
                "svctype": self.default["svctype"] # 모바일 페이지
            }, headers=self.headers).text, 'html.parser')

            Keys = DOM.find('input', {'id': "session_keys"}).attrs['value']

            session_key, key_name, e, N = Keys.split(",")

            return {
                "dynamic_key": DOM.find('input', {'id': 'dynamicKey'}).attrs['value'],
                "session_key": session_key,
                "public_key": PublicKey(int(e, 16), int(N, 16)),
                "key_name": key_name
            }
        
        def encrypt_with_public_key(ID: str, PW: str, Keys: dict) -> str:
            encode_login_info = ''.join([chr(len(s)) + s for s in [Keys['session_key'], ID, PW]]).encode()

            return encrypt(encode_login_info, Keys["public_key"]).hex() # 암호화하고 hex 값으로, e와 N값 순서는 네이버가 구라친겁니다.
        
        Keys = download_keys() # 서버의 공개키와 키 세션들을 가져온다.

        encrypted_info = encrypt_with_public_key(NAVER_ID, NAVER_PW, Keys) # 공개키를 이용하여 세션키와 함께 로그인 정보를 암호화한다.
        form = self.form() # 새로운 로그인 폼 생성
        
        bvsd = self.fill_bvsd(self.new_bvsd_data(), NAVER_ID) # 새로운 bvsd 폼 생성 후 내용 채우기s

        form["dynamicKey"] = Keys["dynamic_key"]
        form["encpw"] = encrypted_info
        form["encnm"] = Keys["key_name"]
        form["bvsd"] = dumps(bvsd)

        result = Naver.get_finalize(self.post(self.base, data=form, headers=self.headers).text)

        if result["result"]:
            self.get(result["url"], headers=self.headers)
            return True
        else:
            return False

if __name__ == "__main__":
    # TEST FIELD
    Browser = Naver()
    if Browser.login("", ""):
        print("성공!")
    else:
        print("실패!")

 

너무 졸려서 여기까지만 글을 쓰겠습니다..

모두 좋은 주말 보내세요.

이니스프리님 이니스프리 포함 1명이 추천

추천인 1

작성자
Hanam09 36 Lv. (25%) 105150/109520EXP

 

안녕!

 

댓글 1

이니스프리
profile image

추천은 일찍 눌렀는데 요새 정신없이 바쁘다보니 댓글이 늦었네요~
이 게시판에 제가 올린 허접한 팁보다는 100배 훌륭한 스크립트를 올려주셔서 진심으로 감사드립니다! ^-^

comment menu
2021.07.23. 21:48

신고

"이니스프리님의 댓글"

이 댓글을 신고 하시겠습니까?

권한이 없습니다.
번호 제목 글쓴이 날짜 조회 수
507 [Python] Requests에서 JSON 데이터를 POST 전송하는 방법 x2 이니스프리 이니스프리 21.08.22.08:16 39
506 [프린터/복합기] 캐논 복합기 MF6XX ID 및 PIN 1 이니스프리 이니스프리 21.08.16.09:19 39
[Python] Naver Login with requests 1 image Hanam09 Hanam09 21.07.17.01:48 114
504 [Python] 엑셀 파일에 암호 설정 (Set password for Excel file using Python) 3 image 이니스프리 이니스프리 21.07.09.21:21 85
503 [Python] 구버전 KeyCaptcha 정답 좌표 찾기 image 네모 네모 21.07.09.02:46 51
502 [Python] 패스워드 걸린 PDF 파일을 오픈하여 패스워드를 삭제한 채로 저장하기 이니스프리 이니스프리 21.07.08.18:26 103
501 [Python] 구글 뉴스 RSS 파싱 2 이니스프리 이니스프리 21.07.04.13:49 62
500 [Python] 이미지 파일의 Exif 정보 삭제하기 (+ 식빵자세 산냥이) image 이니스프리 이니스프리 21.07.03.13:42 57
499 [Python] PDF2image 모듈이 실행되지 않을 때 이니스프리 이니스프리 21.06.23.20:36 137
498 유료 VPN 선택과 관련하여 참고할 웹 문서! 2 이니스프리 이니스프리 21.06.08.19:28 119
497 [Python] 영어로 표기된 날짜를 숫자로 변환 이니스프리 이니스프리 21.05.30.11:39 171
496 [Python] Google Trend의 '최근 인기 검색어' 크롤링 3 이니스프리 이니스프리 21.05.02.12:24 121
495 [Python] for 문에 두 개의 리스트를 넣고 enumerate를 사용하는 방법 이니스프리 이니스프리 21.05.01.21:01 76
494 [Python] 입력받은 연도가 윤년이 아니면 그보다 가장 가까운 과거의 윤년을 출력하기 8 이니스프리 이니스프리 21.04.19.20:23 151
493 [Javascript] 이미지 업로드 전 가로x세로 사이즈를 확인하여 지정된 크기 이상인 경우 alert 띄우는 스크립트 이니스프리 이니스프리 21.04.17.21:28 36
492 [HTML] 특정 사이트의 파비콘을 다운로드 받는 방법 이니스프리 이니스프리 21.04.16.22:02 122
491 [Javascript] 값을 이용하여 배열의 요소를 삭제하는 방법 이니스프리 이니스프리 21.04.13.22:10 51
490 [Gnuboard] DB 테이블 중 g5_board_file에 대하여 이니스프리 이니스프리 21.04.11.16:25 112
489 [Windows] Windows 업데이트 원천 방지하기 5 image Seia Seia 21.04.10.07:10 79
488 [Docker] Docker 다시 알고 사용하기 Seia Seia 21.04.10.07:04 80