삽질하는플머

블럭체인 (eosio) 환경에서 랜덤값 고민

탐구생활/Others

블럭체인을 정의하는 표현은 여러가지가 있지만, "장부의 공유 시스템"이라는 말이 가장 와 닿는듯 하다. 

모든 노드는 동일한 정보를 재생하여 동일한 결과를 남기며, 이는 체인의 가상머신위에서 구동되는 스마트 컨트랙트에도 해당된다. 

A 노드에서 실행된 스마트 컨트랙트의 결과는 B 노드에서도 동일해야 한다는 이야기이다. 


eosio 는 스마트 컨트랙트로 웹어셈블리를 사용하고 있다는 점 때문에 아주아주 마음에 드는 체인이다. 

예전 글에서 가지고 놀았던 바로 그 LLVM 환경을 제공한다. 

처음에 컨트랙트 개발언어가 C++이라고 들었을때는 이뭥미~~?? 컨트랙트를 네이티브로 돌리나?? 싶었는데, WASM 이라는 말에 단번에 수긍해버렸다. 

게다가 이더와 비교불가한 트랜젝션 속도까지... 한 때 게임밥을 먹었던 사람으로서는 참 매력적인 환경이 아닐 수 없다.


게임에서 가장 중요한 것은 무엇일까. 

고전적인 "가위/바위/보" 만 떠올려도 상대방이 무엇을 낼지 모른다는 기대감, 결과를 모르기 때문에 생기는 긴장감 아닐까. 

이런 긴장감을 표현하는 핵심은?? 결국 "랜덤" 함수이다. 


문제는... 블럭체인의 컨트랙트 입장에서 "랜덤" 은 여러가지로 곤란한 의미를 가진다는 것이다. 

앞에서 언급했던 장부의 공유라는 문제를 되짚어보자. A 노드에서 생성한 랜던값은 B 노드도 동일하게 나와야 한다. 

먼 미래에 체인에 참여한 노드가 로그를 리플레이해서 램을 구성할 때도 당연히 동일한 결과가 나와야 한다. 

노드를 실행하는 물리적 컴퓨터 하나에서 돌린 std::rand() 함수의 결과가 다른 노드의 값과 동일하다고 보장할 수 있을까? 

그랬다면 세상 모든 개인키는 예측 가능하고 블럭체인도 성립할 수 없었겠지. 


그렇다면 eosio 의 컨트랙트에서는 어떨까. 

명색이 C++ 환경인데 rand(), std::rand() 가 동작하기는 할까. 


hello.cpp

#include <eosiolib/eosio.hpp>


using namespace eosio;


class hello : public eosio::contract {

  public:

      using contract::contract;


      void hi( account_name user ) {

         print( "Hello, ", name{user} );

         print(std::rand()); // test 1 !!

         print(rand());      // test 2 !!

      }

};




당연한 이야기겠지만, 링크조차 되지 않는다. 


$ eosiocpp -o hello.wast hello.cpp


hello.cpp:11:15: error: no member named 'rand' in namespace 'std'

         print(std::rand());

               ~~~~~^

hello.cpp:12:9: error: use of undeclared identifier 'rand'

         print(rand());

               ^

2 errors generated.



여기에 추가로, 채굴 기반의 블럭체인인 경우'악의를 가지는 노드의 어뷰징' 문제도 고민해야 한다. 

다행히 DPOS 체계인 eos 는 블럭 생성이 BP에게 위임되기 때문에 이 문제에서는 어느정도 안전하다고 볼 수 있다. 

BP가 악의를 가지고 VM을 오염시켜도 해당 트랜젝션이 컨펌되기 어렵고, 합의를 망가뜨리는 이런 행위는 헌법과 투표로 제재받을 수 있다. 



랜덤에 대한 비슷한 고민을 다른 dApp 들은 어떻게 풀었을까. 

EOS 기반의 복권인 https://eosshishicai.com/ 에서는 특정 블럭의 해시값을 모아서 당첨번호로 사용한다. 즉 당첨자 선정이 컨트랙트 외부에서 일어난다.

요즘 핫한 EOS Knights 의 컨트랙트에는 거래 및 캐릭터 상태 외에 전투에 대한 처리는 보이지 않는다. 독립 서버가 담당하는 것으로 판단된다. 


좋은 해결방법이지만 이 글의 고민과는 맞지 않는다. 

