🔑스크립트 엔진/루아(Lua) 스크립트 엔진

동적 스크립트 컴파일 (Dynamically Embedded lua script in exe) | 루아(Lua) 스크립트 엔진

Mawile 2021. 11. 30.
728x90

🎄 동적 스크립트 컴파일 (Dynamically Embedded lua script in exe)

안녕하세요!!

엄청난 내용이 왔습니다.

이것을 응용하면 exe를 합칠수도있고, 여러가지로 응용할 수 도있습니다.

 

이번 포스팅에서는 우리가 C++에서 Lua스크립트를 컴파일할때, 동적으로 C++에 임베딩된 루아스크립트 리소스의 내용을 변환시켜서 컴파일해보겠습니다.

 

그냥 쉽게 말해서 exe를 변환시켜서 내부적으로 포함되어있는 lua스크립트의 내용을 이미 컴파일되어있는 상태에서 동적으로 수정할 수있다는 소리입니다!

그럼 바로 시작하겠습니다!

 

썸네일(Thumbnail)

 

 

🎄 솔루션및 환경세팅

우선 C/C++ 버전은 다음과 같이 맞추었으며, 보시다시피 솔루션은 2개입니다.

"LuaScriptEngine1"은 실제 저희가 실행할 동적 lua실행프로그램이고,

"ScriptInterpreter"는 우리가 위에서 사용할 동적 lua실행프로그램에 포함된(embedded) 루아스크립트를 변환해줄 변환기입니다.

 

그리고 아시죠?? Lua를 사용하기전에는 Lua전용 c++라이브러리를 참조해야한다는것!!

 

이제 본격적으로 개발을 시작해봅시다!!

 

 

🎄 리소스 만들기

우선 Lua스크립트를 동적으로 exe에 포함시키려면(Embedded) 리소스를 만들어야되겠죠?

LuaScriptEngine1쪽에 "리소스 파일을 우클릭" -> "추가(D)" -> "리소스(R)" 을 순서대로 눌러줍니다.

 

그러면 밑의 사진처럼 생성할 리소스의 종류를 골라야하는데, 저희는 일반 바이너리파일을 생성할것이기때문에

"사용자지정(C)"을 클릭해준후, 이름을 "Text"로 하겠습니다!

 

 

이제 바이너리파일의 리소스생성은 끝났습니다.

추가로 실수로 몇개 건들여서 리소스파일이 하나 크래시걸려가지고, 새로 만들었습니다.

그냥 보기좋게 오른쪽의 ascii칸에다가 "hello world" 라고 입력한후,

왼쪽의 16진수칸에다가 00을 추가했습니다.

char의 끝은 언제나 '\0'이여야하기 때문이죠.

 

이제 소스코드작업을 시작하겠습니다!

 

 

🎄 소스코드

우선 전체 솔루션의 내용입니다.

혹시 빠뜨린부분이 있으신지 확인해주세요

프로젝트 LuaScriptEngine1 ScriptInterpreter 모두(공통)
소스파일 SCMain.cpp ITPMain.cpp 없음
헤더파일 ScriptEmbedding.h ITPHeader.h 없음
리소스 헤더파일 resource.h 없음 없음
리소스 text2.bin 없음 없음
Debug x86 x86 x86
루아스크립트 sample.lua sample.lua sample.lua

 

우선 리소스파일의 내용을 처음 수정하겠습니다.

text2.bin

text2.bin이라는 바이너리파일은 아시다시피 루아스크립트의 내용을 넣을리소스공간입니다.

해당 루아스크립트의 내용은 "0~3.txt라는 이름의 텍스트파일에 Hello world라고 적어라." 라고 명령하는 내용입니다.

for i = 0, 3 do
    path = string.format("%s.txt", i)
    file = io.open(path, "a")
    file:write("Hello world")
    file:close()
end

 

 

sample.lua

다음은 해당 리소스의 내용을 변경할 루아스크립트의 내용입니다.

이제 이 루아스크립트의 내용이 LuaScriptEngine1.exe의 리소스에 덮어씌워질겁니다.

for i = 0, 3 do
    path = string.format("%s.txt", i)
    file = io.open(path, "a")
    file:write("Editted !")
    file:close()
end

 

 

ScriptEmbedding.h

이 헤더파일은 SCMain.cpp 소스파일의 내용을 보조합니다.

우선 Lua5.1을 사용하기위해 pragma comment로 라이브러리참조를 명시합니다.

그다음 Lua관련 헤더파일을 포함하고, 우리가 앞서 만들었던 리소스로 인해 생겨진 "resource.h" 헤더파일을 포함시킵니다.

 

그리고 PointerStruct라는 구조체를 만드는데, 이 구조체는 리소스에 담겨진 바이너리파일의 포인터와 크기, 포인터를 성공적으로 받아왔는지에 대한 여부를 묻는 변수가 포함되어 있습니다.

