Jump to content

Any win32 coders that can help answer a question regarding the clipboard API?


dustbyter
 Share

Recommended Posts

Hi AlfAlfa,

I'm looking to implement the API for reading the clipboard. Seems it keeps failing. I can't figure out why. When stepping through the code with the debugger, it works. When running the executable, it fails.

wchar_t *getClipboardText(void)
{
	if( OpenClipboard(NULL) )
	{
		// if we don't find any text of UNICODE format, then we can't copy it 
		if ( IsClipboardFormatAvailable(CF_TEXT) || IsClipboardFormatAvailable(CF_UNICODETEXT) )
		{
			HANDLE hToken;
			wchar_t *wGlobal;
		
			hToken = GetClipboardData(CF_UNICODETEXT);

			if ( !hToken )
				return L"Error-no hToken\0";

			wGlobal = (wchar_t*)GlobalLock(hToken);
		
			GlobalUnlock(hToken);
			CloseClipboard();
			return wGlobal;
		}
		else
		{
			return L"CF_UNICODETEXT format text not present, or no text on clipboard";
		}
	}
	return L"Error-Cannot Open Clipboard\0";
}

After adding the IsClipboardFormatAvailable() API call, it fails here... and returns return L"CF_UNICODETEXT format text not present, or no text on clipboard";

Prior to adding this API call, it would fail on the GetClipboardData call.

I copy ANSI and UTF8 code, so i know what is on the clipboard is valid to be copied... but it doesn't work unless im debugging

Any ideas? I'm just starting to play with win32 API

Link to comment
Share on other sites

Okay dustbyter I threw your code into visual studio with a blank project and added an int main()...

The clipboard grabbing code itself seems right, but if you only want unicode to be able to be copied just have it like, you don't need the 'or CF_TEXT' part:

// if we don't find any text of UNICODE format, then we can't copy it
        if(IsClipboardFormatAvailable(CF_UNICODETEXT))
        {

Now I was unable to reproduce your problem though, either with debugging or running it straight up and as a release build... But I think it's probably because there isn't much going on in my test app, and in yours you probably use it many times and have much more going on in memory...

However I suspect the problem is you are referencing a memory location that you don't own and trying to keep a pointer to it like you'll always be able to access it freely.

Try making your own copy of the unicode text after the GlobalLock call, but before unlocking...

Test this bare bones example which I used your code and it seemed to work for me, then I made my change to it using a std::unique_ptr to hold onto a copy of the retrieved clipboard contents, you could manually worry about having to free a raw wchar_t* but that's too old school I'm on the latest (C++11, C++ 14) C++ features now :D

#include <Windows.h>
#include <stdio.h>
#include <memory>

std::unique_ptr<wchar_t> getClipboardText();


int main(int argcount, char* args[])
{
    if(argcount > 1 && (strcmp(args[1],"-c") == 0 || strcmp(args[1],"--show-clipboard") == 0))
    {
        std::unique_ptr<wchar_t> clipboardContents = getClipboardText();

        printf("Clipboard Contents:\r\n%S", clipboardContents.get());
    }

    return 0;
}

std::unique_ptr<wchar_t> getClipboardText()
{
    if(OpenClipboard(0))
    {
        // if we don't find any text of UNICODE format, then we can't copy it
        if(IsClipboardFormatAvailable(CF_UNICODETEXT))
        {
            HANDLE hToken;
            wchar_t *wGlobal;
        
            hToken = GetClipboardData(CF_UNICODETEXT);

            if (!hToken)
            {
                OutputDebugStringW(L"Error-no hToken");
                return 0;
            }

            wGlobal = (wchar_t*)GlobalLock(hToken);

            //Make your own copy of the clipboard contents, since you don't own the memory pointed to by wGlobal you can only access it temporarily when locked
            int contentsLength = wcslen(wGlobal) + 1; //(null terminator)
            std::unique_ptr<wchar_t> clipboardContents(new wchar_t[contentsLength]);
            memcpy(clipboardContents.get(),wGlobal,contentsLength*sizeof(wchar_t));
        
            GlobalUnlock(hToken);
            CloseClipboard();

            return std::move(clipboardContents);
        }
        else
        {
            OutputDebugStringW(L"CF_UNICODETEXT format text not present, or no text on clipboard");
        }
    }
    OutputDebugStringW(L"Error-Cannot Open Clipboard");
    return 0;
}

FgrgW0P.png

See if you can reproduce your problem in a test application, even though I haven't been able to... By the way what environment are you using, it could be something particular with that, or just something particular with your code... I hope you get it working, I'll help you further if you were able to have a small full example that reproduces your problem! :)