컨트랙트의 액션 실행 시점에서 모든 노드에서 동일한 값을 가지는 랜덤함수를 고민해 보도록 하자. 



게임의 핵심이 랜덤이라면, 랜덤의 핵심은 "시드값"이다. 

컨트랙트가 참조할만한 체인 내 정보들은 어떤것이 있는지 살펴보자. 



1. now(). 

https://developers.eos.io/eosio-cpp/reference#now

1970년부터 잰 초단위 시간, 즉 UNIX TIMESTAMP 이다. 블럭이 실행되는 시점이므로 모든 노드가 일치한 값을 가진다. 


2. get_active_producers()

https://developers.eos.io/eosio-cpp/reference#get_active_producers

실행시점의 블럭 프로듀서 배열이다. BP간 경쟁이 커지면 자주 바뀌겠지만 현재는 큰 변화가 없다. 


3. tapos_block_num(), tapos_block_prefix()

https://developers.eos.io/eosio-cpp/reference#tapos_block_num

현재 실행중인 블럭의 번호와 프리픽스. 입맛당기는 값이지만, 분리된 트랜젝션에서도 동일값을 리턴하는 문제가 있다. 

헤드블럭이 아닌 비가역블럭의 값으로 판단된다.
블럭 아이디의 첫 2바이트는 실제 블럭번호를 2바이트로 캐스팅한 값이고, 뒤따라오는 4바이트는 블럭 프리픽스의 역순 배열이다.  

블럭번호값이 65535마다 오버플로한다는 것에 유의해야 할 것이다. 



4. 시스템 컨트랙트의 테이블.

global 의 "total_ram_stake", "total_activated_stake" 등과 rammarket 의 RAMCORE 값. 

큰 변화는 없지만 예측이 어려우므로 시드의 보조값으로 버무리기에 쓸만하다. 



일단 1과 3을 이용해 다음과 같은 코드를 만들어 보았다. 랜덤 알고리즘은 나무위키에서 선형 합동법 코드를 줏어다 사용했다.


...

 #include <eosiolib/transaction.hpp>

...


static uint32_t next = 1;


int32_t eos_rand(void)

{

    next = next * 1103515245 + 12345;

    return (uint32_t)(next>>16) & 0x7fff;

}


void eos_srand(uint32_t seed)

{

    next = seed;

}


......


// hi 액션을 수정. 랜덤값 출력. 


  auto seedValue = tapos_block_prefix() * tapos_block_num();

  eos_srand((uint32_t) (seedValue * now()));


  print( eos_rand(), " ", seedValue, " ", now() );




정글넷의 "test.oranke" 계정에 컨트랙트를 올리고 테스트. 


$ cleos push action test.oranke hi '["oranke"]' -p oranke

...

#  test.oranke  <= test.oranke::hi             {"user":"oranke"}

>> 23137 244695216 1535182824


$ cleos push action test.oranke hi '["oranke"]' -p oranke

...

#  test.oranke  <= test.oranke::hi             {"user":"oranke"}

>> 13367 851558428 1535182834


$ cleos push action test.oranke hi '["oranke"]' -p oranke

...

#  test.oranke  <= test.oranke::hi             {"user":"oranke"}

>> 10456 968781540 1535182838


$ cleos push action test.oranke hi '["oranke"]' -p oranke

...

#  test.oranke  <= test.oranke::hi             {"user":"oranke"}

>> 27294 968781540 1535182839


$ cleos push action test.oranke hi '["oranke"]' -p oranke

...

#  test.oranke  <= test.oranke::hi             {"user":"oranke"}

>> 12273 968781540 1535182842



액션 실행시간이 1초 이상 떨어져도 tapos_block_prefix() * tapos_block_num() 값이 동일하게 나오는 경우를 볼 수 있다. (붉은색)

now()값을 더해 시드값에 더해 그럭저럭 쓸만한 결과를 얻었다. 컨트랙트의 RAM에 이전 시드값을 저장하는 방식으로 보완한다면 좀 더 유용할 것이다. 

RAM, 즉 테이블까지 진도 나가기엔 체력이 딸리니... 이 글은 여기까지.


모쪼록 eos 로 즐길만한 게임들이 많이 출시되었으면 한다. 


우분투 10.04 + phpLiteAdmin 에서 SQLCipher 사용.

탐구생활/Others

