Solved

Intercept Windows API Calls

Posted on 1998-08-06
52
2,394 Views
Last Modified: 2013-12-03
I want to intercept an API function call.  I am writing a TCP/IP tunneling program and need to intercept all calls to winsock32.dll and then make those calls from with in my program.  I do not want to use any 3d party utilities (like SoftICE) or replace the winsock32.dll.  So how do I do this programmically?  What can I do in C/C++ or ASM to intercept these or any other API calls before windows calls the code in the approiate DLL?
I remember using a program called AutoSOCKS that captured the TCP/IP information and automatically entered necessary proxy information so it appeared that your system was directly connected to the internet.  They did not replace the winsock32.dll file to do this either.  
If needed, I will add more points to the question.
0
Comment
Question by:lordx
  • 21
  • 8
  • 8
  • +4
52 Comments
 
LVL 2

Expert Comment

by:duneram
ID: 1413141
Yes this is possible, but you are going to have to put your replacement API's in a DLL and then inject that DLL into any application that uses those apis.  Do you know which application uses the API's that you want to hook?

The reason I ask this is when a 32 bit program starts up, there is a giant table of imported api's that gets resolved.  Up successful loading of the table, you will have a function address for each api.  The trick is to have your DLL install itself in the target application, then replace the api function address in the table with the address of your replacement api (do this for each of them).  Then in your function you need to make (if you want to perform the real api function) the api call the original function address that you replaced.

Its kinda like this:
(example  these addresses are arbritary (not real))

API Jump table
SetForegroundWindow      0x000f0100  
.

Your dll gets mapped into the target process.  You want to hook the SFW api so you locate the function address in the table and replace it with the address of your function

MySetForegroundWindow starts at 0x07000300 (in this example)

API Jump table
SetForegroundWindow    0x07000300   (my address)

Now whenever the SFW api is called in that app, it will reference the jump table and call yours (in your dll).  You need to make sure you eventually invoke the original address of 0x000f0100 so that the original SFW is called.

I will post an answer in code in a few moments.
0
 
LVL 2

Expert Comment

