logo

English

이곳의 프로그래밍관련 정보와 소스는 마음대로 활용하셔도 좋습니다. 다만 쓰시기 전에 통보 정도는 해주시는 것이 예의 일것 같습니다. 질문이나 오류 수정은 siseong@gmail.com 으로 주세요. 감사합니다.

Customizing GINA, Part 2

by digipine posted Oct 28, 2017
?

Shortcut

PrevPrev Article

NextNext Article

Larger Font Smaller Font Up Down Go comment Print
?

Shortcut

PrevPrev Article

NextNext Article

Larger Font Smaller Font Up Down Go comment Print
Security Briefs
Customizing GINA, Part 2
Keith Brown


Code download available at: SecurityBriefs0506.exe (274 KB)


 
GINA, the Graphical Identification and Authentication component, is a part of WinLogon that you can customize or replace. Last month I introduced GINA customization; this month, I'm going to drill down to implement each of the GINA entry points. If you have not read last month's Security Briefs column, I strongly suggest that you start there before diving into this one (see Security Briefs: Customizing GINA, Part 1).

 

WlxNegotiate and WlxInitialize
I covered the two simple functions WlxNegotiate and WlxInitialize in last month's column. WlxNegotiate and WlxInitialize allow you to negotiate versions with WinLogon and exchange context handles for state management. WinLogon gives GINA a handle (hWlx), and GINA gives WinLogon a pointer to its internal state (pWlxContext). My implementation uses a class called Gina to hold this state, which dispatches each exported GINA function to member functions on the Gina class. I instantiate an instance of the Gina class during the second of these functions, WlxInitialize, so both Negotiate and Initialize are implemented as static methods on the Gina class.
My sample GINA uses Ctrl+Alt+Del secure attention sequence (SAS) events just like the default GINA does. If you need this functionality, in WlxInitialize you should call WlxSetOption and set the WLX_OPTION_USE_CTRL_ALT_DEL option to TRUE. If you forget to do this, you will not get any SAS events when the user presses Ctrl+Alt+Del.

 

WlxDisplaySASNotice
The next function normally called by WinLogon is WlxDisplaySASNotice, and as with all the rest of the functions you'll see, the export from my DLL simply takes the GINA context (pWlxContext), casts it to my Gina class, and calls a corresponding method, as shown here:
 
VOID WINAPI WlxDisplaySASNotice(PVOID pWlxContext) {
LDB(L"-->WlxDisplaySASNotice");
((Gina*)pWlxContext)->DisplaySASNotice();
LDB(L"<--WlxDisplaySASNotice");
}
 
Note the logging macros before and after the call. This is how each of my exported functions is implemented: I log that the function was called, I dispatch the call to the Gina class, then I log that I'm returning. LDB (which stands for Log Debug) compiles to NULL for release builds. In debug builds, this generates a line of output in a log file on the local hard disk. Since debugging a deployed GINA is tricky, I find that a liberal sprinkling of debug log messages coupled with the debug entrypoint I discussed last month can help you avoid the need for a symbolic debugger entirely during development.
Now let's look at the implementation of Gina::DisplaySASNotice:
 
void Gina::DisplaySASNotice() {
NoticeDialog dlg(_pWinLogon, IDD_SASNOTICE);
dlg.Show();
}

 

This really is a simple function. You should display a modal dialog box that tells the user how to log in. The odd thing about this dialog is that it has no buttons on it, and no close box, so there's no way for the user to dismiss it (see Figure 1).
Figure 1 The SAS Notice Dialog 
As I discussed last month, you'll call back into WinLogon whenever you want to display modal dialogs. If you look at the NoticeDialog class, you'll see that it derives from a base class called GinaModalDialog, which provides a very basic framework for handling modal dialogs. It calls into WinLogon's WlxDialogBoxParam method to display the dialog, passing a pointer to itself as the parameter, which the dialog procedure then grabs via WM_INITDIALOG and tucks away as part of the window state. This is coupled with a virtual method named DialogProc that each derived dialog may implement however it likes, which makes dialog state management very simple. I just use member variables for dialogs that need to maintain state. Conceptually this is similar to the way MFC worked, although it's considerably simpler.
The notice dialog doesn't do a thing. It just waits for WinLogon to dismiss it, typically because the user pressed Ctrl+Alt+Del. If your GINA needs to listen for custom SAS events on another thread, your notice dialog should watch for a custom window message that you define (WM_USER, for example), and call WlxSasNotify when this message comes in. Your background thread should post this message whenever it detects the custom SAS. As I mentioned last month, this keeps all your interaction with WinLogon on a single thread, as it should be.

 

