LineageM bot(macro) reverse engineering

 


<Fig1. LineageM>

 


하루 60억원 매출 리니지M

 

엔씨소프트의 핵심 캐시 카우 역할을 하던 PC버전 리니지가 모바일버전으로 개발되었다.
2017년 6월 리니지M 출시하여 100일 이지나 매출 6000억 돌파 라는 엄청난 기록을 새운다.

계정 도용 등의 해킹 사건이 일어났었지만 서버 해킹이 아닌 서드파티를 통한 해킹으로 알려져 있다.
2차인증 기기등록으로 정해진 기기에서만 플레이할 수 있게 조치하였다.
따라서 연동된 계정을 해킹한다 하여도 기기등록을 해제해야 접속 할 수 있다.

  

<Fig2. 기기등록>


모바일 게임 답게 자동사냥(이하 자사)기능을 게임내 자체적으로 기능을 구현하여 알아서 몬스터 처치, 버프, 물약마시기 등등을 한다. 

그래서 사냥터까지 플레이어가 이동시키고 자사 켜두고 딴 일 보는 게임이 되어 버렸다. (다마고찌)

 

<Fig3.게임화면>

 

일반적인 한국형 모바일게임은 에뮬레이터를 통해 플레이하는 것을 막으려고 하는데 리니지M은 열어놨다.
 
왜 안했는지 이제 알 것 같다. 이걸 만약 핸드폰으로 했으면 채굴하러 끌려간 그래픽카드 꼴이다..

리니지m 안한 중고폰 사요!

 

 

어쨌든 PC 리니지도 레벨업이 엄청나게 힘들었지만 리니지M도 마찬가지다.
아인사하드 없으면 레벨업 못한다.  매일 똑같은 하이네잡밭에 캐릭터를 갔다 놓으면 뭔가 의무적으며........채굴하는 느낌이다.
 
어느정도 하다 보면 사냥터까지 보내는 것도 귀찮아지며, 물약 사는 것도 엄청나게 귀찮아진다.

 

<Fig4. 매크로>

 

 

게임 봇인 유니크 매크로라는 프로그램이다. 리니지M 매크로 중 엄청나게 많은 기능들을 지원하며 업데이트를 매 패치 마다 해준다.

자동육성 기능이 있어 좋은 변신이 나올때까지 변신뽑기까지만 하고 버리고 하는 작업장이 생겨 결국 NC에선 50레벨 미만은 뽑기를 막아놨다.

로그기반 봇탐지를 회피하기 위해 각 액션 마다 랜덤성을 부여하는 기능도 제공한다고 한다...ㅎㄷㄷ

 

 

<Fig5. 작업장>

 

에뮬레이터를 막지 않아 다중 계정으로 일반 PC에서 최소 4개이상은 돌아가니 집에서도 작업장을 운영할 수 있다 !

 

 

<Fig6. 중국인 작업장>

 

매크로 프로그램이다 LineageM.exe로 프로그램을 실행하여 동작한다.

라이브러리를 자세히 보면 OpenCV가 있다.

 

 

<Fig7. 매크로>


OpenCV(Open Source Computer Vision)은 오픈 소스 컴퓨터 비전 라이브러리이며 인텔이 개발하다 손 뗀 상태다.
패턴인식, 기계학습등에 많이 쓰인다.  게임 봇에 적용하려고 만든게 아님

사물인식, 물체 인식등 이미지 처리 대부분 opencv 써서 개발한다.


 

 <Fig8. OpenCV>

 

opencv로 매크로를 만드는 정성이면 다른걸 만드셔도 잘 만드실 같다.

무료 테스트 기간이 끝나 월 단위로 판다. 1개 12000원 부터 시작해서 100개는 70만원 정도에 팔고 있다.

블로그는 갓카오님께서 폐쇄조치 하여 접속 할 수 없고, 추적 안 당하시려고 텔레그램을 통해 연락해서 계정 등록을 해야 한다.

 