by:duneram
ID: 1413142
I've done this before and have my own homemade CD's with the code somewhere to do it.  I haven't found the CD yet, but I'll keep looking.  (i have hundreds of homemade cd's....)
0
 
LVL 23

Expert Comment

by:chensu
ID: 1413143
0
Free Tool: Site Down Detector

Helpful to verify reports of your own downtime, or to double check a downed website you are trying to access.

One of a set of tools we are providing to everyone as a way of saying thank you for being a part of the community.

 

Author Comment

by:lordx
ID: 1413144
The winsock spy program is good but it requires me to replace the winsock32.dll.  This is something I did not want to do.  Duneram's idea is what I am looking for.
0
 
LVL 20

Expert Comment

by:Madshi
ID: 1413145
Duneram's idea seems to be good, but (even if it really works :-) you would have do manipulate all applications you want to hook.
On "www.sysinternals.com" there is a sample named "regMonitor" (for Win95 and WinNT). This sample hooks system wide (!) all calls for registry. Perhaps you could hook winsock calls in the same way. (I'm not sure, it's only an idea.)
But then you would have to write drivers! :-(
0
 
LVL 2

Expert Comment

by:duneram
ID: 1413146
I've done the api hook thing and when I get home I'll take a look again for my source
0
 

Author Comment

by:lordx
ID: 1413147
Adjusted points to 750
0
 
LVL 3

Accepted Solution

by:
xyu earned 1000 total points
ID: 1413148
Here is exact answer for Your request...
The ZIP file with entire project I've put at: "http://www.geocities.com/SiliconValley/1741/miscprog/intercept.zip"

The listing is :

/****************************************************************************/
/** intercept.cpp                                                          **/
/** ---------------------------------------------------------------------- **/
/** Example of interception of an API or any DLL function call             **/
/** ---------------------------------------------------------------------- **/
/** The method shown here may be very impressive in conjunction with       **/
/** CreateRemoteThread API                                                 **/
/** ---------------------------------------------------------------------- **/
/** July 23, 1998 by Oleg Kagan                                            **/
/****************************************************************************/

/****************************************************************************/

//============================================================================
#include <windows.h>

// Switch all optimizations off
// (Visual C specific... For any other compiler do the same thing)
//============================================================================
#pragma optimize("", off)

//============================================================================
#define MakePtr(Type, Base, Offset) ((Type)(DWORD(Base) + (DWORD)(Offset)))

//============================================================================
BOOL InterceptDllCall(

      HMODULE hLocalModule,
      const char* c_szDllName,
      const char* c_szApiName,
      PVOID pApiNew,
      PVOID* p_pApiOrg,
      PVOID pApiToChange
      
){
    PIMAGE_DOS_HEADER pDOSHeader = (PIMAGE_DOS_HEADER)hLocalModule;
    PIMAGE_NT_HEADERS pNTHeader;
    PIMAGE_IMPORT_DESCRIPTOR pImportDesc;
    DWORD dwProtect;
      BOOL bSuccess = FALSE;
   
    DWORD dwAddressToIntercept;

      if (pApiToChange) {
            dwAddressToIntercept = (DWORD)pApiToChange;
      }
      else {
            dwAddressToIntercept = (DWORD)GetProcAddress(
                  GetModuleHandle((char*)c_szDllName), (char*)c_szApiName
            ) /*GetProcAddress*/;
      } /*iff*/;

    if (IsBadReadPtr(hLocalModule, sizeof(PIMAGE_NT_HEADERS)))
        return FALSE;
   
    if (pDOSHeader->e_magic != IMAGE_DOS_SIGNATURE)
        return FALSE;
   
    pNTHeader = MakePtr(PIMAGE_NT_HEADERS, pDOSHeader, pDOSHeader->e_lfanew);
    if (pNTHeader->Signature != IMAGE_NT_SIGNATURE)
        return FALSE;
   
    pImportDesc = MakePtr(
            PIMAGE_IMPORT_DESCRIPTOR, hLocalModule,
        pNTHeader->OptionalHeader.DataDirectory[
                  IMAGE_DIRECTORY_ENTRY_IMPORT
            ] /*pNTHeader->OptionalHeader.DataDirectory*/.VirtualAddress
      ) /*MakePtr*/;
   
    if (pImportDesc == (PIMAGE_IMPORT_DESCRIPTOR)pNTHeader) return FALSE;
   
      while (pImportDesc->Name) {
            PIMAGE_THUNK_DATA pThunk;
   
            pThunk = MakePtr(
                  PIMAGE_THUNK_DATA, hLocalModule, pImportDesc->FirstThunk
            ) /*MakePtr*/;
   
            while (pThunk->u1.Function) {
                  if (DWORD(pThunk->u1.Function) == dwAddressToIntercept) {      
                        if (
                              !IsBadWritePtr(
                                    (LPVOID)pThunk->u1.Function, sizeof(DWORD)
                              ) /*!IsBadWritePtr*/
                        ){
                              if (p_pApiOrg)
                                    *p_pApiOrg = PVOID(pThunk->u1.Function);
                              (PDWORD)pThunk->u1.Function = (PDWORD)pApiNew;
                              bSuccess = TRUE;
                        }
                        else {
                              if (
                                    VirtualProtect(
                                          (LPVOID)(&pThunk->u1.Function), sizeof(DWORD),
                                          PAGE_EXECUTE_READWRITE, &dwProtect
                                    ) /*VirtualProtect*/
                              ){
                                    DWORD dwNewProtect;

                                    if (p_pApiOrg)
                                          *p_pApiOrg = PVOID(pThunk->u1.Function);
                                    pThunk->u1.Function = (PDWORD)pApiNew;
                                    bSuccess = TRUE;

                                    dwNewProtect = dwProtect;
                                    VirtualProtect(
                                          (LPVOID)(&pThunk->u1.Function), sizeof(DWORD),
                                          dwNewProtect, &dwProtect
                                    ) /*VirtualProtect*/;
                              } /*if*/
                        } /*iff*/
                  } /*if*/
                  pThunk++;
            } /*while*/
            pImportDesc++;
      } /*while*/

    return bSuccess;
} /*InterceptDllCall(HMODULE, const char*, const char*, PVOID,PVOID*,PVOID)*/

//============================================================================
void ChangeText(

      char* szText
      
){
      size_t nLength = strlen(szText);
      for (size_t i = 0; i < nLength; ++i) {
            szText[i] = char((i % 2) ? tolower(szText[i]) : toupper(szText[i]));
      } /*for (size_t i)*/
} /*ChangeText(char*)*/

//============================================================================
typedef int (*TMessageBoxFuncPtr)(HWND, LPCTSTR, LPCTSTR, UINT);
TMessageBoxFuncPtr p_fnMessageBoxOrg = NULL;

//============================================================================
int WINAPI MyMessageBox(

      HWND hWnd,          // handle of owner window
      LPCTSTR lpText,     // address of text in message box
      LPCTSTR lpCaption,  // address of title of message box
      UINT uType          // style of message box

){
      if (!p_fnMessageBoxOrg) return 0;
      ChangeText((char*)lpText); ChangeText((char*)lpCaption);
      int nResult = (*p_fnMessageBoxOrg)(hWnd, lpText, lpCaption, uType);
      return nResult;
} /*MyMessageBox(HWND, LPCTSTR, LPCTSTR, UINT)*/

//============================================================================
extern "C" int WINAPI WinMain(

      HINSTANCE hInstance,  // handle to current instance
      HINSTANCE /*hPrevInstance*/,  // handle to previous instance
      LPSTR szCmdLine,  // pointer to command line
      int /*nCmdShow*/  // show state of window

){
      char* c_szTitle = "API Call Interception";
      UINT uStyle = MB_OK | MB_ICONHAND | MB_SYSTEMMODAL;

      MessageBox(NULL, "Here it is normal                 ", c_szTitle, uStyle);

      // Lets Change it
      //----------------
      InterceptDllCall(
            hInstance, "user32.dll", "MessageBoxA",
            (PVOID)&MyMessageBox, (PVOID*)&p_fnMessageBoxOrg,
            NULL
      ) /*InterceptDllCall*/;

      MessageBox(NULL, "Beware of the mad hackers         ", c_szTitle, uStyle);

      InterceptDllCall(
            hInstance, "user32.dll", "MessageBoxA",
            (PVOID)p_fnMessageBoxOrg, NULL, (PVOID)MyMessageBox
      ) /*InterceptDllCall*/;

      MessageBox(NULL, "Here it is almoust normal again :)", c_szTitle, uStyle);

      return 0;
} /*WinMain(HINSTANCE, HINSTANCE, LPSTR, int)*/


0
 
LVL 3

Expert Comment

by:xyu
ID: 1413149
Don't forget to check that DLL is loaded :)
0
 
LVL 2

Expert Comment

by:duneram
ID: 1413150
hope that's what your looking for.  I am starting my first vacation in over 2 years...  so I won't be around for about a week.  I never found my source code but I know its on one of my cd's...


0
 

Author Comment

by:lordx
ID: 1413151
Your answer is almost exactly what I am looking for.  The code you submitted works great for API functions in DLL's of the current process but not other processes.  I need to intercept the API calls to other processes as well.  The top of the code lising refrences the CreateRemoteThread function which may work for some NT applications.  I need this to work on both NT and 95/98.  For example, if I have a program running that is displaying messageboxes (or calling any other API functions like "socket" or "connect" for winsock functions), I need this program to alter those messageboxes in the other program.  There are some places in the InterceptDllCall function that reference an HMODULE or HINSTANCE object but if I pass the hInstance of another window to the function it fails.


0
 
LVL 3

Expert Comment

by:xyu
ID: 1413152
HMODULE or HINSTANCE here is not a window.... it is instance of the DLL (Module) I'm working now on program (NT service) that You will be able to configure to inject any specified DLL in all precesses runninng after the service started... than i You can use the DLL to intercept API calls (unforunately I can't put too much time on it now - I have deadline to release the version at place i'm working...) if You could wait ... in 1-2 weeks i plan to finish that server... if not - i can transfer to You GetAdmin (hehe) sources that doing something similar... and You can try to use it to build server by yourself.. if not to take general case it could be faster :)
0
 
LVL 4

Expert Comment

by:agreen
ID: 1413153
xyu: i'm intersting in the GetAdmin sources. Can you send it to green@maxho.com ?
0
 
LVL 3

Expert Comment

by:xyu
ID: 1413154
agreen:
 Wait for 1-2 weeks i'll release the service i talk about with sources...
0
 
LVL 4

Expert Comment

by:agreen
ID: 1413155
xyu:
Ok, I'll be waiting. Please don't forget it and don't leave me with only the thing that your nick means :)
0
 
LVL 3

Expert Comment

by:xyu
ID: 1413156
agreen: How do You know, what my nick is... (meanwile its a shame, but choose it while i was totally drunk but I don't want to change it  :) )

0
 
LVL 4

Expert Comment

by:agreen
ID: 1413157
xyu: :))
So, am I right that your nick came from the russian language ?
Also I think that now you live in Israel and, probably, work at Mercury. This place isn't a chat room, so if you want to know the reasons for such conclusion just mail me. (green@MAXHO.com) :)
0
 
LVL 3

Expert Comment

by:xyu
ID: 1413158
to lordx: OK... i have very nice dll to inject any custom dll into specified process and that the custom dll can use method described in my answer to intercept any of the API or other DLL calls... it works now under NT4 and it suppose to work under WIN98... so if You need it leave me a comment... so I'll publish it... (it called syringe.dll) Next week I plan to publish "plaguesrv.exe" that will be NT service to perform "global infection" :)
leave comments here to contact me
0
 
LVL 20

Expert Comment

by:Madshi
ID: 1413159
xyu,

that sounds very interesting...
Would be very kind of you to eMail me your dlls, drivers, sources, or whatever (Mathias.Rauen@gmx.de).    :-)

