본문 바로가기
게임 해킹

AssualtCube 게임핵 제작(체력, 탄약, 에임핵, 월핵 등...)

by L3m0n S0ju 2021. 7. 13.

 

 

안녕하세요 이번 게시물은 AssaultCube라는 고전 FPS 게임핵 제작 방법을 알려드리고자 합니다. AssaultCube는 오픈소스이기 때문에 코드를 변경하여도 법적인 문제가 되지 않습니다. 코드는 유튜브 재즐보프 채널을 참고했습니다.

 

 

아래 동영상은 게임핵 제작의 결과물입니다. Health Hack, Ammo Hack을 클릭하면 각각 체력과 탄약이 1000개로 고정되고 Wall Hack을 체크하면 상대방의 위치가 네모박스로 표시됩니다. 에임핵은 마우스 오른쪽을 누르면 가장 각도가 가까운 상대방을 조준하도록 설계했습니다.

 

 

Form1.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using ProcessMemoryReaderLib;

namespace CheatEngine01
{
    public partial class Form1 : Form
    {
        Process[] MyProcess; // 프로세스 목록을 저장할 장소
        ProcessMemoryReader mem = new ProcessMemoryReader(); // ProcessMemoryReader 라이브러리에서 있는 클래스


        OverlayForm overlayForm = new OverlayForm(); // 나중에 월핵을 만들때 사용할 Form

        bool attach = false; // 프로세스가 선택됬는지 안됐는지 상태 초기화
        bool healthHack = false; // 초기화
        bool ammoHack = false; // 초기화
        bool wallHack = false; // 초기화

        PlayerData mainPlayer; // 플레이어 본인의 구조체를 나타내는 PlayerData 클래스 생성 
        PlayerData[] enemyPlayer = new PlayerData[30]; 상대방 플레이어 구조체를 나타내는 PlayerData 배열 클래스 생성



        Process attachProc; // 프로세스 클래스 생성

        public Form1()
        {
            InitializeComponent(); // Form 생성하면 자동으로 생기는 초기화 함수
        }

        private void Form1_Load(object sender, EventArgs e) // Form 생성하면 자동으로 생기는 함수
        {
        
        }




        private void ExitBT_Click(object sender, EventArgs e)
        {
            DialogResult result; // 닫기 실행할거냐?
            result = MessageBox.Show("종료하시겠습니까?", "종료메시지", MessageBoxButtons.OKCancel);

            if(result==DialogResult.OK) // 닫기 실행하면 앱 종료
            {
                this.DialogResult = DialogResult.Abort;
                Application.Exit();
            }
        }


      
        private void comboBox1_Click(object sender, EventArgs e) // 클릭했을 때 프로세스 목록  
        {
            comboBox1.Items.Clear(); // 기존의 프로세스 초기화
            MyProcess = Process.GetProcesses(); // 프로세스 목록을 불러옴
            for(int i=0; i < MyProcess.Length; i++ ) // 여기에 작성되는 내용이 총 프로세스 개수 만큼 진행됨  
            {
                string text = MyProcess[i].ProcessName + "-" + MyProcess[i].Id; // 프로세스 이름과 번호 조합해서 목록 생성


                comboBox1.Items.Add(text); // 목록에 추가 
            }
        }

         
        private void comboBox1_SelectedIndexChanged(object sender, EventArgs e) // 콤보박스 메뉴 중에 어떤 항목을 클릭했을때 동작할 내용


