株式会社Ninjastars 技術研究部

「もっと楽しく安全なゲームの世界を創る」

RPA UIAutomationでアプリケーションを自動化する

株式会社Ninjastarsセキュリティエンジニアの一瀬です。
今日はセキュリティの話ではなく、最近流行のRPAにも使われる技術についてお話させていただこうと思います。
対象のOSはWindowsです。
情報セキュリティだけでなく読者の皆様に幅広い情報を提供したく、今回この記事を書かせていただきます。

環境
Windows10 64bit

今回やること
WIndows付属のタスクマネージャーの調査・自動化

ウィンドウ構造の調査
比較的知られたウィンドウ構造解析ツールとしてVisualStudio付属のspy++があります。

f:id:Ninjastars:20190114233128p:plain
タスクマネージャーをspy++で解析
spy++は優秀なツールですが、対象のアプリケーションのウィンドウのパーツがウィンドウハンドルを所持していないと情報の取得・操作が実現できません。
こういった時に出番となるのがUIAutomationという機構になります。またその解析ツールとしてInspectというWindowsSDK付属の便利ツールが存在します。

f:id:Ninjastars:20190114233833p:plain
タスクマネージャーをInspectで解析
コーディング
タスクマネージャーを操作・情報取得するプログラムを作成します。
使用言語はC#で開発させていただきます。

以下を参照に追加。
1.UIAutomationClient
2.UIAutomationTypes
3.WindowsBase

ターゲットプラットフォーム:x64。
実行時は管理者権限で実行をお願いします。

1.名前、状態、CPU、メモリ...のタブをInvoke()でクリック
2.パフォーマンスタブをマウスシミュレートでクリック
3.CPU使用率を取得

MouseOperations.cs

using System;
using System.Runtime.InteropServices;

public class MouseOperations
{
    [Flags]
    public enum MouseEventFlags
    {
        LeftDown = 0x00000002,
        LeftUp = 0x00000004,
        MiddleDown = 0x00000020,
        MiddleUp = 0x00000040,
        Move = 0x00000001,
        Absolute = 0x00008000,
        RightDown = 0x00000008,
        RightUp = 0x00000010
    }