phpLiteAdmin 은  sqlite 의 웹 어드민 툴로서 phpMyAdmin 에 익숙하다면 눈이 번쩍 뜨일만한 물건이다. 

이름부터 비슷하다. 사용법도 아주 유사하다. 

게다가 단일 php파일 하나로 되어있어 별다른 설치 없이 웹서버에 올리고 디렉토리에 쓰기 권한만 설정하면 동작한다. 


SQLCipher sqlite3 위에 OpenSSL 기반의 암호화를 지원하는 라이브러리이다. 

PRAGMA key = 'password' 명령을 실행한 뒤 이후 입력되는 테이블, 필드 정보는 모두 인코딩되어 저장된다. 

설정된 암호의 변경은 PRAGMA rekey = 'newpassword' 명령으로 처리할 수 있다. 


문제는, 기존에 만들어진 암호화되지 않은 DB파일에 암호를 설정하는 기능이 없다는 것. 

때문에 phpLiteAdmin 같은 sqlite 어드민 툴에서 작업한 내용을 적용하려면, 적절한 형식으로 출력한 뒤 다시 암호화된 DB파일에 밀어넣어주어야 한다. 적어도 기본 암호라도 설정할 수 있게 해주면 이후 이 파일의 키값만 새로 지정하는 방식으로 작업할 수 있을텐데... 


뭐 DB설계에 경험이 많다면야 이런 도구 없이도 좋은 스키마를 쭉쭉 뽑아내겠지만, 눈으로 봐야만 뭔가 만들 수 있는 나같은 놈에겐 다른 나라 이야기. 


안되면 되게 한다. 삽질은 그래서 의미를 가지지. 



먼저 우분투의 php5에서 sqlite 를 사용할 수 있게 하자. 


# sudo apt-get install php5-sqlite phpt-sqlite3



phpinfo()로 찍어보면 pdo_mysql, pdo_sqlite 가 설치되며 sqlite3 는 따로 설정되어있는 것을 볼 수 있다. 


PDO

PDO supportenabled
PDO driversmysql, sqlite, sqlite2

pdo_mysql

PDO Driver for MySQLenabled
Client API version5.1.61

pdo_sqlite

PDO Driver for SQLite 3.xenabled
SQLite Library3.7.10

........

SQLite

SQLite supportenabled
PECL Module version2.0-dev $Id: sqlite.c 293036 2010-01-03 09:23:27Z sebastian $
SQLite Library2.8.17
SQLite EncodingUTF-8

DirectiveLocal ValueMaster Value
sqlite.assoc_case00

sqlite3

SQLite3 supportenabled
SQLite3 module version0.7
SQLite Library3.7.10

DirectiveLocal ValueMaster Value
sqlite3.extension_dirno valueno value



phpLiteAdmin은 PDO, SQLite3, SQLiteDatabase 세종류의 모듈을 지원한다. 

이 드라이버의 설정은 phpLiteAdmin.php 의 130번째 줄 근처의 


define("FORCETYPE", false);


이 부분을 다음과 같이 수정해 강제로 설정할 수 있다. 


define("FORCETYPE", "PDO");


또는 


define("FORCETYPE", "SQLite3");



그런데 무슨 이유에서인지 이 부분을 SQLite3 로 설정하면 제대로 동작하지 않는다. 

PDO 모듈을 써서 생성된 파일을 메모장에서 열어보면 다음과 같다. 




앞부분에 보이듯 sqlite3 형식인 것으로 보아 PDO의 sqlite 가 sqlite3 를 기본으로 하는 것 같은데... 

좀 더 확실히 하기 위해 ldd로 pdo_sqlite.so 의 dependency 를 살펴보자. 


# cd /usr/lib/php5/20090626+lfs

# ldd pdo_sqlite.so

        linux-gate.so.1 =>  (0x00bfe000)

        librt.so.1 => /lib/tls/i686/cmov/librt.so.1 (0x007d5000)

        libsqlite3.so.0 => /usr/lib/libsqlite3.so.0 (0x00a98000)

        libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0x002f4000)

        libpthread.so.0 => /lib/tls/i686/cmov/libpthread.so.0 (0x0045e000)

        /lib/ld-linux.so.2 (0x00e93000)

        libcrypto.so.0.9.8 => /lib/i686/cmov/libcrypto.so.0.9.8 (0x00110000)

        libdl.so.2 => /lib/tls/i686/cmov/libdl.so.2 (0x00eb7000)

        libz.so.1 => /lib/libz.so.1 (0x006ca000)