        {
            try
            {
                if (comboBox1.SelectedIndex != -1) // 목록을 선택했다면
                {
                    string selectedItem = comboBox1.SelectedItem.ToString(); // ex) KaKaoTalk-704
                    int pid = int.Parse(selectedItem.Split('-')[selectedItem.Split('-').Length - 1]); // 문자열을 -로 나눈 후 가장 마지막 문자열 pid를 가져온다.


                    attachProc = Process.GetProcessById(pid); // 처음에 생성한 Process 클래스 attachProc에 선택한 프로세스 저장

                    mem.ReadProcess = attachProc; // ProcessMemoryReader 클래스 mem에 어떤 프로세스를 열지를 저장

 
                    mem.OpenProcess(); // mem에 저장한 프로세스 열기
                    MessageBox.Show("프로세스 열기 성공! " + attachProc.ProcessName); // 프로세스 열기 성공
                    int base_ptr = attachProc.MainModule.BaseAddress.ToInt32() + 0x0010F4F4; // 게임 프로세스의 base 주소와 플레이어 구조체를 나타내는 offset(0x0010f4f4)을 더하여 절대주소 저장 -> 플레이어 구조체 offset 주소는 치트엔진을 이용해 쉽게 구할 수 있음


                    int player_base = mem.ReadInt(base_ptr); // 절대주소를 정수형으로 저장
                    mainPlayer = new PlayerData(player_base); // 플레이어 본인의 구조체 mainPlayer 변수에 저장
                    attach = true; // 프로세스가 오류없이 선택됨
                }  
            }
            catch(Exception ex) // 시도했을 때 예외 처리
            {
                attach = false; // 오류 발생
                MessageBox.Show("프로세스 열기 실패! " + ex.Message);
            }
        }




        private void timer1_Tick(object sender, EventArgs e) //0.1초마다 동작
        {
            if (attach) // 프로세스가 오류 없이 선택되면
            {
                try
                {
                    mainPlayer.SetPlayerData(mem); // 데이터 모니터링 -> 프로세스에서 데이터 가져와서 플레이어 구성요소 저장
                    if (healthHack) // 체력 핵이 켜져있으면
                    {
                        mainPlayer.hackHealth(mem); // 체력 1000으로 고정
                    }
                    if (ammoHack) // 탄약 핵이 켜져있으면
                    {
                        mainPlayer.hackAmmo(mem);  // 탄약 1000으로 고정
                    }

                    int hotkey = ProcessMemoryReaderApi.GetKeyState(0x02);  // 마우스 오른쪽 키에 대한 상태를 확인 
                    if (wallHack|| (hotkey & 0x8000)!=0) // 월핵 또는 에임핵(마우스 오른쪽 키)이 눌려있다면 
                    {
                        GetEnemyState(mem); // 적들에 대한 정보 습득
                    }
                       

                    if (wallHack)
                    {
                        overlayForm.hackWall(mainPlayer, enemyPlayer); 월핵이 체크되어있다면 overlayForm의 hackWall 함수 실행
                    }
                
                    if((hotkey & 0x8000) !=0) // 마우스 오른쪽 키가 눌려있다면...
                    {                    
                        float min_err = 100000; // 에러를 굉장히 큰 값으로 초기화 -> 상대방 플레이어와의 각도를 계산해 가장 작은 에러 값을 선택하여 에임핵을 제작할것임 
                        float err=0; // 초기화
                        double min_x_angle = 0; // 초기화 
                        double min_z_angle = 0; // 초기화
                    
                        for (int i = 0; i < 30; i++) // 플레이어 30명에 대하여
                        {
                            // 에임 핵 알고리즘
                            err=mainPlayer.getAimErr(mem, enemyPlayer[i].head_x_angle, enemyPlayer[i].head_z_angle); // getAimErr 함수에서 에러 값을 가져온서 err에 저장


                            if(min_err>err) // 가장 작은 err를 구하고 그때 i 번째 플레이어의 각도도 저장한다.
                            {
                                min_err = err;
                                min_x_angle = enemyPlayer[i].head_x_angle;
                                min_z_angle = enemyPlayer[i].head_z_angle;
                            }
                        }
                        mainPlayer.hackAim(mem,min_x_angle,min_z_angle); // 저장한 i 번째 플레이어의 각도로 상대방 위치를 유추
                    }


                    HealthLBL.Text = "Health: " + mainPlayer.health; // Health 값 실시간 출력
                    AmmoLBL.Text = "Ammo: " + mainPlayer.ammo; // Ammo 값 실시간 출력
                    BulletProofLBL.Text = "BulletProof: " + mainPlayer.bullet_proof; // 방어력 값 실시간 출력
                    AngleLBL.Text = "Angle: " + mainPlayer.x_angle.ToString("#.##") + " | " + mainPlayer.z_angle.ToString("#.##"); // x,z 각도 실시간 출력


                    PositionLBL.Text = "Pos: " + mainPlayer.x_pos.ToString("#.##") + ", " + mainPlayer.y_pos.ToString("#.##") + ", " + mainPlayer.z_pos.ToString("#.##"); // x,y,z 좌표 실시간 출력
                }
                catch { } // 오류 발생한다면 ..
            }
        }