Edited by AlfAlfa
Link to comment
Share on other sites

std::unique_ptr<wchar_t> clipboardContents(new wchar_t[contentsLength]);

Woah. That's how you declare a class-type variable and invoke its constructor in C++ these days? I am getting old...

Link to comment
Share on other sites

std::unique_ptr<wchar_t> clipboardContents(new wchar_t[contentsLength]);

Woah. That's how you declare a class-type variable and invoke its constructor in C++ these days? I am getting old...

Well actually I meant to make it "std::unique_ptr<wchar_t[]>" that way you can index an individual character if you wanted: clipboardContents... Yes you don't have to though, but I like using them that way you just keep it in scope for as long as you want it to live and it frees itself when it goes out of scope or you re-assign it or reset it! C++ does like the smarter way to garbage collect, it's not garbage collection though, it's still manual memory management but in a smart way!

EDIT: To be more clear the line constructs both a std::unique_ptr object and it's constructor gets passed the construction of your actual object or array of objects you want the unique pointer to point to... shared pointer used if you need multiple references to the same object

lol remember:

char *rawCharacters = (char*)malloc(1024);
memset(rawCharacters, 0, 1024);
 
//...
free(rawCharacters);

:)

Hi AlfAlfa,

I'm looking to implement the API for reading the clipboard. Seems it keeps failing. I can't figure out why. When stepping through the code with the debugger, it works. When running the executable, it fails.

wchar_t *getClipboardText(void)
{
	if( OpenClipboard(NULL) )
	{
		// if we don't find any text of UNICODE format, then we can't copy it 
		if ( IsClipboardFormatAvailable(CF_TEXT) || IsClipboardFormatAvailable(CF_UNICODETEXT) )
		{
			HANDLE hToken;
			wchar_t *wGlobal;
		
			hToken = GetClipboardData(CF_UNICODETEXT);

			if ( !hToken )
				return L"Error-no hToken\0";

			wGlobal = (wchar_t*)GlobalLock(hToken);
		
			GlobalUnlock(hToken);
			CloseClipboard();
			return wGlobal;
		}
		else
		{
			return L"CF_UNICODETEXT format text not present, or no text on clipboard";
		}
	}
	return L"Error-Cannot Open Clipboard\0";
}

After adding the IsClipboardFormatAvailable() API call, it fails here... and returns return L"CF_UNICODETEXT format text not present, or no text on clipboard";

Prior to adding this API call, it would fail on the GetClipboardData call.

I copy ANSI and UTF8 code, so i know what is on the clipboard is valid to be copied... but it doesn't work unless im debugging

Any ideas? I'm just starting to play with win32 API

EDIT: dustbyter another update... You went to a console application so I went to a GUI application for a couple reasons: To test whether adding a window handle of a window we own to the OpenClipboard call would help, and because I came up with a new idea... I got the idea from usb devices that also function as keyboards often used for exploitation. If we're having trouble with the APIs to grab the clipboard, maybe lets just simulate a CTRL+V keypress and capture whatever gets pasted in a window we create, then grab the text from there! It's worth a try, and it is an alternative method :)

I've divided this one up into 3 tests ran on 3 separate threads, but it waits for each test to complete before running the next one.

1st: send CTRL+V via SendInput after waiting for the application to open and have focus on the edit control, then use GetWindowTextW to grab the text from it as unicode

