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

안녕하세요. 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("실패!")

 

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

모두 좋은 주말 보내세요.

gamb1t님 gamb1t 포함 2명이 추천

추천인 2

작성자
Hanam09 36 Lv. (50%) 106610/109520EXP

 

안녕!

 

댓글 1

이니스프리
profile image

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

comment menu
2021.07.23. 21:48

신고

"이니스프리님의 댓글"

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

권한이 없습니다.
번호 제목 글쓴이 날짜 조회 수
공지 [작업 완료] 설 명절 맞이 서버 업데이트 안내 3 마스터 마스터 24.02.11.17:21 819
공지 [중요] 호스팅 만료와 관련하여 일부 수칙이 변경됩니다. 4 마스터 마스터 23.01.14.02:23 4485
547 색상선택 결정문제를 겪고 계신 분들 위한 사이트 OAUTH2 20.06.19.18:22 69
546 database 백업 및 복원을 지원하는 site입니다. 해피보이 20.06.03.20:52 77
545 [Python] Tabulate 모듈로 작성한 테이블을 파일로 저장할 때 에러가 발생하는 경우 이니스프리 이니스프리 24.04.20.23:45 79
544 [Python] 쿠팡 크롤링할 때 소소한 팁 이니스프리 이니스프리 1일 전21:50 80
543 [펌] Chrome will soon be less of a memory hog in Windows 10 3 이니스프리 이니스프리 20.06.24.00:12 82
542 [Python] Ubuntu에서 파이썬이 설치된 디렉토리를 간단히 확인하는 방법 2 이니스프리 이니스프리 20.08.03.22:58 88
541 [Python] 독일어, 스페인어, 체코어 등 철자를 영문 알파벳으로 전환하기 이니스프리 이니스프리 24.04.21.17:15 93
540 [Javascript] 값을 이용하여 배열의 요소를 삭제하는 방법 이니스프리 이니스프리 21.04.13.22:10 97
539 [Python] Table을 쉽게 만들어주는 모듈x2 이니스프리 이니스프리 24.04.18.22:04 97
538 순수 React만으로 다이내믹 태그 네이밍하기 Seia Seia 20.01.20.19:18 104
537 React 그리고 Redux 쉽게 이해하기 image Seia Seia 20.09.12.06:31 104
536 [Python] 문자열에서 파일명 또는 폴더명으로 시스템상 지원되는 글자를 제외하고 삭제하기 이니스프리 이니스프리 20.11.01.14:37 104
535 나리야 익명보드 및 내 글 알림 관련 Q&A image 이니스프리 이니스프리 21.02.14.17:30 107
534 [Python] 변수의 이름을 반환하는 함수 이니스프리 이니스프리 24.04.20.23:29 107
533 [Python] 크롤링한 bytes를 string으로 변환 이니스프리 이니스프리 21.02.14.15:32 109
532 중국 배대지 휴무 이니스프리 이니스프리 20.01.14.13:01 112
531 [Python] 3개의 list를 하나의 dictionary로 변환하기 이니스프리 이니스프리 20.06.10.21:47 114
530 IPVanish의 한국서버가 4개로 확충되었습니다. image 이니스프리 이니스프리 20.01.11.11:27 116
529 헷갈리는 출생지를 가진 인물들 1 이니스프리 이니스프리 20.06.26.21:09 116
528 MS Office 제품군 명칭 변경 image 이니스프리 이니스프리 20.07.02.18:05 116