HANDLE proc = OpenProcess(PROCESS_VM_READ, FALSE, <PID>);
ReadProcessMemory(proc, <address_in_other_program>, buffer, 4096, &bytesRead);
I'm leaving out a lot of stuff here - for example, error checking to determine whether you are actually able to open the target process with the VM_READ permission - but it really is that simple. Virtual memory doesn't mean one process cannot access another's memory, it just means that each process' address space is (by default) their own, and if you want to access somebody else's, you have to ask for it.
Windows, by default, allows all processes running as the same user and with the same integrity level to get read access to each others' memory (higher-integrity processes can also read lower-integrity ones, though not vice versa). A process can change its own ACL, or that of any other ACL running with the same or strictly lesser privileges; this can be used to grant a process running under a different user access, if for some reason you wanted to do that. Administrators (and anybody else with the SeDebugPrivilege) can also access the memory of any other process (except for a few that have special "don't debug me!" flags); this is why a debugger launched as Admin can debug (almost) anything.
Note that this code sample also elides two important steps: getting the other process' PID, and getting the address you want to read from. The PID is pretty easy (they're not especially secret, and processes running as the same user can be easily enumerated by anybody), but getting the base address is trickier. It can be done, using debug symbols or just XORing an expected base address with the OS' current ASLR mask (which is the same for all processes running on the machine, though it changes with each reboot), but you do have to put some effort into it. If you request an address that isn't mapped in the target process, the
ReadProcessMemory call will just fail.
As a side note, the APIs are Windows-specific but the concept isn't. Every OS that supports debugging has a way to read the memory of another process. This very much includes Linux, MacOS, etc. Sometimes the default permissions on a inter-process memory access are different, but it's always possible for a sufficiently-privileged process.