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

안녕하세요. 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 647
공지 [중요] 호스팅 만료와 관련하여 일부 수칙이 변경됩니다. 4 마스터 마스터 23.01.14.02:23 4325
542 [Python] 소수점 자리수의 출력 - round() vs format() 1 이니스프리 이니스프리 20.02.15.11:53 31760
541 [Python] 파일을 읽어서 각 행을 리스트로 만드는 5가지 방법 이니스프리 이니스프리 18.11.25.22:16 23291
540 안티앨리어싱 옵션: 알고 쓰자. 5 image 하루살이 title: 황금 서버 (30일)하루살이 17.02.25.23:57 19428
539 GIF 파일의 용량을 줄여주는 사이트 - gifgifs.com image 이니스프리 이니스프리 17.10.09.12:57 17666
538 [Python] tqdm과 enumerate를 함께 사용하기 이니스프리 이니스프리 20.05.29.19:07 16455
537 [OpenCV] !_src.empty() in function 'cvtColor' 에러 해결방법 2 이니스프리 이니스프리 19.11.15.20:40 12737
536 윈도우 작업 스케줄러에서 특정 작업이 실행 안 되는 경우 해결방법 (배치파일 등의 상대경로 관련) 2 image 이니스프리 이니스프리 18.10.02.19:38 12355
535 [파이썬] Selenium에서 클릭이 안 될 때의 해결방법 이니스프리 이니스프리 19.10.27.22:20 10370
534 [Python] Requests에서 JSON 데이터를 POST 전송하는 방법 x2 이니스프리 이니스프리 21.08.22.08:16 9563
533 [Selenium] select box, check box, radio button 조작하기 이니스프리 이니스프리 20.06.27.15:47 8441
532 유튜브 iframe api의 autoplay 옵션과 관련하여 1 image 이니스프리 이니스프리 19.01.05.14:45 7530
531 윈도우 MAK 라이센스의 인증 가능횟수 확인방법 4 이니스프리 이니스프리 18.11.21.23:53 7511
530 일본 아마존에서 MP3 음원 구입하는 방법 5 image 제르엘 제르엘 19.12.23.01:28 7365
529 DB 설계하실 때 사용하면 좋을거 같은 사이트 공유합니다. 1 image JAVA JAVA 17.11.21.15:06 6990
528 국전 '빨간집' 위치 및 영업시간 등 정보 3 이니스프리 이니스프리 18.11.30.16:17 6740
527 제조사별 서버 원격관리 툴의 명칭 정리 (iLO, iDRAC, IPMI 등) 이니스프리 이니스프리 18.11.22.22:10 6459
526 해외직구시 카드 중복결제에 대하여 (아마존 등) 9 이니스프리 이니스프리 18.12.30.15:52 6107
525 텔레그램 비밀대화의 스크린캡쳐 방지기능 이니스프리 이니스프리 20.01.20.23:44 5871
524 크롬 원격 데스크톱 조작법 2 이니스프리 이니스프리 20.06.03.23:03 5822
523 윈도우의 CMD 창에서 Ctrl+V로 붙여넣기 (윈도우 10 / 윈도우 8 이하) 3 image 이니스프리 이니스프리 18.11.08.00:15 5756