        private double Get2DDegree(PlayerData mainPlayer, PlayerData enemyPlayer) // 좌표 평면 각도 차이 계산
        { 
            double x = enemyPlayer.x_pos - mainPlayer.x_pos; // x 좌표 차이
            double y = enemyPlayer.y_pos - mainPlayer.y_pos; // y 좌표 차이
            double correction = 90; // 보정 값 -> 예를 들어 359도와 1도의 차이는 실제론 2도지만 358도 차이로 인식하는 오류에 대한 보정 값

            if (y >= 0) // 보정 값
            { 
                if (x >= 0) 
                { 
                    correction = 90;            
                } 
                else 
                { 
                    correction = 270; 
                }               
            } 
            else // 보정 값
            { 
                if(x>=0) 
                { 
                    correction = 90; 
                } 
                else 
                { 
                    correction = 270; 
                } 
            } 
            return correction + Math.Atan(y / x) * 180 / Math.PI; // 상대 플레이어와의 평면 각도 차이
        } 


        private double GetZDegree(PlayerData mainPlayer, PlayerData enemyPlayer) 3차원 각도 차이 계산
        {
            double xy_distance = Math.Sqrt(Math.Pow(mainPlayer.x_pos - enemyPlayer.x_pos, 2) + Math.Pow(mainPlayer.y_pos - enemyPlayer.y_pos, 2)); // 평면 거리
            double z = enemyPlayer.z_pos - mainPlayer.z_pos; // 높이 차이

            return Math.Atan(z/ xy_distance)* 180 / Math.PI; // 상대 플레이어와의 높이 각도 차이
        }




        private double GetDistance(PlayerData mainPlayer, PlayerData enemyPlayer) // 상대 플레이어와의 거리 계산
        {
            //피타고라스 법칙을 이용해 xy_distance를 먼저 구함 (2D)
            double xy_distance = Math.Sqrt(Math.Pow(mainPlayer.x_pos - enemyPlayer.x_pos, 2) + Math.Pow(mainPlayer.y_pos - enemyPlayer.y_pos, 2));
            //피타고라스 법칙을 이용해 distance를 먼저 구함 (3D)
            double distance = Math.Sqrt(Math.Pow(xy_distance, 2) + Math.Pow(mainPlayer.z_pos - enemyPlayer.z_pos, 2));
            return distance ; // 거리 계산
        }

        private void GetEnemyState(ProcessMemoryReader mem)
        {
            int base_ptr = attachProc.MainModule.BaseAddress.ToInt32() + 0x00110D90; // 게임 프로세스 base 주소에 상대 플레이어 구조체 offset 주소를 더하여 상대 플레이어 구조체의 절대 주소 값 저장
            
            for (int i=0;i<30;i++) // 30번 플레이어 까지
            {
                int[] offsetArray = { i*4, 0 };
                int player_base = mem.ReadMultiLevelPointer(base_ptr, 4, offsetArray);
                enemyPlayer[i] = new PlayerData(player_base); // 상대 플레이어 각각 PlayerData 클래스 생성
                enemyPlayer[i].SetPlayerData(mem); // 구성 요소 초기화
                enemyPlayer[i].distance = GetDistance(mainPlayer, enemyPlayer[i]); // 거리 
                enemyPlayer[i].head_x_angle = Get2DDegree(mainPlayer, enemyPlayer[i]); //평면 각도 차이
                enemyPlayer[i].head_z_angle = GetZDegree(mainPlayer, enemyPlayer[i]); // 높이 각도 차이
            }
        }