2nd: IsClipboardFormatAvailable called on all three text formats CF_UNICODETEXT, CF_TEXT, and CF_OEMTEXT (because of synthesized clipboard formats, this shouldn't be necessary since as long as any text format is available it will convert to your desired text format which is unicode in this case)

3rd: EnumClipboardFormats to show you ALL of the available formats and display the unicode text format contents if they are available

Finally in your last post you said it failed with the error described in your first post, but that was only using IsClipboardFormatAvailable, what is your results for the enumeration of formats test (test 3) ?

If it's failing saying unicode text isn't available that shouldn't be the case as long as any text is available since I learned that it will automatically convert between the text formats to the one you want when you call GetClipboardData(format);

And if it's failing there, that means it didn't fail on the OpenClipboard call right? So we do, have access to the clipboard, it's just what is actually on it...

Maybe there really isn't text on the clipboard and it's another format... That's why I wanted to see the results of the enumeration rather than just checking one particular format individually!

Let me know if the control + v paste and grab text method works... It closes the window after the third test finishes so you won't leave it running if your testing this on a remote machine :D

#include <Windows.h>
#include <stdio.h>
#include <memory>
#include <thread>

#define ID_EDITCHILD 200
LRESULT WINAPI WindowProcedure(HWND hwnd,UINT message,WPARAM wParam,LPARAM lParam);
int SetDebugPrivileges();
void PrintLastError (const char *msg);

class AppData
{
private:
    static std::unique_ptr<AppData> data;
public:
    HINSTANCE hInst;
    HWND hWnd, editWnd;
    wchar_t clipboardText[2048];
    char debugText[1024];
    
    AppData() { hInst = 0; hWnd = 0; editWnd = 0; memset(clipboardText, 0, 4096); }

    static AppData* get()
    {
        if(!data.get())
            data = std::unique_ptr<AppData>(new AppData());
        return data.get();
    }
};
std::unique_ptr<AppData> AppData::data;
AppData *d = AppData::get();

class Clipboarder
{
public:
    static std::unique_ptr<wchar_t[]> contents;
    static std::thread spawnTest1() { return std::thread( [=] { NEWControlVGetText_test1(); } ); }
    static std::thread spawnTest2() { return std::thread( [=] { isTextOfAnyKindAvailable_test2(); } ); }
    static std::thread spawnTest3() { return std::thread( [=] { enumerateAllFormatsWhatFormatsAreAvailable_test3(); } ); }

    static void NEWControlVGetText_test1()
    {
        INPUT ip[2];
        OutputDebugStringW(L"\r\nTest1... Hit Control+V, wait a couple seconds, then grab the text from the edit control window :)");
        memset(&ip[0], 0, sizeof(INPUT)*2);
        // Pause for 3 seconds.
        Sleep(3000);

        SetForegroundWindow(AppData::get()->hWnd);
        SetForegroundWindow(AppData::get()->editWnd);
        SetFocus(AppData::get()->editWnd);

        // Set up a generic keyboard event.
        ip[0].type = ip[1].type = INPUT_KEYBOARD;
        // Press the "CTRL" key
        ip[0].ki.wVk = VK_LCONTROL; // virtual-key code for the left control key
        // Press the "V" key
        ip[1].ki.wVk = 'V'; // virtual-key code for the "v" key
        SendInput(2, &ip[0], sizeof(INPUT));
 
        // Release both keys
        ip[0].ki.dwFlags = ip[1].ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release
        SendInput(2, &ip[0], sizeof(INPUT));

        // Pause for another 2 seconds.
        Sleep(2000);

        //Then grab pasted text (if it pasted successfully to our edit control within our window)
        GetWindowTextW(AppData::get()->editWnd, AppData::get()->clipboardText, 2048);
        
        OutputDebugStringW(L"Control+V Paste And GetWindowText Clipboard Contents:");
        contents = makeWideString(AppData::get()->clipboardText);
        OutputDebugStringW(contents.get());

        std::thread test2 = spawnTest2();
        test2.join();
        std::thread test3 = spawnTest3();
        test3.join();
        SendMessageW(AppData::get()->hWnd, WM_CLOSE, 0, 0); //close the application after our 3 tests complete...
    }
    static void isTextOfAnyKindAvailable_test2()
    {
        HANDLE hToken;
        wchar_t *wGlobal;
        OutputDebugStringW(L"\r\nTest2... Use IsClipboardFormatAvailable to check if text of any kind is available...");
        if(IsClipboardFormatAvailable(CF_UNICODETEXT) || IsClipboardFormatAvailable(CF_TEXT) || IsClipboardFormatAvailable(CF_OEMTEXT))
        {
            //If any one of the three is available, then all should be available as GetClipboardData will convert between the text types for you as long as one is available...
            OutputDebugStringW(L"CF_UNICODETEXT and CF_TEXT and CF_OEMTEXT should all be available...");
            if(!OpenClipboard(AppData::get()->hWnd))
            {
                PrintLastError("Error-Cannot Open Clipboard");
                return;
            }

            hToken = GetClipboardData(CF_UNICODETEXT);
            if(!hToken)
            {
                PrintLastError("Error-no hToken");
                return;
            }

            wGlobal = (wchar_t*)GlobalLock(hToken);

            contents = makeWideString(wGlobal);
            OutputDebugStringW(L"Clipboard Contents From Test2:\r\n");
            OutputDebugStringW(contents.get());
        
            GlobalUnlock(hToken);
            CloseClipboard();
        }
        else
        {
            PrintLastError("Niether CF_UNICODETEXT nor CF_TEXT nor CF_OEMTEXT are present... (according to IsClipboardFormatAvailable())");
        }
    }
    static void enumerateAllFormatsWhatFormatsAreAvailable_test3()
    {
        HANDLE hToken;
        wchar_t *wGlobal;

        OutputDebugStringW(L"\r\nTest3... Enumerate all available formats, show them, and grab unicode text successfully as long as any kind of text is available...");
        AppData *d = AppData::get();
        if(OpenClipboard(d->hWnd))
        {
            OutputDebugStringW(L"Getting Available Clipboard Formats...\r\n");
            int numFormats = CountClipboardFormats();
            sprintf_s(d->debugText,"Number of available clipboard formats: %u\r\n",numFormats);
            OutputDebugStringA(d->debugText);

            UINT format = EnumClipboardFormats(0);
            UINT i = 0;
            while(format != 0)
            {
                if(format == CF_OEMTEXT)
                    sprintf_s(d->debugText,"#%u available format: %u (CF_OEMTEXT)\r\n",++i,format);
                else if(format == CF_TEXT)
                    sprintf_s(d->debugText,"#%u available format: %u (CF_TEXT)\r\n",++i,format);
                else if(format == CF_UNICODETEXT)
                    sprintf_s(d->debugText,"#%u available format: %u (CF_UNICODETEXT)\r\n",++i,format);
                else if(format == CF_BITMAP)
                    sprintf_s(d->debugText,"#%u available format: %u (CF_BITMAP)\r\n",++i,format);
                else if(format == CF_OWNERDISPLAY)
                    sprintf_s(d->debugText,"#%u available format: %u (CF_OWNERDISPLAY)\r\n",++i,format);
                else if(format == CF_WAVE)
                    sprintf_s(d->debugText,"#%u available format: %u (CF_WAVE)\r\n",++i,format);
                else
                    sprintf_s(d->debugText,"#%u available format: %u\r\n",++i,format);
                
                OutputDebugStringA(d->debugText);
                //No need to check for the other two here, EnumClipboardFormats will have unicode as an available format if CF_TEXT or CF_OEMTEXT is available.
                if(format == CF_UNICODETEXT)
                {
                    hToken = GetClipboardData(format);
                    if(!hToken)
                    {
                        OutputDebugStringW(L"Error-no hToken");
                        continue;
                    }

                    wGlobal = (wchar_t*)GlobalLock(hToken);

                    //output contents if unicode is an available format returned from EnumClipboardFormats! (it should be as long as any kind of text is available...)
                    contents = makeWideString(wGlobal);
                    OutputDebugStringW(L"Clipboard Contents From Test3:\r\n");
                    OutputDebugStringW(contents.get());
                
                    GlobalUnlock(hToken);
                }

                format = EnumClipboardFormats(format);
            }
            CloseClipboard();
        }
    }
    static std::unique_ptr<wchar_t[]> makeWideString(wchar_t *fromWideString)
    {
        int stringLength = wcslen(fromWideString) + 1; //(null terminator)
        std::unique_ptr<wchar_t[]> madeWideString(new wchar_t[stringLength]);
        memcpy(madeWideString.get(),fromWideString,stringLength*sizeof(wchar_t));
        return std::move(madeWideString);
    }
};

std::unique_ptr<wchar_t[]> Clipboarder::contents;

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nShowCmd)
{
    MSG messages;
    WNDCLASSEXA wcx;
    LPCSTR szClassName = "ClipboarderClass", szWindowName = "Clipboarder-ng";

    d->hInst = hInstance;
    wcx.hInstance = d->hInst;
    wcx.lpszClassName = szClassName;
    wcx.lpfnWndProc = WindowProcedure;
    wcx.style = CS_HREDRAW|CS_VREDRAW;
    wcx.cbSize = sizeof(WNDCLASSEXW);

    wcx.hIcon = LoadIcon(0, IDI_APPLICATION);
    wcx.hIconSm = LoadIcon(0, IDI_APPLICATION);
    wcx.hCursor = LoadCursor(0, IDC_ARROW);
    wcx.lpszMenuName = 0;
    wcx.cbClsExtra = 0;
    wcx.cbWndExtra = 0;
    wcx.hbrBackground = (HBRUSH)COLOR_BTNFACE;
    if(!RegisterClassExA(&wcx)) return 0;

    SetDebugPrivileges(); //might help on XP systems...

    d->hWnd = CreateWindowA(szClassName,szWindowName,WS_SYSMENU | WS_THICKFRAME,CW_USEDEFAULT,CW_USEDEFAULT,600,420,HWND_DESKTOP,0,d->hInst,0);

    d->editWnd = CreateWindowExW(0,L"EDIT",
                                0,         // no window title
                                WS_CHILD | WS_VISIBLE | WS_VSCROLL |
                                ES_LEFT | ES_MULTILINE | ES_AUTOVSCROLL,
                                0, 0, 0, 0,   // set size in WM_SIZE message
                                d->hWnd,         // parent window
                                (HMENU) ID_EDITCHILD,   // edit control ID
                                (HINSTANCE) GetWindowLong(d->hWnd, GWL_HINSTANCE),
                                0);        // pointer not needed


    UpdateWindow(d->hWnd);
    ShowWindow(d->hWnd, nShowCmd);

    std::thread test1 = Clipboarder::spawnTest1(); //test1 also spawns tests 2 and 3...

    while(GetMessage(&messages,0,0,0))
    {
        TranslateMessage(&messages);
        DispatchMessage(&messages);
    }

    test1.join(); //join up with all three tests before exiting...

    return messages.wParam;
}