Thanx a lot, Madshi.
0
 
LVL 3

Expert Comment

by:xyu
ID: 1413160
Madshi... only after 850 points :)
0
 
LVL 20

Expert Comment

by:Madshi
ID: 1413161
Ohhh that's bad!    :-(
I'll think about it...
0
 
LVL 3

Expert Comment

by:xyu
ID: 1413162
Madshi... Good luck :)
0
 

Author Comment

by:lordx
ID: 1413163
Thanks.  I do need it.  Either publish it here or email me at lordx@wam.umd.edu.
0
 
LVL 3

Expert Comment

by:xyu
ID: 1413164
lordx: do You want it as is or You can wait ro plaguesrv "Global Infector"? If You want it as is You have just to promise not to publish it until i'll release plaguesrv :) are You agree?
0
 

Author Comment

by:lordx
ID: 1413165
How about you send me what you have (source included I hope) so I can start integrating it into my program.  Once I see that it works I will grade the question so you get your points.  When you publish plaquesrv, email me a copy at lordx@wam.umd.edu.  I will not release my program that will use it until after you have published plaquesrv.
0
 
LVL 3

Expert Comment

by:xyu
ID: 1413166
lords... i sent it to You alreade 2-3 days ago... How is it going....?
0
 

Author Comment

by:lordx
ID: 1413168
I am having a problem compiling the code with psapi.lib.  I am using Borland C++ 5.02 and it reports that the file (psapi.lib) has an "invalid OMF record, type 0x21".  The code u provided seems to compile fine, but I can't link samp1.  Since I am not using MS Development Studio (according to the make files you used that) I don't know for sure what format to compile each file to.  Currently I am doing this:
samp1_inj: samp1_inf.dll
syringe: syringe.dll
samp1: samp1.exe
I do realize that you had not completed the project to the point of publication when you sent it to me.  Maby I need to compile psapi.lib under Borland.  I have a version of psapi.cpp from the getadmin program but it didn't compile properly.  Where did u get the psapi.lib file you sent me?
I will try to post a comment later on if I get it working.  Do you have ICQ?  If so it may be easier to do an online chat to discuss the program.
0
 
