2011. 5. 29. 22:06

흠... 2007년경 아주 오래 전에 진행했던 작업을 하나 올려보고자 한다.. 나름 재미있었던 작업이었다..
(참고로 아래의 내용은 2007년경 작성되어진 내용으로, 현재 운영중인 해당 사이트의 바이너리와는 차이가 있음을 밝혀두며,
 만약 문제가 될 소지가 있거나, 삭제 요청이 들어올 경우 즉시 삭제 처리 할 의향이 있음을 밝혀 둡니다.)

QXile은 일종의 P2P 프로그램으로 유료 결제 또는 공짜 포인트 얻기를 통하여 일정량의 포인트를 획득하고,
해당포인트를 사용하여 여러 자료를 다운받을 수 있는 프로그램이다.

다른 P2P 사이트와는 달리 해당 사이트에서 재밌었던 것은 공짜 포인트를 얻을수 있는 2가지 미니게임을 제공한다는
점이었다. 미니 게임은.. 타자게임과 로또게임이었는데..

타자게임은 이용자가 채팅방에 들어가서, 일정 시간마다 랜덤하게 뿌려지는 문구를 모든 사용자가 빠르게 타이핑하여
순위를 메겨 순위별로 일정 포인트를 지급하여주는 방식이었고(아래 그램),

로또게임은 채팅방에 있는 사람들이 1부터 20까지의 숫자를 입력하여, 일정시간마다 추첨이 행해지고 선택한 20개 숫
자중 5개가 일치할시에 150만 포인트, 2등(4개일치) 3만 포인트, 3등(3개 일치) 300포인트를 지급하는 게임이었다.(아래그림)

위의 두 게임을 짬짬히 하며 재미를 더해갈 즈음.. 자동으로 플레이를 해주는 프로그램을 만들어 볼까라는 생각에서 시작해보았다.

먼저 타자게임을 어떻게 구현해볼까 생각하다.. 첫번째 든생각이 랜덤 문장이 나오는 캡션창의 문구를 후킹해 구현해볼까를
생각해보고 덤벼 봤는데 녹녹치가 않았다. 내공이 후달려서 그런건지..

어쨌든 방법을 바꾸어 캡션바 객체의 캡션명 후킹이 아닌 직접적으로 해당 랜덤문자열이 생성되어지는 주소 공간을 찾기로 하고
몇일 삽질하다가 결국은 랜덤 문자열이 생성되어 저장되어지는 주소 공간을 찾아 낼 수 있었다..(ollydbg 이용)

0041893A   .  8BC8          mov     ecx, eax
0041893C   .  FF52 0C       call    near dword ptr ds:[edx+C]
0041893F   .  83C0 10       add     eax, 10
00418942   .  894424 18     mov     dword ptr ss:[esp+18], eax  // 이부분
00418946   .  8B4424 10     mov     eax, dword ptr ss:[esp+10]
0041894A   .  8B0F          mov     ecx, dword ptr ds:[edi]
0041894C   .  50            push    eax
0041894D   .  51            push    ecx
0041894E   .  8D5424 20     lea     edx, dword ptr ss:[esp+20]
00418952   .  68 64544500   push    QXile.00455464                ;  ASCII "제한속도: %s(타/분)문구: /%s"
00418957   .  52            push    edx

위의 부분에서 OFFSET 418942 부분을 보면 랜덤하게 생성되어진 문자열의 주소값을 불러오는 부분을 찾아 내었지만 다음
문제에 봉착하게 되었다. 바로 문자열이 상대주소 OFFSET에 저장되어진다는 점이었다 ㅡㅡ; 따라서 문자열이 저장되어
지는 공간을 절대주소로 지정하기 위해서 위의 부분을 아래와 같이 인라인 패칭 하였다.

먼저 프로그램상의 코드 영역중 빈공간을 찾아서(OFFSET 4500E3에서 찾아 낼 수 있었다.) 해당영역으로 강제 분기 시킨후,