LRESULT WINAPI WindowProcedure(HWND hwnd,UINT message,WPARAM wParam,LPARAM lParam)
{
    switch(message)
    {
    case WM_PAINT:
        ValidateRect(hwnd,FALSE);
        return TRUE;
    case WM_SIZE:
            // Make the edit control the size of the window's client area.

            MoveWindow(d->editWnd,
                       0, 0,                  // starting x- and y-coordinates
                       LOWORD(lParam),        // width of client area
                       HIWORD(lParam),        // height of client area
                       TRUE);                 // repaint window
            return 0;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hwnd,message,wParam,lParam);
    }
    return 0;
}

int SetDebugPrivileges()
{
    TOKEN_PRIVILEGES priv = {0};
    HANDLE hToken = NULL;

    if( OpenProcessToken( GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken ) )
    {
        priv.PrivilegeCount           = 1;
        priv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

        if( LookupPrivilegeValue( NULL, SE_DEBUG_NAME, &priv.Privileges[0].Luid ) )
        {
            if(AdjustTokenPrivileges( hToken, FALSE, &priv, 0, NULL, NULL ) == 0)
            {
                sprintf_s(AppData::get()->debugText,"AdjustTokenPrivilege Error! [%u]\n",GetLastError());
                OutputDebugStringA(AppData::get()->debugText);
            }
            else
                OutputDebugStringW(L"Debug Privileges Set Successfully!");
        }
        CloseHandle( hToken );
    }
    return GetLastError();
}