<Fig9. 판매중인 매크로>

 

다 C#으로 개발되어 있어서 분석하기 편하다.

C#은 dnSpy를 따라올 도구가 없다.

https://github.com/0xd4d/dnSpy

 

<Fig10. dnSpy>

 

C#이라 얼마 안 걸릴 줄 알았는데 봇 개발자는 분석 당하지 않으려고 난독화를 적용하셨다....

난독화 적용되어 있을 때는 dnSpy로는 수정할 수 없다....

수정하려면 별 수 없이 assem으로 짜야한다.

결국 +ida로 분석했다

 

<Fig11. 수정불가>

 

보통 매크로 같은 경우 해당 프로세서가 마우스 이벤트를 사용하여 control 한다. 매크로를 돌린 상태에서 컴퓨터를 할 수 없다는 이야기이다. 반면 유니크 매크로는 에뮬레이터에 dll inecjtion 하여 메모리상에서 control 한다.

따라서 프로세스 갯 수 만큼 개별적으로 컨트롤 할 수 있는 넘나 좋은 특징이 있다.

 

<Fig12. 구우조>

 

 

injection 된 dll은 Ucapture1.dll이다.

 

<Fig13. Process explorer>

 

후킹 모듈에 덕지덕지 보안 기능 붙여 놓으면 엄청난 cpu 점유율을 볼 수 있다.

예민한 후킹 모듈이다 보니 packing 등은 적용하지 않았다.  

 

<Fig14. UCapture1.dll>

 

분석해보면 실시간 처리(?)로 초당 스크린샷 찍어 LineageM로 던져 opencv로 상태 확인한다.

 

<Fig15. 매크로 동작>

 

매크로 시작을 클릭하면 매크로 서버에서 현재 아이디가 기간만료인지 남은 기간이 있는지 확인한다.

기간만료 상태면 다음 팝업 창이 뜨며 진행이 되지 않는다.

 

<Fig16. 입금하세요>

 


매크로 서버와 매크로 프로그램은 평문 데이터 통신한다. 아이디와 패스워드가 노출되는 것을 확인할 수 있다.

재미있는 점은 매 실행 시 나머지는 고정이고 붉은 박스는 계속 변경된다.  킁킁 seed 냄새가 나쥬??

 

<Fig17. Network>

 

 

저 붉은 박스는 분석해보니 GetCurrentProcess 함수를 통해 Process ID를 받아오기 때문에 매번 실행 시 변경되는 것이었다.

 

<Fig18. Password>

 

IP주소와 MAC주소 하드디스크 시리얼정보와 매크로 프로세스 ID를 구한뒤 format string으로 IP | MAC | HDDSerial | PROCESSID 형태로 만들고 생성된 정보는 이후 매크로 명령시 암호화 key로 사용된다.

 

 

<Fig19. AES Decrypt 256>

 

 

서버에서 받아오는 데이터 중 <Request_AesResult> .. </Request_AesResult> 필드는 서버에서 인증된 사용자인지 확인 후 전송하는 action이다.

이 내용은 암호화 되어 있으며, AESDecrypt256 함수를 통해 복호화를 수행한다.


AESDecrypt256 함수의 첫번째 파라메터인 InputText가 <Request_AesResult> .. </Request_AesResult> 필드 내용이고 두번째 파라메터인 pw은 클라이언트에서 생성한 "IP | MAC | HDDSerial | PROCESSID" 이다.

이 pw을 통해 pbkdf1로 aes 256 의 key와 salt를 생성한다.

salt 값은 pw의 길이로 생성하는데 보통 pw가 10자리는 넘어 2byte정도 된다.

뭐 어차피 sha1(password+salt) 연산이기 때문에 2byte여도 큰 문제는 없다.


표준 rfc2898를 읽어보면 dkLen > 20,  Salt는 8byte로 명시되어 있다. 