예상대로 libsqlite3.so 를 참조하고 있다. 

phpLiteAdmin이 SQLite3 모듈로 잘 동작했다면 sqlcipher 를 기반으로 php5의 모듈을 재빌드해 올리기만 하면 될텐데 싶지만... 

그래도 그나마 다행이다. 


일단 sqlcipher 를 빌드하자. 


# sudo apt-get install tcl tcl-dev libssl libssl-dev

# git clone git://github.com/sqlcipher/sqlcipher.git

# cd sqlcipher

# ./configure --enable-tempstore=yes CFLAGS="-DSQLITE_HAS_CODEC" LDFLAGS="-lcrypto"

# make

# sudo make install



윈도에서 빌드는 개삽질인데, 리눅스에서는 TCL만 깔아주면 한방에 끝나네~~
이제 pdo_sqlite.so 가 참조하는 라이브러리를 /usr/local/lib 에 생성된 libsqlite3.so.0.8.6 로 바꿔주면 되는데, 

어차피 sqlcipher는 sqlite3의 모든 기능을 가지고 있고 테스트머신에 이런저런 신경쓰기도 귀찮아서 그냥 /usr/lib 에 복사해 버렸다. 


# sudo cp /usr/local/lib/libsqlite3.so.0.8.6 /usr/lib/



내친김에 기존의 sqlite3 모듈도 새로 만들어 넣어두자. sqlcipher 홈페이지에 잘 설명되어있으니 그대로 따라하면 된다. 

다만 php-5.4.0/ext/sqlite3 소스에 config.m4 파일이 config0.m4 로 되어있으니 phpize 전에 이 이름을 바꿔주는 것만 신경쓰자. 

이렇게 만들어진 sqlite3.so 를 apt-get 으로 설치한 /usr/lib/php5/20090626+lfs/sqlite3.so 와 교체해주고 아파치를 재구동하면 완료. 




남은 문제는 phpLiteAdmin에서 언제, 어떻게 기본 암호를 설정해주느냐 하는 것. 

여기저기 살펴보니 660줄 근처에 Database class 에 정의된 쿼리에 대한 래퍼함수가 눈에 띈다. 이 함수 도입부분에 다음 코드를 추가하자. 


        ........

//generic query wrapper

public function query($query, $ignoreAlterCase=false)