0041893A   .  8BC8          mov     ecx, eax
0041893C   .  FF52 0C       call    near dword ptr ds:[edx+C]
0041893F   .  83C0 10       add     eax, 10
00418942      E9 9C770300   jmp     QXile.004500E3
00418947      90            nop
00418948      90            nop
00418949      90            nop
0041894A   >  8B0F          mov     ecx, dword ptr ds:[edi]
0041894C   .  50            push    eax
0041894D   .  51            push    ecx
0041894E   .  8D5424 20     lea     edx, dword ptr ss:[esp+20]
00418952   .  68 64544500   push    QXile.00455464                                  ;  ASCII "제한속도: %s(타/분)문구: /%s"
00418957   .  52            push    edx


아래와 같이 생성된 문자열의 주소 값을 가지고 있는 eax레지스터의 주소값에 있는 문자열을 내가 원하는
빈공간에 복사하는 구문을 아래와 같이 구현하였다.

004500DE      00            db      00
004500DF      00            db      00
004500E0      00            db      00
004500E1      00            db      00
004500E2      00            db      00
004500E3   .  894424 18     mov     dword ptr ss:[esp+18], eax
004500E7   .  8B4424 10     mov     eax, dword ptr ss:[esp+10]
004500EB   .  A3 2A014500   mov     dword ptr ds:[45012A], eax
004500F0   .  60            pushad
004500F1   .  B9 00000000   mov     ecx, 0
004500F6   >  8A1408        mov     dl, byte ptr ds:[eax+ecx]
004500F9   .  80FA 00       cmp     dl, 0
004500FC   .  74 03         je      short QXileCha.00450101
004500FE   .  41            inc     ecx
004500FF   .^ EB F5         jmp     short QXileCha.004500F6
00450101   >  890D 36014500 mov     dword ptr ds:[450136], ecx
00450107   .  A1 2A014500   mov     eax, dword ptr ds:[45012A]
0045010C   .  8D30          lea     esi, dword ptr ds:[eax]
0045010E   .  8D3D 3D014500 lea     edi, dword ptr ds:[45013D]
00450114   .  8B0D 36014500 mov     ecx, dword ptr ds:[450136]
0045011A   .  F3:A4         rep     movs byte ptr es:[edi], byte ptr ds:[esi]
0045011C   .  61            popad
0045011D   .^ E9 2888FCFF   jmp     QXileCha.0041894A
00450122      00            db      00
00450123      00            db      00
00450124      00            db      00
00450125      00            db      00
00450126      00            db      00
00450127      00            db      00
00450128      00            db      00
00450129      00            db      00
0045012A   .  00000000      dd      00000000                                           ;  Original String Address
0045012E      00            db      00
0045012F      00            db      00
00450130      00            db      00
00450131      00            db      00
00450132      00            db      00
00450133      00            db      00
00450134      00            db      00
00450135      00            db      00
00450136   .  00000000      dd      00000000                                           ;  String length
0045013A      00            db      00
0045013B      00            db      00
0045013C      00            db      00
0045013D      00            db      00                                                 ;  Real String Address

먼저 랜덤문자열의 주소값을 가지고 있는 eax레지스터를 내가 지정한 빈공간 OFFSET 45012A에 저장시킨후,
스택 꼬임 방지를 위해 스택을 백업한 후(pushad),
생성되어진 랜덤 문자열의 끝문자 '0'를 검색하기 위해 루프를 돌려 문자열의 길이를 파악한다.
카운팅 되어진 문자열 길이는 ecx레지스터에 저장하고, 이 길이 만큼을 원문자열 주소시작부 부터 불러내다가,
정해놓은 공간인 OFFSET 45013D에 복사하도록 한다. 복사가 끝나면 아까 백업한 레지스터를 복원시키고(popad)
위의 강제 분기시켰던 이후로 흐름을 돌린다.