일반적인 경우 데이터 길이가 더 적은 경우 padding을 붙이거나 에러를 출력한다.


C#의 참 독특한점은 PasswordDeriveBytes.GetBytes 로 pseudo 난수 바이트를 생성해준다.

pbkdf1의 return 값은 20byte인데 이 함수를 통해 32byte와 16byte 총 48byte를 생성해 줄 수 있다.


20bye까지는 pbkdf1 연산 결과이고 이후 pseudo 난수인데 어떻게 생성되는지 알아야 python 으로 porting할텐데 이걸 내가 구지 지금 알아야 할까 생각이 들고.. (의도한건가??) 결국 매크로를 수정해서 pw인 connect_info를 항상 고정으로 만들어 버렸다. ^^;;

 

 

<Fig20. 늘 18>

 

 

다시 정리하자면 매크로 서버에서 action은 클라이언트에서 받은 키로 암호화 하여 클라이언트한테 전송한다.


프로그램 실행 -> ( IP | MAC | HDDSerial | PROCESSID ) 정보 전송 -> 매크로 사용 요청 ->


-----------------server-side-------------------

message= action

pw = ( IP | MAC | HDDSerial | PROCESSID )

salt = len(pw)

key,iv =pbkdf1(pw,salt)

ecn= aes.enc(message, key, iv)

-----------------------------------------------


enc값이 Request_AesResult이다. 클라이언트한테 전송한다.

 

------------------client-side-----------------

message = Request_AesResult 내용

pw = ( IP | MAC | HDDSerial | PROCESSID )

salt = len(pw)

key,iv =pbkdf1(pw,salt)

action = aes.dec(message, key, iv)

-----------------------------------------------


서버에서 받은 action으로 매크로 기능을 수행한다.

action 값으로 분기를 수행하는 매커니즘이기 때문에 action 값이 중요한 열쇠가 된다.



<Fig21. action 처리 루틴>


우리는 action값을 모른다. 알아내려면 분석량 * action 으로 증가 될 뿐 ㅠ

이걸 crack 하기 위해서 매크로 프로그램 변조를 통해 매 key가 고정으로 생성되게 만든뒤 action값을 적절한 게-씡(유추)하여 암호문을 생성해 요청 시 전달하면 아마....되겠지?


# -*- coding: UTF-8 -*-
from Crypto.Cipher import AES
from Crypto.Hash import SHA as SHA1, HMAC
import base64
import struct
import binascii
import StringIO

def pkcs7encode(text):
        l = len(text)
        output = StringIO.StringIO()
        val = 16 - (l % 16)
        for _ in xrange(val):
            output.write('%02x' % val)
        return text + binascii.unhexlify(output.getvalue())

def pkcs7decode(text):
        nl = len(text)
        val = int(binascii.hexlify(text[-1]), 16)
        if val > 16:
            raise ValueError('Input is not padded or padding is corrupt')
        l = nl - val
        return text[:l]

def encrypt(message, key , iv):
    aes = AES.new(key, AES.MODE_CBC, iv)
    return base64.b64encode(aes.encrypt(pkcs7encode(message.encode("utf-16")[2:])))

def decrypt(encrypted, key , iv):
    aes = AES.new(key, AES.MODE_CBC, iv)
    return pkcs7decode(aes.decrypt(base64.b64decode(encrypted))).decode("utf-16")

def PBKDF1(password, salt, dkLen, count=100):
    pHash = SHA1.new(password+salt)
    digest = pHash.digest_size 
    for i in xrange(count-1):
        pHash = pHash.new(pHash.digest())
    return pHash.digest()


#pw 18|18|18|18
key  = "\xAB\x80\xEE\xCD\x8A\xDF\x30\xAC\x64\x45\x17\x73\xE3\xB0\x55\xD6"
key += "\xC1\x06\x9D\xA8\x35\x60\x3C\x3D\xE3\x34\x52\xA5\x15\x51\x7B\xF3"
iv   = "\x64\x45\x17\x73\xE3\xB0\x55\xD6\x5B\x4E\x87\xD6\x68\xCA\xFB\x10"