{

$this->db->query("PRAGMA key = '1234';");

if (strtolower(substr(ltrim($query),0,5))=='alter' && $ignoreAlterCase==false) // ......

        ........



이제 수정된 phpLiteAdmin을 우분투의 아파치에서 접근할 수 있게 해주고 DB파일을 만들어보자. 


 



생성된 DB파일을 열어보면 알아볼 수 없도록 암호화되어 저장된 것을 알 수 있다. 




스키마 설계 및 기본정보 입력이 마무리된 파일은 나중에 "PRAGMA rekey" 명령으로 새 암호를 할당해 배포하면 됨. 



phpliteadmin.7z


MinGW에서 윈32용 SQL CIPHER 빌드 (작성중)

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

sqlite 암호화. SQL CIPHER

탐구생활/Others
sqlite에 암호화 기능이 추가된 SQL CIPHER 라는 물건이 있다. 
http://sqlcipher.net/

암호화에 OpenSSL을 사용해 DB내용을 완전히 인코딩하는 멋진 녀석인데... 
이게 윈도에서 빌드하기가 까다롭다고 악명이 높다. 
http://groups.google.com/group/sqlcipher/browse_thread/thread/55c6296b56bf4533

실제로 제공되는 소스에는 iOS, 리눅스, 심지어는 윈도 CE용 Make파일도 있는데
윈도용은 쏙 빠져있네. 

오래간만에 들러봤더니 아예 윈도용 바이너리를 빌드해서 판매하고 있군. 
개발자 한명당 $150이라 편의성을 생각하면 비싼편은 아니지만
치매 예방겸 한번쯤 도전해 볼 개연성은 충분할 듯~~


shared dll 빌드로 이미 산을 하나 퍼낸 하모씨의 삽을 빌려다 뚝딱뚝딱 static dll 빌드 성공.
기념 스샷 한 방~


원격 커멘드 - rcmd

탐구생활/Others
원격 컴퓨터의 콘솔 프로그램을 실행하고 그 결과를 내 컴퓨터에 뿌려줘야 한다면? 바로 떠오르는 해법은 SSHD 를 설치해 사용하는 것이다. 하지만 로컬 네트웍에서 간단한 작업을 위해 cygwin 깔고 SSHD 설정하고 키를 교환한다는 건... 왠지 소잡는 칼을 생선 다듬는 데 휘둘러대는 폼안나는 일이라 생각된다. 

해서 간단한 유틸리티를 만들어볼까 폼잡다가 윈도 2000 리소스킷에서 멋진 솔루션을 발견했다. 이름하여 rcmd / rcmdsvc !! 
오늘은 이 물건을 가지고 재미나게 놀아보자!! (검색 안해보고 만들었으면 속 좀 쓰릴 뻔...)

다운로드는 여기서 "Remote command service" 를 클릭하면 rcmd.zip 을 얻을 수 있다. 압축을 풀면 rcmd.exe 와 rcmdsvc.exe 가 나온다. 

(나중에 찾아가기 귀찮으니 걍 여기에 붙여두자)

1. rcmdsvc.exe 를 서비스로 등록. 

"Service Controller Query Tool" 이라 불리는 sc.exe 는 예전에는 리소스킷으로 제공되었었는데 XP에는 이미 포함되어있다. 
압축이 풀린 rcmd.exe 와 rcmdsvc.exe 를 윈도우의 System32 폴더로 옮기고 커맨드창에서 다음과 같이 입력하자. 

C:\>sc create rcmdsvc binPath= C:\WINDOWS\system32\Rcmdsvc.exe displayname= "Remote command service" start= auto

[SC] CreateService SUCCESS


제어판의 서비스 항목에 방금 설치한 Remote command service 가 제대로 들어갔는지 살펴보자. 




여기서 "시작" 버튼을 살포시 눌러주거나, net start rcmdsvc 또는 sc start rcmdsvc 로 서비스를 시작한다. 
(등록된 서비스를 삭제하는 명령은 sc delete rcmdsvc 이다.)


2. 로컬에서 테스트.

이제 준비된 서버에 접속해보자. rcmd.exe 를 과감하게 실행시킨다. 




서버의 이름을 입력하라는 안내가  나온다. 로컬 서버의 IP인 127.0.0.1 을 입력하면 서비스가 실행된 계정인 LocalService 의 홈디렉토리를 기준으로 셸이 열리게 된다. 

실행 후 서버의 CMD를 띄우는 방식 외에 다음과 같이 직접 명령어를 입력하고 결과를 받아올 수도 있다. 
(도메인 이름 앞에 역슬래시 두개가 붙는 것에 유의하자.)

C:\> rcmd \\127.0.0.1 dir c:\ /w


실행 결과는 다음과 같다. 




3. 원격지에서 접속.

로컬에서만 띄우면 이게 뭐지 싶을 것이다. 이제 원래 목적이었던 원격지에서의 접속을 테스트 해 보자. 
이 글에서 rcmdsvc 서비스를 동작시키는 컴퓨터의 내부 네트웍 IP 는 192.168.29.1 이다. 다른 컴퓨터에서 이 컴퓨터의 dir 명령을 내려보자. 

C:\> rcmd \\192.168.29.1 dir c:\ /w

Error - Failed to connect to <\\192.168.29.1>, Error = 1326


1326 에러는 대상 컴퓨터의 IPC 권한을 갖지 못했기 때문에 발생한다. 네트워크 공유 연결과 동일한 방식으로 권한을 획득해주자. 

C:\> set user=[사용자명]
C:\> set passwd=[비밀번호]]

C:\> net use \\192.168.29.1\IPC$ %PASSWD% /user:%USER%
명령을 잘 실행했습니다.


이제 앞에서 내렸던 rcmd 명령을 다시 실행하면 원격지의 C 드라이브 내용을 볼 수 있게 된다. 

확보한 IPC 권한은 다음과 같은 방법으로 반환할 수 있다. 

C:\>net use \\192.168.29.1\IPC$ /delete
\\192.168.29.1\IPC$이(가) 제거되었습니다.


콘솔창에서 입력이 아닌 델파이에서 대상지의 IPC 권한을 확보하는 방법은 예전에 델마당에 올렸던 강좌를 참고하자.