        private void HealthBT_Click(object sender, EventArgs e) // Health 버튼 클릭 시
        {
            if(healthHack) // 만약 이미 체력 핵이 동작 중일 경우 체력 핵을 종료하고 텍스트를 동작안함으로 변경
            {
                healthHack=false;
                HealthHLBL.Text = "동작 안함";
            }
            else
            {
                healthHack = true;
                HealthHLBL.Text = "동작 중";
            }
        }

        private void AmmoBT_Click(object sender, EventArgs e) // 위와 동일
        {
            if (ammoHack)
            {
                ammoHack = false;

                AmmoHLBL.Text = "동작 안함";
            }
            else
            {
                ammoHack = true;
                AmmoHLBL.Text = "동작 중";
            }
        }

        private void WallHackCHB_CheckedChanged(object sender, EventArgs e)
        {         
            if (WallHackCHB.Checked == true)  // 만약에 체크박스가 체킹되어 있다면... 
            {   
                overlayForm.Show(); // 보여준다!
                wallHack = true;
            }
            else // 만약에 체크박스가 체킹되어 있지 않다면... 
            {
                overlayForm.Hide(); // 숨긴다!
                wallHack = false;
            }
        }
    }
}

 

 

 

 

PlayerData.cs

using ProcessMemoryReaderLib;
using System;
using System.Diagnostics;

namespace CheatEngine01
{
    internal class PlayerData
    {
        int base_addr; // "ac_client.exe"+00169A38 구조체의 위치

        // 구조체 모양
        int health_offset = 0xF8; // 체력 오프셋
        int bullet_proof_offset = 0xFC; // 방어력 오프셋
        int ammo_offset = 0x150; // 탄약 오프셋
        int x_pos_offset = 0x34; // x 좌표 오프셋
        int y_pos_offset = 0x38; // y 좌표 오프셋
        int z_pos_offset = 0x3c;  // z 좌표 오프셋
        int x_angle_offset = 0x40; // x 각도 오프셋
        int z_angle_offset = 0x44; // z 각도 오프셋

        // 캐릭터 정보
        public int health;
        public int bullet_proof;
        public int ammo;
        public float x_pos;
        public float y_pos;
        public float z_pos;
        public float x_angle;
        public float z_angle;
        public double distance;
        public double head_x_angle;
        public double head_z_angle;

        public PlayerData(int player_base) // mainPlayer 또는 적 플레이어 구조체의 위치, 모두 0으로 초기화
        {
            base_addr = player_base;
            health = 0;
            bullet_proof=0;
            ammo =0;
            x_pos=0;
            y_pos=0;
            z_pos=0;
            x_angle=0;
            z_angle=0;
            distance=0;
            head_x_angle=0;
            head_z_angle=0;
    }

        public void SetPlayerData(ProcessMemoryReader mem) // 실제 값들 저장
        {
            health = mem.ReadInt(base_addr + health_offset);
            bullet_proof = mem.ReadInt(base_addr + bullet_proof_offset);
            ammo = mem.ReadInt(base_addr + ammo_offset);
            x_pos = mem.ReadFloat(base_addr + x_pos_offset);
            y_pos = mem.ReadFloat(base_addr + y_pos_offset);
            z_pos = mem.ReadFloat(base_addr + z_pos_offset);
            x_angle = mem.ReadFloat(base_addr + x_angle_offset);
            z_angle = mem.ReadFloat(base_addr + z_angle_offset);
        }

        internal void hackHealth(ProcessMemoryReader mem)
        {
            mem.WriteInt(base_addr + health_offset, 1000); // 체력 1000으로...
        }

        internal void hackAmmo(ProcessMemoryReader mem)
        {
            mem.WriteInt(base_addr + ammo_offset, 1000); // 탄약 1000으로...
        }


        internal void hackAim(ProcessMemoryReader mem, double x_angle, double z_angle)
        {
            float _x = Double2Float(x_angle); // double -> float
            float _z = Double2Float(z_angle); // double -> float
            mem.WriteFloat(base_addr + x_angle_offset, _x); // x 각도 세팅
            mem.WriteFloat(base_addr + z_angle_offset, _z); // z 각도 세팅
        }