void PrintLastError (const char *msg)
{
        DWORD errCode = GetLastError();
        char *err;
        if (!FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
                           NULL,
                           errCode,
                           MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // default language
                           (LPTSTR) &err,
                           0,
                           NULL))
            return;

        static char buffer[1024];
        //sprintf(buffer,"%s: %X %s\n", msg, errCode, err);
        sprintf_s(buffer,"%s: %X %s\n", msg, errCode, err);
        OutputDebugStringA(buffer);
        LocalFree(err);
}

My results of running this application with all three tests: (after copying text from within Microsoft Visual C++ IDE)aUf2EPm.png

Edited by AlfAlfa
Link to comment
Share on other sites

I converted the project to a console application and it worked flawlessly.

Hmm - making me wonder if the issue is in the agent's project. The idea is that there are agents that get deployed on various windows boxes on a network and you have a way to speak to them. Tried it on WinXP, Win7, Win10 systems thus far and it fails on all them with the error as described in the first post.

Just stumps me as not sure why the application won't get access to the clipboard.

Link to comment
Share on other sites

I converted the project to a console application and it worked flawlessly.

Hmm - making me wonder if the issue is in the agent's project. The idea is that there are agents that get deployed on various windows boxes on a network and you have a way to speak to them. Tried it on WinXP, Win7, Win10 systems thus far and it fails on all them with the error as described in the first post.