    [DllImport("user32.dll", EntryPoint = "SetCursorPos")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool SetCursorPos(int x, int y);

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool GetCursorPos(out MousePoint lpMousePoint);

    [DllImport("user32.dll")]
    private static extern void mouse_event(int dwFlags, int dx, int dy, int dwData, int dwExtraInfo);

    public void SetCursorPosition(int x, int y)
    {
        SetCursorPos(x, y);
    }

    public void SetCursorPosition(MousePoint point)
    {
        SetCursorPos(point.X, point.Y);
    }

    public MousePoint GetCursorPosition()
    {
        MousePoint currentMousePoint;
        var gotPoint = GetCursorPos(out currentMousePoint);
        if (!gotPoint) { currentMousePoint = new MousePoint(0, 0); }
        return currentMousePoint;
    }

    public void MouseEvent(MouseEventFlags value)
    {
        MousePoint position = GetCursorPosition();

        mouse_event
            ((int)value,
                position.X,
                position.Y,
                0,
                0)
            ;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct MousePoint
    {
        public int X;
        public int Y;

        public MousePoint(int x, int y)
        {
            X = x;
            Y = y;
        }
    }
}

Program.cs

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Windows.Automation;
using System.Windows;

namespace UIAutomation
{
    class Program
    {

        private static AutomationElement mainForm;

        static void Main(string[] args)
        {
            Process process = System.Diagnostics.Process.Start("taskmgr.exe");
            try
            {
                
                Thread.Sleep(2000);
                
                //ウィンドウハンドルから、AutomationElementを取得
                mainForm = AutomationElement.FromHandle(process.MainWindowHandle);
                

                AutomationElement rowHeader = FindElementsByName(mainForm, "列のヘッダー").First();
               

                //Name:"列のヘッダー"の子要素を順にクリック
                foreach (var control in GetChildren(rowHeader))
                {
                    InvokeControl(control);
                    System.Threading.Thread.Sleep(1000);

                }

                MouseOperations mo = new MouseOperations();

                AutomationElement tabControl = FindElementsByName(mainForm, "タブ コントロール").First();

                //Name:"タブ コントロール"の子要素の2番目の要素を取得
                AutomationElement performanceTab = FindChildAt(tabControl, 1);
                
                //IsInvokePatternAvailabel = false なのでInvoke()でクリックは出来ない
                //BoundingRectangleで領域を取得し、マウスエミュレートでクリックを行う
                Rect rect = performanceTab.Current.BoundingRectangle;
                mo.SetCursorPosition((int)rect.Left + 5, (int)rect.Top + 5);
                mo.MouseEvent(MouseOperations.MouseEventFlags.LeftDown);
                System.Threading.Thread.Sleep(100);
                mo.MouseEvent(MouseOperations.MouseEventFlags.LeftUp);
                System.Threading.Thread.Sleep(100);

                //AutomationIdからCpu使用率を取得
                AutomationElement cpuUtil = FindElementById(mainForm, "sidebar_cpu_util");
                Console.WriteLine(cpuUtil.Current.Name);

                System.Threading.Thread.Sleep(5000);

            }
            finally
            {
                process.CloseMainWindow();
            }
        }

        // 指定したID属性に一致するAutomationElementを返します
        private static AutomationElement FindElementById(AutomationElement rootElement, string automationId)
        {
            return rootElement.FindFirst(
                 TreeScope.Element | TreeScope.Descendants,
                new PropertyCondition(AutomationElement.AutomationIdProperty, automationId));
        }

        // 指定したName属性に一致するAutomationElementをすべて返します
        private static IEnumerable<AutomationElement> FindElementsByName(AutomationElement rootElement, string name)
        {
            return rootElement.FindAll(
                TreeScope.Element | TreeScope.Descendants,
                new PropertyCondition(AutomationElement.NameProperty, name))
                .Cast<AutomationElement>();
        }

        // 指定したName属性に一致するボタン要素をすべて返します
        private static IEnumerable<AutomationElement> FindButtonsByName(AutomationElement rootElement, string name)
        {
            const string BUTTON_CLASS_NAME = "Button";
            return from x in FindElementsByName(rootElement, name)
                   where x.Current.ClassName == BUTTON_CLASS_NAME
                   select x;
        }

        //指定した要素の子要素をすべて返します。
        private static List<AutomationElement> GetChildren(AutomationElement parent)
        {
            if (parent == null)
            {
                throw new ArgumentException();
            }

            AutomationElementCollection collection = parent.FindAll(TreeScope.Children, Condition.TrueCondition);

            if (collection != null)
            {
                List<AutomationElement> result = new List<AutomationElement>();
                for (int i = 0; i < collection.Count; i++)
                {
                    result.Add(FindChildAt(parent, i));
                }

                return result;
            }
            else
            {
                return null;
            }
        }

        //指定した要素の指定インデックスの子要素を返します。
        private static AutomationElement FindChildAt(AutomationElement parent, int index)
        {
            if (index < 0)
            {
                throw new ArgumentOutOfRangeException();
            }
            TreeWalker walker = TreeWalker.ControlViewWalker;
            AutomationElement child = walker.GetFirstChild(parent);
            for (int x = 1; x <= index; x++)
            {
                child = walker.GetNextSibling(child);
                if (child == null)
                {
                    throw new ArgumentOutOfRangeException();
                }
            }
            return child;
        }

        //指定した要素をクリックします。
        private static void InvokeControl(AutomationElement targetControl)
        {
            InvokePattern invokePattern = null;

            try
            {
                invokePattern =
                    targetControl.GetCurrentPattern(InvokePattern.Pattern)
                    as InvokePattern;
            }
            catch (ElementNotEnabledException)
            {
                return;
            }
            catch (InvalidOperationException)
            {
                return;
            }

            invokePattern.Invoke();
        }


    }
}

最後に
当社では情報セキュリティに限定せず、各種情報技術に関して情報収集・技術研鑽を行っています。
実は今回のようなWindowsアプリケーションに関する構造分析は、セキュリティ技術にも応用できます。
例えば動的解析ツールであるCheatEngine、うさみみハリケーン、Ollydbgなどのアプリケーションの起動を検知する技術に利用できます。
検知した瞬間に保護対象のアプリをシャットダウンさせることで、動的解析からのシステム保護を実現できます。
このように常に一元的な視点ではなく多元的な視点を持ち、お客様のアプリケーション・サービスの安全確保に努めさせていただきます。

f:id:Ninjastars:20190118105554p:plain
解析ツールを検知

参考サイト
UIAutomationで.Net製デスクトップアプリのGUIコンポーネントの自動制御を試みるまでのハートフルストーリー - たーせる日記
UIAutomationでマウスのドラッグ&ドロップの自動制御を試みるまで - たーせる日記

注意事項
本レポートに記載されている内容を許可されていないソフトウェアで行うと、場合によっては犯罪行為となる可能性があります。そのため、記事の内容を試す際には許可されたソフトウェアに対してのみ実施するようにしてください。

本レポートについて
お問い合せ
E-mail:ichise@ninjastars-net.com

株式会社Ninjastarsエンジニア
一瀬健二郎