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

안녕하세요. 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 739
공지 [중요] 호스팅 만료와 관련하여 일부 수칙이 변경됩니다. 4 마스터 마스터 23.01.14.02:23 4409
506 파이썬에서 리스트의 문자열들이 특정 문자열 안에 포함되어 있는지 확인하는 방법 1 이니스프리 이니스프리 19.11.10.02:34 2879
[Python] Naver Login with requests 1 image Hanam09 Hanam09 21.07.17.01:48 2860
504 [Python] 원하는 URL로 크롬 창 열기 이니스프리 이니스프리 20.07.16.14:01 2828
503 [Python] ImportError: No module named '_curses' 에러에 대한 대처 방법 이니스프리 이니스프리 21.09.19.21:44 2790
502 [Python] PDF2Image 모듈 - PDF 파일을 이미지 파일로 변환 이니스프리 이니스프리 20.05.30.21:01 2769
501 [Python] 확장자 확인 및 변경 이니스프리 이니스프리 20.08.23.12:17 2768
500 [Selenium] 다운로드받은 파일명 변경 이니스프리 이니스프리 20.01.03.12:47 2711
499 [파이썬] beep음 재생하기 (윈도우/맥/리눅스) 이니스프리 이니스프리 20.06.27.18:43 2682
498 [Python] PDF2image 모듈이 실행되지 않을 때 이니스프리 이니스프리 21.06.23.20:36 2621
497 Selenium에서 파일 업로드할 때 파일을 선택하는 방법 이니스프리 이니스프리 19.07.02.23:45 2614
496 Autohotkey 내장변수 정리 - 62개 이니스프리 이니스프리 18.10.04.16:48 2492
495 [Python] 특정시간까지 대기하기 1 이니스프리 이니스프리 20.03.06.22:28 2486
494 [Python] 스크린 캡쳐하는 모듈 이니스프리 이니스프리 20.05.25.21:30 2413
493 [Python] 매시간 또는 매분 정각에 작동하는 스크립트 - time.sleep을 사용하지 않는 방법 3 이니스프리 이니스프리 20.05.26.18:09 2380
492 [파이썬] Selenium 쿠키를 Requests에서 불러오는 방법 이니스프리 이니스프리 19.11.10.15:13 2359
491 크롬에서 방향키가 작동하지 않는 경우 해결책 1 이니스프리 이니스프리 19.03.31.11:56 2352
490 [우분투/파이썬] pip로 pytesseract를 설치 후 not installed or not in your path 메세지에 대한 대처방법 이니스프리 이니스프리 18.11.12.20:05 2172
489 각 카드사별 해외이용 정지방법(2018.04.06) 4 image 자뻑보이 자뻑보이 18.05.28.19:04 2171
488 변압기 소음을 줄이는 방법 6 이니스프리 이니스프리 19.01.27.12:27 2158
487 [파이썬] 리스트에서 중복되는 요소가 있는지 여부 판단 / 중복되는 요소만 뽑아서 리스트에 넣기 이니스프리 이니스프리 19.12.08.15:17 2152