#pragma once
#pragma comment(lib, "lua51")

extern "C" {
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
}

#include <Windows.h>
#include <conio.h>

#include <iostream>

#include "resource.h"

#define MAWILE_FAILED(n) (n == 0)
#define BREAK int _ = _getch()

struct PointerStruct {
	char* ptr;
	DWORD size;
	bool result;
};

void ErrorMessage(PointerStruct* ptrStruct, const wchar_t* ptr) {
	std::wcout << ptr << std::endl;
	ptrStruct->result = false;
	BREAK;
}

 

이제 자신의 바이너리 리소스파일을 불러오는 함수를 정의합시다.

이 함수는 말그대로 "text2.bin"이라는 리소스파일의 내용을 포인터에 넣어서 반환시킵니다.

이때, FindResourceA함수의 두번째파라미터는 유의해주세요.

왜냐하면, 두번째파라미터는 어떤 리소스를 찾을지에 대해 묻는것에 저랑 무조건 똑같이 하시면 안되구요,

"resource.h" 헤더파일에서 자신에게 주어진 알맞은 이름의 리소스매크로를 넣어줘야합니다.

PointerStruct LoadBinaryResource() {
	PointerStruct ptrStruct;
	HINSTANCE hInstance;
	HRSRC hrSrc;
	HGLOBAL hGlobal;
	LPVOID lpLockPtr;
	DWORD lpLockPtrSize;

	ptrStruct.result = true;

	hInstance = GetModuleHandleA(0);
	if (MAWILE_FAILED(hInstance)) {
		ErrorMessage(&ptrStruct, L"Failed to get the module handle.");
		return {};
	}

	hrSrc = FindResourceA(hInstance, MAKEINTRESOURCEA(IDR_TEXT2), "TEXT");
	if (MAWILE_FAILED(hrSrc)) {
		ErrorMessage(&ptrStruct, L"Failed to find resource.");
		return {};
	}

	hGlobal = LoadResource(hInstance, hrSrc);
	if (MAWILE_FAILED(hGlobal)) {
		ErrorMessage(&ptrStruct, L"Failed to load resource.");
		return {};
	}

	lpLockPtr = LockResource(hGlobal);
	if (MAWILE_FAILED(lpLockPtr)) {
		ErrorMessage(&ptrStruct, L"Failed to lock resource.");
		return {};
	}

	lpLockPtrSize = SizeofResource(hInstance, hrSrc);
	if (MAWILE_FAILED(lpLockPtrSize)) {
		ErrorMessage(&ptrStruct, L"Failed to get a size of locked pointer.");
		return {};
	}

	ptrStruct.ptr = (char*)lpLockPtr;
	ptrStruct.size = lpLockPtrSize;

	return ptrStruct;
}

 

 

SCMain.cpp

어려운 부분은 앞의 헤더파일에서 다 넣어놨기때문에, 이 부분은 어려운부분이 딱히 존재하지 않습니다.

그냥 LoadBinaryResource로부터오는 버퍼의 메모리주소를 기억하고, 메모리주소의 내용을 luaL_dostring을 통해 루아스크립트를 실행해주면 됩니다.

#include "ScriptEmbbeding.h"

int main(int argc, char** argv) {
	lua_State* lState;
	PointerStruct ptrStruct;
	char* binPtr;
	int result;

	ptrStruct = LoadBinaryResource();
	if (ptrStruct.result == false) {
		return -1;
	}

	binPtr = new char[ptrStruct.size + 1];
	memset(binPtr, 0x00, ptrStruct.size + 1);
	memcpy(binPtr, ptrStruct.ptr, ptrStruct.size);

	std::cout << "[ Embedding Stript ]\n" << binPtr << "\n\n";

	lState = luaL_newstate();
	luaL_openlibs(lState);

	result = luaL_dostring(lState, binPtr);
	if (result) {
		std::cout << lua_tostring(lState, -1) << std::endl;
	}

	delete[] binPtr;
	BREAK;

	return 0;
}

 

 

ITPHeader.h

이 헤더파일은 ITPMain.cpp 소스파일의 내용을 보조합니다.

이 헤더파일에서는 별로 크게 이야기할 요소는 없는것같습니다.

GetBufferFromFile함수는 첫번째파라미터로 받은 파일의 이름을 통해 해당 파일을 열고,

그 파일의 이름과 내용이 담긴 포인터를 반환합니다.

#pragma once

#include <Windows.h>
#include <conio.h>

#include <filesystem>
#include <iostream>
#include <fstream>
#include <string>
#include <tuple>

#define BREAK int _ = _getch()

void ErrorMessage(const wchar_t* ptr) {
	std::wcout << ptr << std::endl;
	BREAK;
	exit(EXIT_FAILURE);
}

