포스트

packet sniffing 프로젝트 뜯어보기

앞서서

이 포스트는 내가 원하는 기능의 앱을 개발하기 위한 기본 과정을 참조할 수 있는 깃헙 프로젝트를 학습 목적으로 직접 분석해보는 포스트입니다.

실제로 이 프로젝트를 사용하는게 아닙니다.

감사콩


프로젝트의 구조

이 프로젝트는 따져보자면 Windows Service 템플릿의 프로젝트이다. 앱의 설정을 담당하는 App.config 와 엔트리포인트를 담당하는 Program.cs 두 개의 베이스코드를 제외하면 모두 직접 생성한 서비스들이다.


Program.cs

엔트리 포인트인 메인 메서드가 있는 Program.cs 파일에 스태틱 변수나 메서드들이 많이 있다.

네임스페이스 내의 전체 코드는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
internal static class Program
{
    public static bool IsConsole = Console.OpenStandardInput(1) != Stream.Null;
    [STAThread]
    static void Main(string[] args)
    {
        Properties.Settings.Default.Providers.Clear();
        Bluegrams.Application.PortableSettingsProvider.SettingsFileName = AppDomain.CurrentDomain.FriendlyName + ".ini";
        Bluegrams.Application.PortableSettingsProvider.ApplyProvider(Properties.Settings.Default);
        if (!AdminRelauncher()) return;
        VersionCompatibility();
        if (!IsConsole) Warning();
        AttemptFirewallPrompt();
        if (!IsConsole)
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new MainWindow());
        }
        else
        {
            var httpBridge = new HttpBridge();
            httpBridge.args = args;
            httpBridge.Start();
        }
        if (File.Exists(Utilities.Logger.fileName) && Properties.Settings.Default.AutoUpload)
        {
            var fileBytes = File.ReadAllBytes(Utilities.Logger.fileName);
            var fileText = File.ReadAllText(Utilities.Logger.fileName);
            if (fileBytes.Length > 100 && fileText.Contains("8|"))
                Utilities.Uploader.UploadLog(fileBytes);
        }
    }
    static void AttemptFirewallPrompt()
    {
        var ipAddress = Dns.GetHostEntry(Dns.GetHostName()).AddressList[0];
        var ipLocalEndPoint = new IPEndPoint(ipAddress, 12345);
        var t = new TcpListener(ipLocalEndPoint);
        t.Start();
        t.Stop();
    }
    static void VersionCompatibility()
    {
        (var region, var installedVersion) = VersionCheck.GetLostArkVersion();
        if (installedVersion == null)
        {
            MessageBox.Show("Launch Lost Ark before launching logger", "Lost Ark Not Running", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            Environment.Exit(0);
        }
        else if (region == Region.Unknown)
            MessageBox.Show("DPS Meter is out of date.\nDPS Meter might not work until updated.\nCheck Discord/Github for more info.\nFeel free to add a message in the discord informing shalzuth that it's out of data", "Out of date!", MessageBoxButtons.OK, MessageBoxIcon.Warning);
        else if (Properties.Settings.Default.Region != region)
        {
            Properties.Settings.Default.Region = region;
            Properties.Settings.Default.Save();
        }
    }
    static void Warning()
    {
        if (Properties.Settings.Default.IgnoreWarning) return;
        if (AppDomain.CurrentDomain.FriendlyName.Contains("LostArk"))
        {
            //var tempName = "LostArkDps" + Guid.NewGuid().ToString().Substring(0, 6) + ".exe";
            var tempName = "DpsMeter.exe";
            MessageBox.Show("LostArkLogger.exe is flagged.\nRenaming to " + tempName + " !", "Error!", MessageBoxButtons.OK, MessageBoxIcon.Error);
            File.Copy(AppDomain.CurrentDomain.FriendlyName, tempName);
            Process.Start(tempName);
            Environment.Exit(0);
        }
        else
        {
            Process.GetProcessesByName("LostArkLogger").ToList().ForEach(p => p.Kill());
            if (File.Exists("LostArkLogger.exe")) File.Delete("LostArkLogger.exe");
        }
        var res = MessageBox.Show("The game director has instructed Amazon Game Studios to ban users using a DPS Meter.\n\nAt this time, please refrain from using the DPS Meter.\n\nSelect \"Retry\" to voice your feedback, as this is not a hack nor a solution that should violate TOS", "Warning!", MessageBoxButtons.AbortRetryIgnore, MessageBoxIcon.Warning);
        if (res == DialogResult.Abort) Environment.Exit(0);
        else if (res == DialogResult.Retry)
        {
            Process.Start("https://forums.playlostark.com/t/talk-to-us-already-about-the-dps-meter/370558");
            Environment.Exit(0);
        }
        Properties.Settings.Default.IgnoreWarning = true;
        Properties.Settings.Default.Save();
    }
    private static bool AdminRelauncher()
    {
        if (!new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator))
        {
            var startInfo = new ProcessStartInfo
            {
                UseShellExecute = true,
                WorkingDirectory = Environment.CurrentDirectory,
                FileName = Assembly.GetEntryAssembly().CodeBase.Replace(".dll", ".exe"),
                Verb = "runas"
            };
            try { Process.Start(startInfo); }
            catch (Exception ex) { MessageBox.Show("This program must be run as an administrator!", "Error!", MessageBoxButtons.OK, MessageBoxIcon.Error); }
            return false;
        }
        return true;
    }
}