LVL 3

Expert Comment

by:xyu
ID: 1413169
lordx...
You can find psapi.dll at winnt\system32...
Use implib.exe utility from borland C to generate psapi.lib... (the version i sent to You is in VC5 lib format)...
don't forget to disable all optimizations...
Wait a second... i'll generate for You BC5.01 project...



0
 

Author Comment

by:lordx
ID: 1413170
Ok, I got it to compile and run.  (I had to use IMPLIB.EXE for Borland to convert psapi.dll
and syringe.dll to an importable library which created a psapi.lib and syringe.lib that I
could use).
How can I see that it is working?

It looks like I should see the "Hacked" messagebox when samp1_inj.dll is started but I don't.

So, I remarked the "ifdef NEVER" and "endif" statements in samp1_inj.cpp and syringe.cpp so it would always display those message boxes.
When I run samp1.exe:
- I get the "Attach" and "Detatch" messageboxs for samp1_inj.dll and syringe.dll linking to
samp1.exe.  
- I get the "Attach" message box for syringe.dll linking to notepad.exe.
- I don't see any changes in notepad.  I did a search in notepad for text that was not in
the window to have it display a search error messagebox, but the text was not modified.
- When I exit notepad I get the "Detatch" message for syringe.dll's link to notepad.exe

So it is attaching the DLL, but maby not running the HackMessageBox entry function that
makes the modifications.
0
 