WlxLoggedOutSAS
WlxLoggedOutSAS is the next function WinLogon will call in your GINA, when the user presses Ctrl+Alt+Del or you generate a custom SAS from your notice dialog. This is by far the most complicated function your GINA will implement, although conceptually it's not that difficult. This is where GINA collects credentials of some sort from the user, and either logs the user in or rejects the logon attempt. My sample displays a dialog asking for a user name and password, as shown in Figure 2.
 
int WlxLoggedOutSAS(
IN PVOID pWlxContext,
IN DWORD dwSasType,
OUT PLUID pAuthenticationId,
OUT PSID pLogonSid,
OUT PDWORD pdwOptions,
OUT PHANDLE phToken,
OUT PWLX_MPR_NOTIFY_INFO pNprNotifyInfo,
OUT PVOID* pProfile
);

 

Figure 2 The Logged Out SAS Dialog 
Besides your context pointer (pWlxContext), WinLogon only passes in one other parameter: dwSasType. Microsoft defines several SAS types, reserving the range of values 0 to 127. If you define your own SAS type, make sure its value is 128 or greater. There are two SAS types my sample cares about:
 
WLX_SAS_TYPE_CTRL_ALT_DEL
WLX_SAS_TYPE_AUTHENTICATED

 

The second SAS type (WLX_SAS_TYPE_AUTHENTICATED) is currently undocumented, but you must handle it if you want to fully support the Windows® XP Remote Desktop feature. I'll discuss it later.

The return value from WlxLoggedOutSAS tells WinLogon what GINA wants it to do:
 
WLX_SAS_ACTION_NONE
WLX_SAS_ACTION_SHUTDOWN
WLX_SAS_ACTION_LOGON

 

You should return NONE if you want to cancel the logon attempt (for example, if the user presses the Cancel button or supplies invalid credentials too many times). SHUTDOWN tells WinLogon to shut down the system. To demonstrate this, my sample allows any user to press the Shutdown button on the logon dialog. However, in your own GINA, you may not want an anonymous user to be able to do this.
LOGON indicates that GINA has successfully logged on the user. If you return this value, you must also supply the other out parameters from WlxLoggedOutSAS that indicate the details of the logon. This includes a token handle, profile path information, and other details. Your best bet for obtaining these values comes from calling the low-level function LsaLogonUser, which is complicated enough to warrant its own section.

 

Calling LsaLogonUser
The LsaLogonUser function accepts credentials and produces a logon session if the credentials are valid. More specifically, this function dispatches the request to an authentication package such as Kerberos or MSV1_0 (the Windows NT® logon provider), which will then verify the credentials and establish a session. Because there are many different forms of credentials, ranging from a simple user name and password to a Kerberos server ticket and authenticator, the input to this function is defined as a binary BLOB that will be interpreted by the underlying authentication package. This BLOB is passed via the AuthenticationInformation parameter shown in Figure 3.
There are a lot of parameters to this method, so I've taken the liberty of marking the input and output parameters. I'm not going to explain what each and every one of these parameters is for; you can find those details in the documentation. Instead I'll focus on what the current documentation doesn't tell you, and give you tips on using this function from a custom GINA.
My sample supports both domain logons (Kerberos) and local workstation logons (MSV1_0); fortunately the input BLOB for both of these providers looks exactly the same. Here's the MSV1_0 structure definition:
 
typedef struct _MSV1_0_INTERACTIVE_LOGON {
MSV1_0_LOGON_SUBMIT_TYPE MessageType;
UNICODE_STRING LogonDomainName;
UNICODE_STRING UserName;
UNICODE_STRING Password;
} MSV1_0_INTERACTIVE_LOGON;

 