// 도메인에 연결을 만든다.
function UseConnection(const aDomain, aUserID, aPassWord: String; var ConnStr: String): Boolean;
var
  NetResource: TNetResource;
  ConnStrLen: DWORD;
  RetFlag: DWORD;
  RetValue: DWORD;
begin
  FillChar(NetResource, SizeOf(TNetResource), 0);

  with NetResource do
  begin
    dwType := RESOURCETYPE_ANY;
    lpLocalName := nil;   // 로컬 드라이브 지정하지 않음
    lpRemoteName := PChar('\\' + aDomain);
    lpProvider := nil;
  end;

  SetLength(ConnStr, MAX_PATH);
  ConnStrLen := MAX_PATH;
  RetValue:=
    WNetUseConnection(
      0, NetResource,
      PChar(aPassWord), PChar(aUserID), CONNECT_INTERACTIVE,
      PChar(ConnStr), ConnStrLen, RetFlag
    );
  SetLength(ConnStr, StrLen(PChar(ConnStr)));

  ConnStr := '\\'+aDomain;

  Result := RetValue = 0;
end;


var
  ConnStr: String;
...
  UseConnection('192.168.29.1\IPC$', 유저이름, 패스워드, ConnStr) 


이 호출이 True면 권한 확보가 된 상태이므로 rcmd 를 사용할 수 있게 된다. 다 사용한 권한은 WNetCancelConnection() API 로 반환한다. 

WNetCancelConnection(PChar(ConnStr), true)


응용범위는 사실 무궁무진한데... 예를 들어 여러대의 PC에서 메인 PC의 스케줄에 따라 rsync 로 동기화해야 할 경우도 이 방법을 사용하면 중앙의 한 대의 PC에서 간단히 해결할 수 있다. 또한 Screen 과 버무리면 지금 서비스중인 게임에서 SSH 를 걷어내고 설정을 간단히하는 용도로 사용할 수도 있을 것이다. 

쓰기 편한 방법은 항상 보안위험에 노출되어있다. 하지만 SSHD 를 띄우고 키를 교환해두는 것도 불편함을 해결하기 위해 보안은 나몰라라 하는 것은 마찬가지이고, 또한 IPC가 로컬네트웍에서만 유효하다는 것을 감안하면 생선 비늘 벗기는 용도로는 딱 알맞은 칼이라 생각된다. 




한가지 http://www.simpleisbest.net/archive/2005/10/18/260.aspx 이 사이트의 내용을 보면 "HKEY_CURRENT_USER 레지스트리에 접근하면 데스크탑에 로그온한 사용자의 레지스트리가 아닌 로컬 서비스 계정의 레지스트리가 액세스되어 버릴 것이다" 라는 무시무시한 구절이 있다. 흠.. 이건 좀 곤란한데... 진짜로 그런지 확인해보자. 

일단 로컬시스템에서 적당한 레지스트리를 익스포트 하자. (뭐하는데 쓰는지는 잘 모르겠지만 값이 달랑 하나라 선택)

regedit /E "C:\test.reg" HKEY_CURRENT_USER\Calendar


출력된 C:\test.reg 파일의 내용은 다음과 같다. 

Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\Calendar]
"Skin"="1"


이제 이 Skin 값을 뭐 한 100 으로 바꿔준 뒤, 원격지에서 임포트 할 때 제대로 들어가는지 확인해 보자.

rcmd \\192.168.29.1 regedit /S C:\test.reg


로컬로 돌아와 레지스트리 편집기를 열어서 확인해본다. 




흠... 제대로 잘 들어가는뎅... 





당연한 얘기지만 서비스를 Interactive Service 로 설정하지 않으면, 즉 데스크탑과 상호연동을 시키지 않으면 윈도를 띄우거나 하는 일은 불가능. 즉 rcmd 로 원격지의 "윈도 어플" 을 실행시키면 이 윈도는 LocalService 계정에서 실행되고 나타나지 않는다.  

심심해서 rcmdsvc 의 로그온 설정의 "서비스와 데스크톱 상호 작용 허용" 을 체크하고 재구동한 뒤 원격지에서 로컬컴퓨터의 맵서버를 구동시켜 봄. 원격지에서 조작윈도나 루아윈도를 띄우는 명령을 내리면 이 창은 모두 로컬에서 나타난다.




오래전 GPG 글에서 rcmd 관련된 언급을 발견하고 반가워서 트랙백을 걸어둠.