Just stumps me as not sure why the application won't get access to the clipboard.

In an effort to not turn this thread into walls of code (lol) I have edited my third post with my third update... With a new method, hopefully the CTRL+V plus getwindowtext will work for us :D

P.S. I added a "SetDebugPrivileges()" function since you mentioned XP... Since it might help for XP... I remember having to use that to get full administrator access within your application on XP (even though you are always running as administrator it was just an extra step on XP, on later systems it didn't seem to matter much though and instead you needed administrator access in cases where SetDebugPrivileges with SE_DEBUG_NAME privilege would work on XP)

I mean this is just the clipboard here, it shouldn't be this tricky just to grab whats on it! :)

Edited by AlfAlfa
Link to comment
Share on other sites

If you fear showing swathes of code, use the spoiler tag to hide it from those that would be unable to deal with that level of nerd porn.

Thanks for the tip Cooper! I've applied it to the previous post, and the latest best post is visible without expanding anything. :)

Sorry, haven't had time to play with this given the holidays. I'll play with it hopefully next week and provide some updates.

That's cool, whenever you do though, maybe the text isn't on the clipboard as a text format. Perhaps it's encoded or encrypted and stored on the clipboard in a data format or custom format! Did you think of that? I haven't however found a way to determine the clipboard size of any available clipboard format though. I think you just have to know how to read whatever format you need... So maybe the next thing would be to actually get the data from all available formats and read the first 10 bytes or something like that and see if anything looks familiar or if it looks encrypted :)

Link to comment
Share on other sites

  • 2 weeks later...

Worked on this a bit today and didn't have any luck.

Starting to suspect maybe some type of permission issue?? although its not a very critical task... just reading the clipboard.

From a goal perspective, I am able to load a dll into memory of target machine and read clipboard that way, but would have been nice to add it to the stage 1 executable.

EDIT:

When the process runs as Local System (service) then does not work. But if it runs as a user who is an admin on the system, then it runs just fine...

Edited by dustbyter
Link to comment
Share on other sites

Well I'm thinking it's not that it's intentionally prevented or blocked, but just the fact that it is a lower level process which doesn't have access to win32 gui stuff, like the clipboard. You have to provide a way around it like you have, to reach over into win32 gui land. It's kind of like how with trying to execute a gui application through php's "exec" function can't and wont work! When I've had to do that you have to work around it (in that case I had a gui application running with a pipe or socket open, then through the php code send it what gui application to run and then have it execute it instead of php directly executing it)

I know a little off track here but it is a similar issue.

Perhaps if you build a kernel driver or some deeper system calls could get you through to the clipboard without the extra dll injecting step! Like even debugging the clipboard apis and see what lower level apis they use could also get you through. :smile:

Link to comment
Share on other sites

Did some research, and seems that its a user issue.

Since my service is running as a different account, it cannot reach into the clipboard of the active user, as they have a "different" clipboard.

I would need to somehow figure out a way to sign in as that other user from my service. Probably not worth the effort, since the DLL injection works just fine.

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...