가장 먼저나온 구문이다.

1
2
3
4
5
6
7
public static bool IsConsole = Console.OpenStandardInput(1) != Stream.Null;

[STAThread]
static void Main(string[] args)
{
    ...
}

IsConsole 변수는 단순히 bool 값이고, 변수에 해당하는 값은 Console.OpenStandardInput(1) 로 받은 1바이트 크기의 인풋 스트림이 Null 이 아닌지의 여부이다.

디버그 모드에서의 IsConsole 결과값은 True 였다.

메인 메서드에는 STAThread 속성이 사용되었다. 툴팁의 설명은 아래와 같다.

어플리케이션의 COM 쓰레딩 모델이 STA(Single-Threaded Apartment) 임을 명시하는 속성

여기서 말하는 COM 이란 무엇일까? MS 도큐먼트에는 다음과 같이 정의되어있다.

COM is a platform-independent, distributed, object-oriented system for creating binary software components that can interact. COM is the foundation technology for Microsoft’s OLE (compound documents) and ActiveX (Internet-enabled components) technologies.

자세한 내용은 해당 포스트(COM 이란 무엇인가) 에 정리해놓겠다.

중요한 것은 쓰레딩 모델이 STA 라는 것인데, 이는

자세한 내용은 또 따로 포스트(COM 의 Processes, Threads, Apartments)에 정리해놓겠다.


다음은 메인 메서드이다.

1
2
3
Properties.Settings.Default.Providers.Clear();
Bluegrams.Application.PortableSettingsProvider.SettingsFileName = AppDomain.CurrentDomain.FriendlyName + ".ini";
Bluegrams.Application.PortableSettingsProvider.ApplyProvider(Properties.Settings.Default);

Properties.Settings.DefaultApplication Settings 와 관련된 코드이다. Application Settings 의 자세한 내용은 .NET(Dotnet) application settings 관리 포스트를 확인하자.

정확힌 모르겠다. Properties.Settings.Default.Providers 는 wrapper 클래스에서 사용된 application settings providers 의 collection 을 반환하며, Clear 메서드는 해당 collection 의 모든 item 들을 삭제한다.

그 다음 Bluegrams 는 개발을 위한 오픈소스 소프트웨어 툴을 만드는 곳인데, 그 중에 PortableSettingsProvider 는 .NET 프레임워크의 application settings 구조 내부의 SettingsProvider 클래스를 커스텀해 built-in Settings Provider 와 비슷하게 만든 implementation 이다. App Settings 를 XML 포맷으로 저장할 수 있다.

깃헙에서 소개하는 기능은 아래와 같다.

  • Make application settings portable together with the app
  • Configure the location and name of the settings file
  • Easily plug into existing apps (just one line needed to make existing app settings portable)

기본적으로 .NET 에서 제공하는 Applcation Settings 값들을 그대로 적용해 저장할 파일 이름까지 설정하는 코드인 것 같다.

참고로 Application Settings 관련 설정된 User-Scoped Settings 값들은 Project Designer 또는 App.config 에서 확인 가능하다.

project designer - settings

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="utf-8"?>
<configuration>
 <!-- 다른 설정 내용 생략 -->
 <userSettings>
  <TestProject.Properties.Settings>
   <setting name="IgnoreWarning" serializeAs="String">
    <value>False</value>
   </setting>
   <setting name="Region" serializeAs="String">
    <value>Steam</value>
   </setting>
   <setting name="Npcap" serializeAs="String">
    <value>False</value>
   </setting>
   <setting name="AutoUpload" serializeAs="String">
    <value>False</value>
   </setting>
   <setting name="DisplayNames" serializeAs="String">
    <value>True</value>
   </setting>
  </TestProject.Properties.Settings>
 </userSettings>