text = "게싱실화냐"
enc =  encrypt(text, key , iv)
print enc
dec =  decrypt(enc, key , iv)
print dec

 


 

 

 

다행히 훌륭한 게-씡(유추)으로 성공했다.


 

<Fig22. 매크로>

 

잘돌아간다.

 


1줄 요약. 게임 매크로도 키 생성까지하면서 사용자 인증한다.



<Fig23. 열일하는 NC....봇탐지 회피는 개뿔.....잘가요>

저작자 표시
신고
Posted by kkoha 트랙백 0 : 댓글 9


RX480 써멀 재도포 이후 팬을 컨트롤하는 놈이 궁금해졌다. 


어떠한 원리로 컨트롤 하는지 알고 싶기 떄문에 RX480을 분석하기로 한다.




<Fig1. Fan Pinout>


RX480의 쿨러는 4PIN을 사용하고 있다. 그림 1과 같이


검은색 = GND

노란색 = +12V

녹색 = Senser

파란색 = Control


으로 구성되어 있다.



<Fig2. signal>


로직 애널라이저로 Sensor pin을 확인하면 Pulse-width modulation (PWM)로 구성되어 있는 것을 확인할 수 있다.


참고 자료 : https://en.wikipedia.org/wiki/Pulse-width_modulation


보드에서 FAN의 속도를 조절하기위해 FAN 전압을 GPU에 sending 한뒤 GPU쿨러의 센서가 현재의 RPM을 response하여 온도관리 한다.

따라서 쿨러의 현재 RPM을 전송하지 못하여도 메인보드에선 전압을 통해 FAN을 제어 하여 지속적으로 전압을 전송하기 때문에 온도 조절이 되지 않아 GPU 가 타는 불상사가 일어날 수 있다.

그렇기 때문에, GPU가 특정 온도 이상 올라가면 자동으로 GPU의 전원을 차단하는데 이는 하드웨어적으로 구현하기에는 상당히 무리가 있어 분명 어딘가에 저장할 것이라고 판단하였다 . 제일 의심 가는 부분은 바로 BIOS다.



<Fig3. BIOS Version>



TechPowerUP 도구를 사용하여 RX480의 바이오스를 추출할 수 있다. 물론 플래싱 까지 가능하다.



<Fig4. BIOS Extract>


해당 바이오스 펌웨어는 당연히 Binwalk에서도 아무것도 할 수 없다.


삽질을 통해 분석한 결과...



<Fig5. BIOS Analysis>


해당 구조로 구성되어 있는 것을 확인할 수 있다.

조금 설명하자면 Little Endian으로 기록되어 있으며, 처음에는 AMD Magic signature로 추정되는 0x55AA가 기록되어 있으며 (linux kernel AMD gpu driver 참고함) 현재의 총 블럭 개수가 기록되어 있다. 이후 식별된 데이터는 Block의 크기가 기록되어 있어 해당 영역은 0x73 * 0x200 = 0xE600 만큼 크기인것을 확인할 수 있다. 이후 확인한 CheckSum이였으며,  계산 원리는 간단하다. 0번지 offset부터 사이즈 만큼 모든 byte를 더한 값이 CheckSum 데이터이다.


또한 온도 기록방식도 특이하였다. 최대 온도를 예를 들어 0x2134 이며 계산 식은 0x2134  / 0x64 = 0x55 (85) 인것을 확인할 수 있다.


RX480의 바이오스는 무결성 확인이 없기 때문에 아무나 수정해서 올릴 수 있다. 따라서 최대 온도와 RPM 온도 등의 설정정보를 범위 이상으로 놓으면 (?).....


온도 관련된 정보들은 어느정도 찾았으니 나머지는 여러분이 분석해보길 바란다.




<Fig6. cool!>