Now here's the tricky part. The UNICODE_STRING fields are pointers to buffers that contain Unicode characters for the domain, user name, and password. You might be tempted to allocate these string buffers separately. If you do this, LsaLogonUser will fail. Instead you must dynamically allocate a block of memory large enough to hold the data structure shown in Figure 3, plus all the string buffers that go along with it. In other words, you must serialize this data structure into a single contiguous buffer before calling LsaLogonUser. Rather than bore you with the code, I'll simply point you to the helper function that forms this request. It's called _allocLogonRequest, and it can be found in the SecurityHelper.cpp file in the sample code available for download from the MSDN®Magazine Web site.
GINA should specify a LogonType of "Interactive" when logging in a user from WlxLoggedOutSAS, unless that user is logging in remotely via Remote Desktop, in which case "RemoteInteractive" should be used. Specify a LogonType of "Unlock" when allowing a user to unlock the workstation.
The only other tricky parameter to LsaLogonUser is LocalGroups, which allows the caller to specify any number of extra Security Identifiers (SIDs) that should be added to the resulting token as it's returned. Think about that for a moment. You have the power to make any user an administrator, for example. Heck, your GINA could include a checkbox, "Make me an admin for this session." If checked, you could specify the well-known SID for the local Administrators group via LocalGroups, and the user would suddenly be an administrator for the length of that logon session.
Don't get me wrong; I'm not suggesting that you do this, just like I'm not suggesting that you hardcode a backdoor into your GINA that allows anyone with a user name of "h@x0r" to log in using the built-in administrator account without knowing the password. But GINA can do these sorts of things, which is why I mentioned last month that you must put a strong access control list (ACL) on your custom GINA DLL file to ensure that a normal user doesn't overwrite it with a malicious GINA. And your GINA had better be bulletproof. You don't want an attacker to exploit a buffer overflow in a GINA and subsequently run arbitrary code there. Note that LsaLogonUser may only be called by code running in the trusted computing base, which GINA is part of by virtue of running inside WinLogon, which runs as SYSTEM. A normal user cannot call LsaLogonUser to elevate privileges.
My sample GINA passes NULL for LocalGroups, and unless you have a good reason not to, yours should as well.
Several outputs from LsaLogonUser are useful in your implementation of WlxLoggedOutSAS. For example, GINA must cache the Token parameter as part of its state. Also, the LogonId LUID is the unique 64-bit identifier for the new logon session, and it maps directly to the pAuthenticationId out parameter for WlxLoggedOutSAS.
The ProfileBuffer output from LsaLogonUser is a BLOB whose format depends on the logon provider. Once again, the Kerberos and MSV1_0 providers agree on the format of this BLOB, which is shown in Figure 4. This structure contains a wealth of information that a fully featured GINA needs. For example, the default GINA checks the PasswordMustChange field to see if the user's password is due to expire soon, and gives the user a chance to change it right then and there. Besides conveniences like this, you'll need to look at the ProfilePath field to be able to properly fill out the pProfile out parameter in WlxLoggedOutSAS, which is another BLOB that must be serialized.

 

Other WlxLoggedOutSAS Considerations
Once LsaLogonUser returns, if you've successfully established a logon, you can cache the user name and domain as part of your state for convenience, as they will be needed elsewhere. Another alternative would be to simply look up the user name and domain whenever they are needed, since you're already caching the user's token, but for domain accounts that will require round-trips to a domain controller, so I simply cache these values in member variables on the Gina class.
WinLogon normally loads the user's profile after WlxLoggedOutSAS returns with a successful logon. To get this default behavior, my sample sets *pdwOptions to 0. Your GINA can customize how the profile is loaded by setting this flag to WLX_LOGON_OPT_NO_PROFILE, which tells WinLogon that GINA has already loaded the profile.
The pNprNotifyInfo output parameter allows other network providers such as Novell Netware to get a peek at the user name and password being used so they can automatically log the user into their networks as well. This prevents the user from having to log on multiple times.
The pLogonSid output parameter points to a fixed sized buffer provided by WinLogon that is large enough to hold a logon SID of the form S-1-5-5-x-y, where x and y are unique values that identify the new logon session. Unfortunately, LsaLogonUser doesn't supply this SID, but you can find it by looking in the token. Ask GetTokenInformation for the TokenGroups class of information, then enumerate the group SIDs in the token until you find the one with the SE_GROUP_LOGON_ID flag. You can then use the CopySID function to copy this value into the buffer provided by pLogonSid. WinLogon uses this SID to grant permissions to the interactive window station and desktop. You can read more about this at What Is A Window Station.
Pay close attention to the return value from LsaLogonUser. If it fails, my sample is careful to check if the account requires an immediate password change or if the user's password has expired. If so, I pop up a password change dialog to give the user a chance to change her password. On any other failures I look up the corresponding error message by calling FormatMessage, display it to the user, and then return WLX_SAS_ACTION_NONE.

 