</configuration>

다음 코드를 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void Main(string[] args)
{
    // 생략
    if (!AdminRelauncher()) return;
    // 생략
}

private static bool AdminRelauncher()
{
    if (!new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator))
    {
        var startInfo = new ProcessStartInfo
        {
            UseShellExecute = true,
            WorkingDirectory = Environment.CurrentDirectory,
            FileName = Assembly.GetEntryAssembly().CodeBase.Replace(".dll", ".exe"),
            Verb = "runas"
        };
        try { Process.Start(startInfo); }
        catch (Exception ex) { MessageBox.Show("This program must be run as an administrator!", "Error!", MessageBoxButtons.OK, MessageBoxIcon.Error); }
        return false;
    }
    return true;
}

WindowsPrincipal 은 윈도우의 group membership 을 확인할 수 있게 해주는 클래스이다. 해당 클래스의 IsInRole(WindowsBuiltInRole.역할) 메서드를 사용해 현재 프로그램이 해당 역할로 실행되었는지 확인할 수 있다. 위 코드의 경우 특정 WindowsIdentity 객체를 통해 초기화 하는 방법을 사용했다. WindowsIdentity 객체는 윈도우 유저를 나타내며, GetCurrent 메서드를 사용해 현재 윈도우 유저의 WindwosIdentity 객체를 반환받을 수 있다.

.NET 에서의 Principal, Identity 객체에 대한 정보는 해당 도큐먼트(Principal and Identity Objects) 를 확인하자.

admin 권한이 아닌 경우, if문 안의 내용이 실행된다. Process.Start 메서드를 이용해 외부 응용 프로그램을 실행하는데, ProcessStartInfo 를 설정하여 실행할 프로세스를 지정하는 코드이다.


잠깐 ProcessStartInfo 에 사용가능한 속성들을 살펴보자.

PropertiesTypeDefinition
UseShellExecutebool프로세스를 시작하기 위해 OS Shell 을 사용할 것인가
WorkingDirectorystringUseShellExecute 값이 true 인 경우 실행할 프로세스의 실행파일이 포함된 경로를 뜻한다.
UseShellExecute 값이 false 인 경우 WorkingDirectory 속성은 실행파일을 찾는데 사용되는게 아니다. 실행되는 프로세스의 context 내부에서 값 자체로서의 의미를 가진다.
FileNamestring시작할 어플리케이션의 이름 혹은 기본 실행 앱이 설정되어있는 파일 타입의 문서 이름
VerbstringFileName 에서 명시한 어플리케이션이나 문서를 열 때 사용할 Verb(action)

Verb 란?

각각의 파일 확장자는 자기만의 verb 들이 있다. 이는 Verbs 속성을 통해 알 수 있으며, ProcessStartInfo 객체를 초기화한 뒤 인스턴스.Verbs 에서 foreach 문을 통해 하나씩 확인해볼 수 있다. 그 외의 Verb 는 사용할 수 없다.

예를 들면, “print” verb 는 FileName 에 명시된 문서를 출력할 것이다. verb 의 예시로는 “Edit”, “Open”, “OpenAsReadOnly”, “Print”, and “Printto” 등이 있다. Verb 를 사용할 경우, FileName 속성에 꼭 파일 확장자를 같이 써주어야 한다.

추가적인 속성들은 도큐먼트 를 확인하자.


다시 돌아와서, 코드를 살펴보자.

1
2
3
4
UseShellExecute = true,
WorkingDirectory = Environment.CurrentDirectory,
FileName = Assembly.GetEntryAssembly().CodeBase.Replace(".dll", ".exe"),
Verb = "runas"

shell 을 사용하도록 설정하고, 실행파일의 경로는 현재 실행된 경로로 지정한다. Assembly.GetEntryAssembly 메서드로 기본 어플리케이션 도메인의 프로세스 실행 파일을 알아내고, CodeBase 속성으로 원본 어셈블리 경로를 받아낸다. 그 후 .dll 확장자를 .exe 로 변환한다. Verb 로는 “RunAs” 를 사용해서 다른 사용자 계정으로 실행한다. 아마도 어드민 계정이 사용될 때 까지 계속 새 프로세스를 실행하도록 설정하는 것 같다.