바이오스 수정해서 올리지 마세요. 벽돌 될 수 있습니다. 전 못고치며 모든 책임은 당신에게 있습니다.


import struct

rom_checksum = 0x21
venderid = 0x238
subid = 0x23A
deviceid = 0x24A
subvendorid = 0x238
min_temp = 0x9ED8
med_temp = 0x9EDA
high_temp = 0x9EDC
max_temp = 0x9EE4
max_RPM = 0x9EEB 
max_gpufreq = 0x9C59
max_memory_freq = 0x9C5D
power_control_limit = 0x9C61
shutdown_temp = 0x9F1D 
max_temp_power = 0x9F17

print "################################"
print "# RX480 Bios Analysis v0.01    #"
print "#                              #"
print "#             hacked by koha   #" 
print "################################\n"

f=open("bios.rom","rb")
databuffer = f.read()
f.close()

checksum = struct.unpack("B",databuffer[rom_checksum])[0]
size = struct.unpack("B",databuffer[0x02])[0] * 0x200

check=0
for i in range(size):
    check += struct.unpack("B",databuffer[i])[0]
check = checksum - (check % 0x100)
if checksum == check:
    print "Checksum ok\n"

print "vendor ID : " + databuffer[venderid:venderid+2].encode('hex')
print "Sub ID : " + databuffer[subid:subid+2].encode('hex')
print "device ID : " + databuffer[deviceid:deviceid+2].encode('hex')
print "subvendor ID : " + databuffer[subvendorid:subvendorid+2].encode('hex')+"\n"




저작자 표시
신고
Posted by kkoha 트랙백 0 : 댓글 0


14나노 핀펫 공정으로 만든 RX480은 래퍼런스 제품이 30만원 밖에(?) 안하는 가격으로 가성비 괜찮은 제품 중 하나인데.... 발열량이 상당하다. 따라서 14,000원만 투자하여 써멀 구리스 재도포를 통해 온도를 낮춰보려고 한다.



<Fig1. RX480>


그래픽 카드 기준으로 옆면 각각 3개의 나사를 제거하면 블로워팬 뚜껑을 딸 수 있다.


<Fig2. rx480 heat sink>


뚜껑 따면 은색의 싸구려 히트싱크를 볼 수 있다.


히트싱크를 분리하려면 보드 뒤편에 있는 브라켓을 분리해야 하는데 이 브라켓을 고정하는 피스 중 하나에 AMD 워런티 씰이 존재한다. 얘 없어지면 3년 무상 a/s 사라진다...


워런티 씰 우회는 드라이기 신공 등 여러 신공이 있지만


<Fig3. warranty seal stickers>


브라켓에서 4개 중 씰 없는 나사 3개 분리 이후 그림4 와 같이 빙빙 돌리면 워런티 씰 보존 상태에서 브라켓 분리가 가능하다. (취.약.점) 당연히 역순으로 조립할 수 있다.


<Fig4. warranty seal stickers >


히트팬 분리하면 기존에 발라져 있던 써멀의 흔적을 볼 수 있다. 말끔히 제거하자.


<Fig5. rrreeemmooovvee>


ARCTIC MX-4 thermal compond (Thermal Conductivity: 8.5 W/mK) 출동!!


<Fig6. MX-4>


GPU 중간에 1자로 도포 한뒤 역 조립한다.



<Fig7. 111111>


동일 환경에서  3d mark fire strike 2번 테스트 한 결과다. fire strike(이하 파스) 보는거 정말 지겹다.


일반 상태에서 재부팅 이후 파스 테스트 시 최소 44도 ~ 최대 90도


<Fig8. Normal>


서멀 교체 이후 파스 테스트 시 최소 39도 ~ 최대 84도


<Fig9. Replace thermal compound>


온도는 내려갔으나 fan speed가 요동을 친다... 음..??


사실상 DIY 종결판 RX480...;;;

저작자 표시
신고
Posted by kkoha 트랙백 0 : 댓글 0