WlxActivateUserShell
Once the user is logged on, GINA is asked to launch the shell.
 
BOOL WlxActivateUserShell(
IN PVOID pWlxContext,
IN PWSTR pszDesktopName,
IN PWSTR pszMprLogonScript,
IN PVOID pEnvironment
);

 

My sample does what most GINAs should do: it launches USERINIT.EXE. Well, technically it looks in WinLogon's registry key for a named value called Userinit, and launches whatever programs are specified in this comma-delimited string. Most machines will have a value that looks something like this:
 
 
"C:\WINDOWS\system32\userinit.exe,"

 

This little program is responsible for running logon scripts and calling CreateProcess to start the user's shell, which is named in another value aptly called "Shell". Most machines will have the following value for Shell:

 
"Explorer.exe"
 
The trick here is that you can't simply call CreateProcess to launch USERINIT.EXE. If you did, it would run as SYSTEM just like GINA! Instead, you must call CreateProcessAsUser, which takes one extra argument: the handle to the token that your GINA got earlier from calling LsaLogonUser. This will cause USERINIT.EXE and consequently the user's shell to run in the new logon session you've created.
Also, be careful to specify the desktop and environment given to you by WinLogon. You can see my call to CreateProcessAsUser in the SecurityHelper.cpp file in the sample code. Once you return from this function, your GINA won't be called again until something interesting happens, such as the user pressing Ctrl+Alt+Del, in which case you'll see a call to WlxLoggedOnSAS.

 

WlxLoggedOnSAS and WlxDisplayLockedNotice
WinLogon calls WlxLoggedOnSAS when a user is logged on and a SAS occurs. The only interesting argument is dwSasType. My sample watches for WLX_SAS_TYPE_CTRL_ALT_DEL and pops up the dialog shown in Figure 5.
Figure 5 The Logged-On SAS Dialog 
The implementation of this function is simply an exercise in getting some user input and returning a value to WinLogon indicating what the user needs. The only significant functionality you need to implement is a change password dialog, which should call NetUserChangePassword to attempt a password change.
To give you an idea of how easy this function is to implement, here are the constants you can return from it:
 
WLX_SAS_ACTION_NONE
WLX_SAS_ACTION_LOCK_WKSTA
WLX_SAS_ACTION_LOGOFF
WLX_SAS_ACTION_PWD_CHANGE
WLX_SAS_ACTION_TASKLIST
WLX_SAS_ACTION_SHUTDOWN_REBOOT
WLX_SAS_ACTION_SHUTDOWN
WLX_SAS_ACTION_SHUTDOWN_SLEEP
WLX_SAS_ACTION_SHUTDOWN_HIBERNATE
WLX_SAS_ACTION_SHUTDOWN_POWER_OFF

 

So if the user presses the Task Manager button, GINA simply returns the TASKLIST value, and WinLogon will launch the user's task manager. To lock the workstation, return LOCK_WKSTA, and so on. If the user asks to shut down the computer, my sample asks for confirmation and then returns the SHUTDOWN value, but if you wanted to, you could call the power management APIs to determine what other options are available, such as suspend or hibernate, and give the user further options.

The WlxDisplayLockedNotice function is as simple as WlxDisplaySASNotice. In fact, my sample code uses the same dialog class to display this dialog; it just uses a different dialog resource to get an appropriate interface. This dialog also has no buttons and will be dismissed only when a SAS is detected.

 

WlxWkstaLockedSAS
WinLogon calls WlxWkstaLockedSAS when a logged-on user has locked her workstation and a SAS occurs. This function is similar to WlxLoggedOutSAS, except that it takes no out parameters:
 
int WlxWkstaLockedSAS( IN PVOID pWlxContext, IN DWORD dwSasType );
 