그 후 설정한 정보들로 새로운 Process 를 시작하고, 예외가 발생하면 어드민 권한으로 실행하라는 메세지 박스를 출력한다. 그 후 false 를 return 하여 현재의 프로세스는 Main 메서드에서 종료하도록 한다. 프로세스가 시작될 때 Admin 역할인 경우만 true 가 return 되기 때문에, 이 경우만 Main 메서드에서 다음 코드로 넘어갈 수 있게 해놓은 것 같다.

Assembly 란?

위 코드를 살펴보다가 Assembly 라는 단어가 생소해서 C# 에서 어떤 뜻인지 찾아봤다. Assembly 는 .NET 기반 어플리케이션에서 deployment, version control, reuse, activation scoping, and security permissions 등의 기본 단위이다. Assembly 는 executable(.exe) 혹은 dynamic link library(.dll) 의 형태를 가지며 .NET 어플리케이션의 기본 요소이다.

자세한 내용은 도큐먼트(Assemblies in .NET) 참조


다음 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void Main(string[] args)
{
    // 생략
    VersionCompatibility();
    // 생략
}

static void VersionCompatibility()
{
    (var region, var installedVersion) = VersionCheck.GetLostArkVersion();
    if (installedVersion == null)
    {
        MessageBox.Show("Launch Lost Ark before launching logger", "Lost Ark Not Running", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
        Environment.Exit(0);
    }
    else if (region == Region.Unknown)
        MessageBox.Show("DPS Meter is out of date.\nDPS Meter might not work until updated.\nCheck Discord/Github for more info.\nFeel free to add a message in the discord informing shalzuth that it's out of data", "Out of date!", MessageBoxButtons.OK, MessageBoxIcon.Warning);
    else if (Properties.Settings.Default.Region != region)
    {
        Properties.Settings.Default.Region = region;
        Properties.Settings.Default.Save();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[DllImport("kernel32")] public static extern bool QueryFullProcessImageName([In] IntPtr hProcess, [In] int dwFlags, [Out] StringBuilder lpExeName, ref int lpdwSize);

public static (Region, Version) GetLostArkVersion()
{
    var lostArkProcesses = Process.GetProcessesByName("LOSTARK");
    foreach(var lostArkProcess in lostArkProcesses)
    {
        var sb = new StringBuilder(1024);
        int bufferLength = sb.Capacity + 1;
        QueryFullProcessImageName(lostArkProcess.Handle, 0, sb, ref bufferLength);
        var lostArkExe = sb.ToString();
        var version = new Version(FileVersionInfo.GetVersionInfo(lostArkExe).ProductVersion.Split(' ')[0]);
        if (version == SupportedSteamVersion) return (Region.Steam, version);
        else if (version == SupportedKoreaVersion) return (Region.Korea, version);
        else return (Region.Unknown, version);
    }
    return (Region.Unknown, null);
    var fileName = @"C:\Program Files (x86)\Steam\steamapps\common\Lost Ark\Binaries\Win64\LOSTARK.exe";
    if (!File.Exists(fileName))
    {
        var installLocation = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 1599340")?.GetValue("InstallLocation");
        if (installLocation != null)
        {
            fileName = Path.Combine(installLocation.ToString(), "Binaries", "Win64", "LOSTARK.exe");
        }
    }
    if (File.Exists(fileName)) return (Region.Steam, new Version(FileVersionInfo.GetVersionInfo(fileName).ProductVersion.Split(' ')[0]));
    else return (Region.Steam, new Version("0.0.0.0"));

}

Main 에서 VersionCompatibility 라는 스태틱 메서드를 실행하는데, 그 메서드 첫줄에서 Utilities/VersionCheck.cs 파일에 정의된 GetLostArkVersion 스태틱 메서드를 실행하고 있다. 첫줄부터 심상치 않은 코드가 있다…


1
[DllImport("kernel32")] public static extern bool QueryFullProcessImageName([In] IntPtr hProcess, [In] int dwFlags, [Out] StringBuilder lpExeName, ref int lpdwSize);

첫번째 코드를 살펴보자. DllImportAttribute 를 사용하고 있다. 도큐먼트에 따르면 어트리뷰트가 사용된 메서드는 unmanaged DLL(Dynamic-Link Library) 로부터 실행됨을 뜻하는 것 같다.

DLL 은 간단히 말하면 공유 라이브러리(shared library)인데, 관련 자세한 내용은 따로 포스트 DLL(Dynamic-Link Library)란 무엇인가 에 정리해두었다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.