LVL 3

Expert Comment

by:xyu
ID: 1413171
lordx... my ICQ is 6478563 call me now....
and my email is ok@netvision.net.il

0
 

Author Comment

by:lordx
ID: 1413172
Thanks for the code.  It worked perfectly once I added the "_" to the HackMessageBox string in the call to InjectModule (in samp1.cpp).
If you could make the neccessary changes for it to work under win98 I would give you another 200 points.
0
 
LVL 3

Expert Comment

by:xyu
ID: 1413173
lordx.. i'm working on it ....
0
 
LVL 2

Expert Comment

by:lucidity
ID: 1413174
lordx/xyu, I have download this intercept demo and got it to work in 98. I was curious if any of you have gotten it to work for Winsock functions. I have tried to capture send but nothing yet.
0
 
LVL 2

Expert Comment

by:lucidity
ID: 1413175
Another thing, This API call interception only seems to work locally within the app. Can this be adapted to catch ALL API calls?
0
 
LVL 3

Expert Comment

by:xyu
ID: 1413176
lucidity... yes it can be adapted... and its easy... You just have to get SYRINGE.DLL written by me and use it :)
0
 
LVL 2

Expert Comment

by:lucidity
ID: 1413177
And how shall I go about doing that? Are you willing to give me the source.. actually I don't need the source, just how to implement it, but the source would be nice. We could trade... I have some source for some stuff I think is cool and potentially useful:

RAW TCP/IP send accept....
Hide process from taskmanager....
Transparent windows like Norton Utilities....
Multi-Threaded Web Server....
File Monitor...

Pick one, Anyway, If it works for me I would owe you one, just name it. I need to be able to see and alter calls to winsock, I'm making a free fuzzy logic web page screener so I need to see the html text and what sites they are going to, maybe GetHostByName(). I would of course credit you for the code.

ciao,
Jason
0
 
LVL 3

Expert Comment

by:xyu
ID: 1413178
lucidity: my E-mail is ok@netvision.net.il what about Yours?
0
 
LVL 2

Expert Comment

by:lucidity
ID: 1413179
liquidy@geocities.com
0
 
LVL 2

Expert Comment

by:lucidity
ID: 1413180
you got the right email but I haven't heard anything from you yet.... whats up?

0
 
LVL 20

Expert Comment

by:Madshi
ID: 1413181
xyu,

I'm back again...  :-)

I still don't have 850 points. So I tried your little source example, converted it to Delphi and got it to work. Also now I'm able to inject other processes.
But there is one big problem: Your code works only with implicit loaded DLLs. Am I right? Is there a possibility to intercept calls to explicit loaded DLLs/functions, too? Of course I could just overwrite the DLL function with a jump to my function. But then I couldn't call the original function anymore...

Thanx for your answer...

Regards, Madshi.

P.S: You don't have to tell me here and now HOW it works. Of course I would be glad about that. I just want to know IF you know how to get this done. Then I'll post a question with some points (<850).  :-)
0
 
LVL 3

Expert Comment

by:xyu
ID: 1413182
You can intrecept any API (e.g. DLL calls) there is an extendet sample that allows You to "infect" notepad.exe  :)

0
 
LVL 20

Expert Comment

by:Madshi
ID: 1413183
xyu,

if you look at the "Import Table" of "notepad.exe", you'll see that notepad links all the DLLs implicitly. But please look at "Explorer.Exe". There the "Import Table" is empty. So the way your example works it just can't intercept api calls from the explorer. I've tested it. I've injected the explorer with my own little DLL, but your sources just don't find the api calls in the import table. The loops run through without finding something...
But I want to intercept the CopyFile (e.g.) api calls that the explorer does. So - if your extended sample does not use a completely different way of intercepting, it will not work! Or am I wrong?

Regards, Madshi.
0
 
LVL 3

Expert Comment