The trick to implementing this function is realizing that there are two use cases to be considered. The normal case is where the user who locked her workstation is returning and unlocking it. The corner case is when someone other than the logged-on user tries to unlock the computer. If that person is an administrator, they should be allowed to forcefully log off the user in order to access the workstation themselves.
Here's the approach I take in the sample. I pop-up my logon dialog, and populate the name and domain of the currently logged-on user that I cached earlier in WlxLoggedOutSAS. I then give keyboard fo-cus to the password edit box. This simplifies the normal case where the user is simply unlocking her own workstation.
Once the password prompt returns, I attempt to log the user by calling LsaLogonUser with a logon type of Unlock, which is a special type of logon designed just for GINA, and properly audits the attempt to unlock the workstation.
If the logon succeeds, I look at the resulting token and compare the user SID with the SID of the currently logged-on user. If these SIDs are the same, then I know the user has simply returned to unlock her workstation. I close the new token and return WLX_SAS_ACTION_UNLOCK_WKSTA to WinLogon to indicate that the workstation should be unlocked.
If the SIDs don't match, I check to see if the new user is an administrator by calling CheckTokenMembership, looking for the well-known local Administrators group SID.
If the user is an administrator, I return WLX_SAS_ACTION_FORCE_LOGOFF, and WinLogon logs off the user and brings GINA back around to display the logon prompt.

 

Trivial Functions
There are several trivial functions that my sample implements, so including WlxIsLockOk, WlxIsLogoffOk, WlxLogoff, WlxShutdown, and others, I'll mention them briefly here. Their names are usually self-explanatory.
In WlxIsLockOk and WlxIsLogoffOk, I simply return TRUE. My sample GINA never stops anyone from logging off or locking the workstation, although yours might need to do this.
In WlxLogoff, I clear any variables that have to do with the currently logged-on user. This means closing the user's token and freeing the cached strings for the user name and domain.
WlxShutdown is a pretty obvious notification. I originally assumed that this was the last message I'd ever receive from WinLogon (that's my experience so far). But I've been told by reliable sources that it may be possible to receive status notifications even after WlxShutdown has been called. With this in mind, I keep enough of the GINA alive to process any of these extra messages that happen to come along.
WlxNetworkProviderLoad is obsolete, according to sources inside Microsoft. My GINA simply returns FALSE here, since the entry point is never actually called by WinLogon.
And finally, DisplayStatusMessage, RemoveStatusMessage, and GetStatusMessage allow WinLogon to give me messages for the user from time to time, which I display in a modeless dialog box.

 

Supporting Remote Desktop
In order to support Terminal Services and the Windows XP Remote Desktop feature, you need to make special considerations in your code. Think about what happens when you use the remote desktop client, MSTSC.EXE, to log onto a remote workstation. What happens when you press Ctrl+Alt+Del? You're raising an interrupt on your local hardware, not on the remote machine.
If you want to be friendly to Remote Desktop, the first thing to do is detect whether you're running in a session for a remote user. That information will be useful to you throughout your GINA's lifetime, and it's easy to discover:
 
bool UserIsRemote() { return 0 != GetSystemMetrics(SM_REMOTESESSION); }
 
My sample GINA uses this to skip the Ctrl+Alt+Del requirement for remote desktop users by calling WlxSasNotify to simulate Ctrl+Alt+Del on their behalf. You can see this in my implementation of WlxDisplaySASNotice.
When a user first logs on through Remote Desktop on Windows XP, something rather magical happens inside WinLogon, and GINA must help facilitate it. If you have two workstations side by side you can see this happen. Say workstation A just booted up and is waiting for someone to log on. Its GINA is waiting in WlxDisplaySASNotice. On workstation B, you connect to A via remote desktop and log on. As soon as you enter your logon credentials via B's screen, you'll see something change back on A's screen. What's happening here is a shuffle of terminal services sessions. In Remote Desktop, the operating system ensures that only one user is ever connected to the machine, either via the console or via a remote session. Part of this bookkeeping is ensuring that the logged-on user is always working in Terminal Services session zero.
In this case, the GINA that was loaded on workstation A is running in session 0, but the user is connecting via a new session. Session 1 is a temporary session with its own instance of WINLOGON.EXE, which has also loaded your GINA. This copy of GINA authenticates the user in session 1, and then the magic shuffle starts. After you return from WlxLoggedOutSAS with a successful logon, you'll see a call to WlxGetConsoleSwitchCredentials in session 1. This is the last chance your GINA has to communicate the results of the logon before it disappears forever!
The data structure defined for this function is pretty complicated, but you don't need to fill it all out. One thing you absolutely need to pass is the token you just obtained from LsaLogonUser. WinLogon also expects the UserName field (things won't work properly if you don't pass at least this field).
WinLogon in session 1 now marshals this data, pipes it over to session 0, and calls the GINA's WlxLoggedOutSAS there with the special value WLX_SAS_TYPE_AUTHENTICATED. This constant isn't in the documentation, but that's an oversight, not a secret. This is your signal to grab the data from the session 1 GINA by calling WlxQueryConsoleSwitchCredentials, and return the user's token to WinLogon in session 0.
Interestingly enough, at this point if you call the UserIsRemote function I described earlier, you'll see that session 0 has suddenly gone remote! The user on workstation B is now using session 0, and the login will proceed as normal. Session 1 has been terminated. Back on workstation A's console, yet another temporary Terminal Services session has been constructed, typically session 2 in the scenario I've described. This session exists to inform the user that the workstation is in use, and allow the user to switch back to the console if so desired.
Another thing you'll want to check for is whether the user supplied credentials via the Remote Desktop client. You can discover this by calling WinLogon's WlxQueryTsLogonCredentials function. If the user has already provided credentials, you don't want to prompt her to supply them again, so this is mainly a convenience.
If this all sounds complicated, you're right, it really is. That's why the team working on the next version of Windows (code-named "Longhorn") is trying to get rid of this rather messy interface once and for all. But if you're customizing the logon experience on Windows XP and need to support Remote Desktop, you must jump through these hoops. The sample accompanying this article illustrates how it's done, including a number of details that I didn't have room to discuss in these two columns.

 