이렇게 하여 계속해서 변화하는 주소값에 저장되어지던 문자열을 프로그램의 빈공간인 고정 OFFSET 45013D에
계속해서 복사 할 수 있게 되었다.

이렇게 복사 하여놓은 절대주소 값은 항상 고정되어 있으므로 향후에 만들어질 오토프로그램에서 아래와 같이
Private Const Add_TypeStr = &H45013D        '문자열 시작 주소
Private Const Add_TypeStrLen = &H450136     '문자열 길이
선언하여 사용하였다.(언어는 VB 6.0)

먼저 프로그램 선언부에 다른 프로그램의 메모리 영역을 접근하기 위하여 아래와 같은 API 선언을 하였다.(물론 실제 소스에선
부수적인 처리를 위해 많은 API들이 추가 선언 되었다.)
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Private Declare Function OpenProcess Lib "kernel32" (ByVal dwDesiredAccess As Long, ByVal bInheritHandle As Long, ByVal dwProcessId As Long) As Long
Private Declare Function ReadProcessMemory Lib "kernel32" (ByVal hProcess As Long, lpBaseAddress As Any, lpBuffer As Any, ByVal nSize As Long, lpNumberOfBytesWritten As Long) As Long
Private Declare Function WriteProcessMemory Lib "kernel32" (ByVal hProcess As Long, lpBaseAddress As Any, lpBuffer As Any, ByVal nSize As Long, lpNumberOfBytesWritten As Long) As Long
Private Declare Function VirtualProtectEx Lib "kernel32" (ByVal hProcess As Long, lpAddress As Any, ByVal dwSize As Long, ByVal flNewProtect As Long, lpflOldProtect As Long) As Long
Private Declare Function VirtualAlloc Lib "kernel32" (lpAddress As Any, ByVal dwSize As Long, ByVal flAllocationType As Long, ByVal flProtect As Long) As Long

여기서 접근 권한을 해제하는데 사용되어지는 API는 VirtualProtectEx이며,

Private Const PROCESS_VM_READ = (&H10)
Private Const PROCESS_VM_WRITE = (&H20)
Private Const PROCESS_VM_OPERATION = (&H8)
Private Const PROCESS_QUERY_INFORMATION = (&H400)
Private Const PROCESS_READ_WRITE_QUERY = PROCESS_VM_READ + PROCESS_VM_WRITE + PROCESS_VM_OPERATION + PROCESS_QUERY_INFORMATION

Private Const PAGE_READWRITE = &H4&

같이 위와같은 상수들도 선언하여 주었다.

선언후에 프로그램 핵심부에서는 아래와 같이 위의 고정 OFFSET에서 문자열의 길이와 내용을 불러낼 수 있었다.
    '문자열 길이를 저장한 주소공간 변수인, Add_TypeStrLen과 윈도우 캡션명(핸들을 얻기 위함)을 넘기고

    Int_ReadLen = GetLong(Add_TypeStrLen, "큐X일 채팅(게임)")
    '이상하게 값중에 쓰레기 값이 오는것이 있어.. 그걸 걸래내기 위함
    If (Int_ReadLen > &H50) Or (Int_ReadLen < &H10) Then
        Text1.Text = "(" & Int_ReadLen & ") 값이 작거나 커 처리 하지 않습니다!!" & vbCrLf
        Exit Sub
    End If
    '읽어온 문자열 길이만큼 빈공간 생성
    Str_Return = Space$(Int_ReadLen)
    '읽어온 길이와 실제 문자열이 저장된 공간의 주소값을 넘겨 문자열을 가져옴
    Str_Return = GetString(Add_TypeStr, Int_ReadLen, "큐X일 채팅(게임)")
    Str_Return = Trim(Str_Return)
    Lbl_TypeString.Caption = Str_Return