std::tuple<char*, int> GetBufferFromFile(const std::string& filename) {
	std::ifstream ifs;
	char* ptr;
	int filesize;

	filesize = std::filesystem::file_size(filename);
	ifs.open(filename, std::ios::binary);
	ptr = nullptr;

	if (ifs.is_open()) {
		ptr = new char[filesize + 1];
		memset(ptr, 0x00, sizeof(char) * (filesize + 1));
		ifs.read(ptr, filesize);

		ifs.close();
	}

	return std::make_tuple(ptr, filesize);
}

 

 

ITPMain.cpp

이 소스파일은 "LuaScriptEngine1.exe"이라는 실행파일안에 포함된 리소스를 동적으로 변환시킵니다.

이제 "DMScriptName"에다가 "LuaScriptEngine1.exe"를 입력해주시고,

이제 "LuaScriptName"에다가 "sample.lua"를 입력해주시면 성공적으로 리소스의 내용이 바뀔것입니다.

 

여기서 주의할점이있는데, UpdateResourceA의 세번째파라미터는 "LuaScriptEngine1"쪽 솔루션에 저장된 "resource.h"에 정의된 우리가 저장한 리소스매크로를 의미해야합니다.

 

저의 리소스 매크로는 102이기때문에, MAKEINTRESOURCEA(102)로 넣어줬습니다.

IDR_TEXT1은 처음 만들었다가 크래시떠서 그냥 놔두고 IDR_TEXT2로 하고있습니다.

#include "ITPHeader.h"

int main(int argc, char** argv) {
	HANDLE buResourceHandle;
	BOOL lpResult;
	std::string DMScriptName, LuaScriptName;

	std::cout << "동적으로 스크립팅을 진행 할 exe파일의 이름을 입력하세요: ";
	std::cin >> DMScriptName;

	std::cout << "스크립팅될 루아스크립트의 이름을 입력하세요: ";
	std::cin >> LuaScriptName;

	buResourceHandle = BeginUpdateResourceA(DMScriptName.c_str(), FALSE);
	if (buResourceHandle == NULL) {
		ErrorMessage(L"Failed to BeginUpdateResource");
	}

	auto[lpBuffer, lpBufferSize] = GetBufferFromFile(LuaScriptName);
	if (lpBuffer == nullptr) {
		ErrorMessage(L"Failed to getBufferFromFile");
	}

	std::cout << "[ Embedding Stript ]\n" << lpBuffer << "\n\n";

	lpResult = UpdateResourceA(buResourceHandle, "TEXT", MAKEINTRESOURCEA(102),
		MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL), (LPVOID)lpBuffer, lpBufferSize);
	if (lpResult == FALSE) {
		ErrorMessage(L"Failed to UpdateResource");
	}

	lpResult = EndUpdateResourceA(buResourceHandle, FALSE);
	if (lpResult == FALSE) {
		ErrorMessage(L"Failed to EndUpdateResource");
	}

	delete[] lpBuffer;
	BREAK;

	return 0;
}

 

 

 

이제 모두 빌드해주면 다음과 같이 파일이 정렬되어 있으면 좋습니다.

 

먼저 LuaScriptEngine1.exe를 실행시켜보겠습니다.

그러면 다음과같이 리소스의 내용과 함께, 리소스의 내용에 따라 루아스크립트가 실행됩니다.

 

그리고 0.txt를 열어보면 이런식으로 리소스의 내용과 같이 파일이 만들어집니다.

 

이제 ScriptInterpreter.exe를 실행시켜서 LuaScriptEngine1.exe의 리소스내용을

sample.lua의 내용과 일치하게 변환시켜보겠습니다.

 

변환이 완료되면, 다음과 같이 읽어드린 소스파일의 내용이 출력됨과 동시에 리소스덮어씌우기가 완료됩니다.

이제 다시 ScriptEngine1.exe를 실행시켜보면 텍스트파일의 내용이 변화된것을 육안으로 확인할 수가있습니다.

당연히 텍스트파일의 내용이 변했다는것은 ScriptEngine1.exe가 가지고있는 리소스의 내용이 성공적으로 바뀌었다는것이겠죠?

 

 

🎄 마치며...

우리가 앞서서 배웠던 리소스를 변환시키는 기술은 여러가지로 응용되서 사용될 수 있습니다.

예를들어서 여러개의 EXE파일을 하나의 EXE파일로 합쳐서 백도어처럼 실행시키거나,

다음과 같이 스크립트언어와 함께병행사용하여 좀더 확장성있게 접근할 수 있습니다.

아니면은 동적 컴파일을 구현할 수있고, 동적 컴파일을 통해 서버파일(해킹관련이야기)을 생성할 수있습니다.

 

사실 저가 처음으로 리소스를 건들여보면서 행복회로가 엄청돌아갔습니닼ㅋㅋㅋ

궁금한부분이 있으시면 망설이지마시고 댓글로 질문주세요!

 


728x90

댓글