Conclusion
Writing a custom GINA is not easy. MSGINA is a very complicated piece of machinery, and replacing it is not trivial. But a lot of folks have found it necessary over the years. If you can't use the stub type GINA that I discussed last month and therefore are forced to write a custom GINA from scratch, I hope you find these columns with their accompanying samples helpful in your quest. Just remember that you are implementing the heart of the interactive logon plumbing in Windows, and it's critical that your code be absolutely correct and bulletproof! To share knowledge with other GINA developers, please visit my wiki at Customizing GINA.

 

Send your questions or comments for Keith at  briefs@microsoft.com.

 

Keith Brown is a co-founder of Pluralsight, specializing in developing and delivering high quality training for software developers. Keith's most recent book is The .NET Developer's Guide to Windows Security. Keith also keeps a web log and contact info at www.pluralsight.com/keith.
TAG •

List of Articles
No. Subject Author Date Views
45 [MFC] Dialog에서 부모 윈도우 알아내기 digipine 2017.10.28 923
44 Customizing GINA, Part 1 digipine 2017.10.28 21759
» Customizing GINA, Part 2 digipine 2017.10.28 96711
42 The .Net Developer's Guide to Directory Services Programming digipine 2017.10.29 7168
41 GINA(Graphical Identification aNd Authentication), SAS(Secure Attention Sequence) digipine 2017.10.29 1258
40 RPC에 대하여... (1) : RPC 가 사용하는 TCP/IP 포트는 ? digipine 2017.10.29 1295
39 RPC에 대하여... (2) : RPC 가 사용하는 포트를 바꿔보자 digipine 2017.10.29 1118
38 RPC에 대하여... (3) : RPC 작동을 위한 테스트 방법 digipine 2017.10.29 542
37 Windows API 멀티 쓰레드 구현법 digipine 2017.10.29 691
36 Mutex, Critical Section Class 만들기 digipine 2017.10.29 392
35 CreateSemaphore Semaphore Manager digipine 2017.10.29 447
34 VC++(MFC)에서 MDB 생성 / 압축 / 연동관리자 digipine 2017.10.29 2654
33 VC++ 에서 대소문자 변경하는 함수 digipine 2017.10.29 310
32 세마포어의 개념과 사용법 digipine 2017.10.29 765
31 Serialize를 이용한 객체 복사하기 (Copy constructor) digipine 2017.10.29 499
30 DLL과 EXE간의 데이타 공유하기 digipine 2017.10.29 837
29 VS2003 이상에서 iostream 구현의 문제점 digipine 2017.10.29 256
28 VS2005 ConvertBSTRToString 에서 LNK2019 에러 대처법 digipine 2017.10.29 192
27 [C#] Convert char[] to string digipine 2017.10.29 301
26 [C#] C#에서 C++ DLL의 Call by Referance out 인수 사용하는 방법 digipine 2017.10.29 1076
Board Pagination Prev 1 2 3 Next
/ 3