by:xyu
ID: 1413184
Explorer.exe is not a problem... i'v intercepted MessageBox for test :) the intreception have to be performed for appropriate (or all) module :) it's easy.... more than that i'll release Plague package next month... so You'll be able to use it for free :)
0
 
LVL 20

Expert Comment

by:Madshi
ID: 1413185
xyu,

that sounds good (especially that word "free" :-))...
Where can I get this Plaque package from? Will it come with sources?

Thank you,
regards, Madshi.
0
 
LVL 20

Expert Comment

by:Madshi
ID: 1413186
xyu,

and me again...
Thanx for the tip with the modules. I only looked through the exe's import table. Now I'm scanning through all DLLs, too. And now your loops intercept "CopyFileA" in "Shell32.DLL". But somehow the explorer's copy actions are still not hooked... :-(
So the same question again: What if a process uses LoadLibrary/GetProcAddress directly ("explicit" linking)? Then the jump address is not in the locations you look through! Right?

BTW, in your sources is a little bug! Perhaps you corrected that already, perhaps not - because it has no serious effect. But it's not nice, though.

if ( !IsBadWritePtr( (LPVOID)pThunk->u1.Function, sizeof(DWORD)
) /*!IsBadWritePtr*/ ...

That's wrong! It should be like this:

if ( !IsBadWritePtr( (LPVOID)(&pThunk->u1.Function), sizeof(DWORD)
) /*!IsBadWritePtr*/ ...

Regards, Madshi.
0
 
LVL 3

Expert Comment

by:xyu
ID: 1413187
I'm not sure it is a bug.... otherwise You will not be able to compile it... -> is not defined for void* :)

0
 
LVL 20

Expert Comment

by:Madshi
ID: 1413188
xyu,

I AM QUITE SURE!  :-)
Nevertheless it works with this bug in most cases, too. This IsBadWritePtr statement tests a wrong pointer and thus returns always that the pointer is bad. If you look at the "dwProcess" value, returned from the VirtualQuery function (if it succeeds), you'll find that you have already read/write access. I moved the second VirtualQuery line directly under the first one. And it all still worked! Please take a closer look at that!

Regards, Madshi.

P.S: I'm not sure about that compiling issue, because I'm using Delphi. But it's clear somehow. You give in the address that is stored in ...u1.Function. But later you don't want to write the the address that is stored in ...u1.Function but you want to write to ...u1.Function itself.
0
 
LVL 3

Expert Comment

by:xyu
ID: 1413189
ok... it accepted as a bug... sorry

0
 
LVL 3

Expert Comment

by:xyu
ID: 1413190
Madshi, meanwile my mail is ok@netvision.net.il :)
See You :)
Good Luck :)
0
 
LVL 2

Expert Comment

by:lucidity
ID: 1413191
I cannot get your syringe demo to work on anything but NT. is this right?
0
 
LVL 3

Expert Comment

by:xyu
ID: 1413192
lucidity: yes it's right, because CreateRemoteThread existsonly on NT and Win98, ans VistualAllocEx works only with NT, nut now i'm in process of creating my own AllocRemote that will work on Win98 as well...
0
 
LVL 2

Expert Comment

by:lucidity
ID: 1413193
keep me posted, if you get it to work let me know... If you need any help let me know.
0

Featured Post

Announcing the Most Valuable Experts of 2016

MVEs are more concerned with the satisfaction of those they help than with the considerable points they can earn. They are the types of people you feel privileged to call colleagues. Join us in honoring this amazing group of Experts.

Question has a verified solution.

If you are experiencing a similar issue, please ask a related question

If you have ever found yourself doing a repetitive action with the mouse and keyboard, and if you have even a little programming experience, there is a good chance that you can use a text editor to whip together a sort of macro to automate the proce…
What my article will show is if you ever had to do processing to a listbox without being able to just select all the items in it. My software Visual Studio 2008 crystal report v11 My issue was I wanted to add crystal report to a form and show…
This is Part 3 in a 3-part series on Experts Exchange to discuss error handling in VBA code written for Excel. Part 1 of this series discussed basic error handling code using VBA. http://www.experts-exchange.com/videos/1478/Excel-Error-Handlin…
This video shows how to use Hyena, from SystemTools Software, to bulk import 100 user accounts from an external text file. View in 1080p for best video quality.

856 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question