아래는 위에서 사용되어진 사용자 함수의 원형 별거 없음 캡션으로 핸들구해서 pid구한 후 해당 pid에
고정 OFFSET 값에 저장되어진 값들을 불러내서 리턴혀주는게 다인 뭐 그냥 그런 사용자 함수.

Private Function GetString(address As Long, Length As Long, WinTitle As String) As String
   
    hWin = FindWindow(vbNullString, WinTitle)
    Call GetWindowThreadProcessId(hWin, pid)

    hProcess = OpenProcess(PROCESS_READ_WRITE_QUERY, False, pid)
    sBuffer = String(Length, 0)
    test = ReadProcessMemory(hProcess, ByVal address, ByVal sBuffer, ByVal Length, 0)
    GetString = sBuffer
    CloseHandle hProcess
   
End Function

Private Function GetLong(address As Long, WinTitle As String) As Long
   
    hWin = FindWindow(vbNullString, WinTitle)
    Call GetWindowThreadProcessId(hWin, pid)

    hProcess = OpenProcess(PROCESS_READ_WRITE_QUERY, False, pid)
    test = ReadProcessMemory(hProcess, ByVal address, longBuffer, ByVal 4, 0)
    CloseHandle hProcess
    GetLong = longBuffer

End Function

요래저래 해서 문자열까지 불러오는데 성공....

Private Sub Send_TypeStr(ByVal Send_Str As String, ByVal str_len As Long)
   
    Dim Chk_Flag As Long
   
    'Chk_Flag = SendMessage(ChatBox_hWnd, WM_SETTEXT, -1, ByVal CStr(Send_Str))
    'Call Delay(1)
    'Chk_Flag = PostMessage(ChatBox_hWnd, WM_KEYDOWN, vbKeyReturn, 0)
    'Call Delay(1)
    'Chk_Flag = PostMessage(ChatBox_hWnd, WM_KEYUP, vbKeyReturn, 0)
   
    Chk_Flag = SendMessage(ChatBox_hWnd, WM_SETTEXT, -1, ByVal CStr("/" & Send_Str))
    'Debug.Print TypeSpeed_Interval(Val(Txt_TypeSpeed.Text), str_len)
    Call Delay(TypeSpeed_Interval(Val(Txt_TypeSpeed.Text), str_len))
    Chk_Flag = PostMessage(ChatBox_hWnd, WM_KEYDOWN, vbKeyReturn, 0)
    Chk_Flag = PostMessage(ChatBox_hWnd, WM_KEYUP, vbKeyReturn, 0)
    DoEvents

End Sub

Private Function TypeSpeed_Interval(ByVal Input_TypeSpeed As Long, str_len) As Single
   
    TypeSpeed_Interval = Int(60 * (LenB(Lbl_TypeString.Caption))) / Input_TypeSpeed
   
End Function

위의 사용자 함수를 하나 더 맹글고 문자열과 길이를 받아(길이는 그냥 단순히 원하는 타수를 비스무레하게 만들기위한
딜레이를 주기위한 파라메터)

SendMessage API를 사용하여 가져온 문자열을 채팅창에 뿌려준 후, 길이로 대충 계산한 시간만큼 딜레이 시키고,
PostMessage API를 사용하여 엔터키까지 날려주면 끝.. ㅡㅡ;;

써놓고 나니 횡설 수설인듯.. 암턴 그리하야... 만들어진 플그램 화면을 보면 아래와 같음.. 아... 이놈에 무지한 디자인 감각..

아쉽게도 너무 오래된 내용이어서 실제 돌아가는 화면을 캡쳐 떠놓진 못했지만... 머 그때 당시엔 훌륭하게 동작하였음...

케케케 완죤 재미난 분석작업 이었음..

아... 그나저나 두게임 다 한방에 올릴라고 했는데 겁나 길어져서 담에 올려야 겠음...

그럼 다들 즐거운 밤들 되셔욤~~

바이바이~~


Posted by 땡보