        private float Double2Float(double input) // double -> float 변경 함수
        {
            float result = (float)input;
            if(float.IsPositiveInfinity(result))
            {
                result = float.MaxValue;
            }
            else if(float.IsNegativeInfinity(result))
            {
                result = float.MinValue;
            }
            return result;
        }

        internal float getAimErr(ProcessMemoryReader mem, double _x_angle, double _z_angle) // 각도 차이를 각각 제곱해서 더함
        {
            return Double2Float (Math.Pow(x_angle - _x_angle, 2) + Math.Pow(z_angle - _z_angle, 2));
        }
    }
}

 

 

 

OverlayForm.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace CheatEngine01
{
    public partial class OverlayForm : Form
    {
        Graphics g; // Graphics 클래스 변수 g 선언
        Pen myPen = new Pen(Color.Red); // 빨간색 Pen 클래스 변수 myPen 선언
        IntPtr hAssaultCube; // 핸들을 저장할 변수
        PosEnemy[] posEnemy = new PosEnemy[30]; // 상대 위치 좌표 클래스

        public const string WINDOWNAME = "AssaultCube"; // 게임 윈도우 창 이름 저장
        RECT rect; // 윈도우 형태 저장할 변수

        [StructLayout(LayoutKind.Sequential)]
        public struct RECT
        {
            public int Left;        // x position of upper-left corner
            public int Top;         // y position of upper-left corner
            public int Right;       // x position of lower-right corner
            public int Bottom;      // y position of lower-right corner
        }
        public struct PosEnemy
        {
            public float x;
            public float z;
            public float size;
        }

        [DllImport("user32.dll")]
        static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);  // 윈도우 창 속성 변경하는 함수

        [DllImport("user32.dll", EntryPoint = "GetWindowLong")] // 윈도우 창 정보 가져오는 함수
        static extern int GetWindowLong(IntPtr hWnd, int nIndex);

        [DllImport("user32.dll", SetLastError = true)] // 윈도우 창 모양 알아내는 함수
        static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect);

        [DllImport("user32")] // 윈도우 이름을 통해 윈도우 창을 찾는 함수
        public static extern IntPtr FindWindow(String lpClassName, String lpWindowName);


        public OverlayForm()
        {
            InitializeComponent(); // 기본적으로 생성됨
        }

        private void OverlayForm_Load(object sender, EventArgs e)
        {
            // 창에 대한 속성 조절
            this.BackColor = Color.Wheat; // 배경 색 없음
            this.TransparencyKey = Color.Wheat; // 투명한 영역에다가 이미지 업데이트 -> 게임 창 위에 새로운 투명한 창을 생성하여 상대편 플레이어의 위치를 나타낼 것임


            this.TopMost = true; // 가장 상단 노출 -> 창이 뒤로 가면 상대방 플레이어 표시가 안보일 수 있음
            this.FormBorderStyle = FormBorderStyle.None; // 창의 틀이 완전히 삭제

            int presentStyle = GetWindowLong(this.Handle, -20); // 윈도우 창 정보 가져옴
            SetWindowLong(this.Handle, -20, presentStyle | 0x80000 | 0x20); // 마우스 이벤트를 뒤로 전달 속성 추가

            // 창에 대한 위치와 크기 조절
            hAssaultCube = FindWindow(null, WINDOWNAME); // AssaultCube 창을 찾아 핸들을 저장
            GetWindowRect(hAssaultCube, out rect); // 윈도우 창 모양을 알아내는 함수 -> AssaultCube 게임 창의 형태를 알 수 있음


            // 창 사이즈
            int height = rect.Bottom - rect.Top; // 높이
            int width = rect.Right - rect.Left; // 폭
            this.Size = new Size(width, height);

            //창 위치
            this.Top = rect.Top;
            this.Left = rect.Left;

            timer1.Enabled = true;
        }

     

        // 모든 창이 초기화된 뒤에 사용되도록 로드되는 마지막에 타이머 온
        private void timer1_Tick(object sender, EventArgs e)
        {
            GetWindowRect(hAssaultCube, out rect);

            // 창 사이즈
            int height = rect.Bottom - rect.Top;
            int width = rect.Right - rect.Left;
            this.Size = new Size(width, height);

            //창 위치
            this.Top = rect.Top;
            this.Left = rect.Left;

        }

        private void OverlayForm_Paint(object sender, PaintEventArgs e)
        {
            g = e.Graphics; // 발생한 이벤트 e에 Graphics를 Graphics 클래스 변수 g에 저장

            for(int i=0;i<10;i++) // 상대방 플레이어 10명까지
            {
                if(posEnemy[i].x!=-1234) // 상대가 시야 안에 있을 때 -> -1234는 상대가 내 시야 밖에 있음을 나타냄


                {
                    g.DrawRectangle(myPen,posEnemy[i].x-posEnemy[i].size/2 , posEnemy[i].z-posEnemy[i].size/2, posEnemy[i].size, posEnemy[i].size*2); // 빨간색이 설정된 myPen을 이용해 좌표를 기준으로 상대방 플레이어 위치에 사각형 생성

                }
            }
        }

        internal void hackWall(PlayerData mainPlayer, PlayerData[] enemyPlayer)
        {
            for (int i=0; i<10;i++) // 10명의 플레이어를 검사
            {
                float x_angle_pos= mainPlayer.x_angle - Double2Float(enemyPlayer[i].head_x_angle); // 나와 상대방의 x 각도 차이


                float z_angle_pos= mainPlayer.z_angle - Double2Float(enemyPlayer[i].head_z_angle); // 나와 상대방의 z 각도 차이

                // 실제 각도와 다르게 측정되는 경우, 실제 각도로 보정
                // 예: 359 - 1 = 358 --> -2도
                // 예: 1 - 359 = -358 --> 2도


                if(360-45<=Math.Abs(x_angle_pos)&&Math.Abs(z_angle_pos)<=360)  // 보정 값 적용
                {
                    if(x_angle_pos>0)
                    {
                        x_angle_pos -= 360;
                    }
                    else
                    {
                        x_angle_pos += 360;
                    }
                }

                if((Math.Abs(x_angle_pos)<=45)&& enemyPlayer[i].health > 0) // 내 시야에 있는 것 체크, 상대방이 죽으면 사각형이 제거되도록 설계


                {

                    // 창 크기를 시야 각도로 나눠서 상대방의 위치를 탐지하는 알고리즘


                    float x_corr = (rect.Right - rect.Left) / 90 * x_angle_pos; 

                    float z_corr = (rect.Bottom - rect.Top) / 60 * z_angle_pos;
                    posEnemy[i].x = ((rect.Right - rect.Left)/2) - x_corr; 
                    posEnemy[i].z = ((rect.Bottom - rect.Top)/2) + z_corr;
                    posEnemy[i].size =Double2Float( 1800 / enemyPlayer[i].distance); // 1800은 여러 후보들 가운데 적절한 크기 선정


                }
                else // 상대가 시야 밖일 때
                {
                    posEnemy[i].x = -1234; // 시야 밖
                    posEnemy[i].z = -1234; // 시야 밖   
                }
            }
            this.Invalidate(); // 페인트 초기화
        }

        private float Double2Float(double input) // double을 float로 변환하는 함수
        {
            float result = (float)input;
            if (float.IsPositiveInfinity(result))
            {
                result = float.MaxValue;
            }
            else if (float.IsNegativeInfinity(result))
            {
                result = float.MinValue;
            }
            return result;
        }
    }
}

 

 

 

 

ProcessMemoryReader.cs 라이브러리

 


생략... // 깃허브에 있습니다.

 

 

'게임 해킹' 카테고리의 다른 글

게임 메모리 해킹 - 스타듀 밸리  (0) 2021.07.14
게임 메모리 해킹 - 포켓몬스터  (0) 2021.06.30

댓글