Just wanted to stop in to say - of course memory reading can be detected. The most obvious way would be for them to hook/watch NtOpenProcess.
That's all. #TheBank2017
Just wanted to stop in to say - of course memory reading can be detected. The most obvious way would be for them to hook/watch NtOpenProcess.
That's all. #TheBank2017
There are ways around that too..
If Jagex ever builds a comprehensive "Anti Cheat" system.
You could run NXT on an OS inside a Hypervisor, and run your Memory reading engine Outside on the host OS. And send emulated mouse & keyboard events to the VM. No way in hell they could ever detect you then... Though it would be quite a lot more complex. Need to understand Extended Page Tables in depth.
I'm planning to try a prototype this against the "Big Guns" at Valve VAC.
Issue #1 : emulated video card
Issue #2 : detection of the VMkernel with a hypervisor approach.
Issue #3 : https://villavu.com/forum/showthread.php?t=115467
But I do like where you're headed. Just needs some refinement. If they are looking for you they will find you no matter what you do. That's why it's a constant game of cat & mouse.
Also, for the mouse or keyboard emutalion a directinput hook can be used.
Per aspera ad Astra!
----------------------------------------
Slow and steady wins the race.
I also noticed the same on VMWare. However, it really depends on the drivers being used.
Detecting the VM is probably the biggest issue here imo. Its pretty hard to hide the fact that you're using a VM. If they're gonna go through the effort of scanning other processes/memory, they might as well just throw in VM detection.
But why is that an issue?
I feel it's like detecting that you are in fact using a PC to play RS in the sense that it doesn't really relate to whether you are botting or not, not sure how strong such a correlation would be.
The poker bots that has been around have always been using a VM to run the client, and the bot on the physical machine. I am not sure if this has changed the past 5 years since when I was looking into it. And these bots fight against pretty good anti-cheating measures.
Last edited by slacky; 10-01-2017 at 02:51 AM.
!No priv. messages please
I Don't mean to derail this post. I'm Fascinated by Alar82's Memory reading techniques! :-)
1. PCI passthrough your Graphics Hardware. I'm currently using it!
2. KVM has a feature called hidden state='on' Which removes virtualization "signatures". Fool guest into thinking it's running native hardware. Xen has even better isolation. People currently do this to get around Nvidia's detection (its against TOS). It's the only way you can install Nvidia's driver. Nvidia Want you to shell out $1000s for Tesla Workstation graphics. A real Anti-Cheat would probably detect you using DMI (firmware) information. But that could be emulated too!
It's also Wise to enable all processor extensions and "nested" virtualization, so CPU appears native.
3. Loved that thread & code! Looking forward to trying it! On a regular Native OS.
I think the Cat & mouse game is quite exciting! Last I checked something like 40% of all Pro eSport Gamers Cheat. ;-) Not that I care about that realm.
Last edited by BotEngines; 10-01-2017 at 02:54 AM.
Do you normally play rs or cs:go through a vm? Why the fuck would anyone bottleneck like that unless the game straight up didn't natively support that OS? Even in that sense. There are ways to detect the underlying OS if not baremetal and check if the game supports it.
I personally don't know anyone that plays games legit on a VM.
Can't speak for poker bots, haven't really looked into them.
The year I was using Linux as my OS, I used a Windows VM alongside it for a lot of stuff (mainly due to photoshop and such tools I could only get on Windows), but then if I had the VM open already I could jump into RS every now and then, and even some CS 1.6 [CS had no native support for Linux at that time].
But just asking me is pointless, I am/was one in a very large playerbase, only jagex, assuming they gather such data have such statistics, and know how it correlates to botting, now I am not saying it's common to use a VM, but there needs to be a solid correlation for it to be worth extra effort needed to figure out if you bot or not, which I am not sure if solely basing of if you use a VM is, or will be enough or not.
But I am gonna get out of this thread, not really interesting for me. Just wanted to point this out.
Last edited by slacky; 10-01-2017 at 03:31 AM.
!No priv. messages please
Sure. If you need the kernel mode emulation, you should use somethin like this: https://github.com/djpnewton/vmulti.
Per aspera ad Astra!
----------------------------------------
Slow and steady wins the race.
It's actually way easier to detect than anything mentioned above..
Scenario: Process A opens Process B in order to get a "HANDLE" to pass to ReadProcessMemory. To detect this, all we need to do is enumerate global list of handles.
1. Process-B will do: NtQuerySystemInformation with SYSTEM_HANDLE_TABLE_ENTRY_INFO.
2. Process-B will Duplicate each handle into itself.
3. Check if that process handle is its own.
4. If step-3 is true, get handle owner (Process-A) and scan it for RPM or hook RPM in that process (hook is nice because you can check if it is reading your process directly).
5. Take measures such as banning or blah..
With decent knowledge of WinAPI, almost anyone can do this and can find other ways too. Note: You don't even need to check handles.. Can do other checks too.
C++ Code:#define DUPLICATE_SAME_ATTRIBUTES 0x00000004
#define STATUS_INFO_LENGTH_MISMATCH 0xC0000004
#define STATUS_BUFFER_OVERFLOW 0x80000005
auto size = sizeof(SYSTEM_HANDLE_INFORMATION);
std::unique_ptr<std::byte> info(new std::byte[size]);
while (NtQuerySystemInformation(SystemHandleInformation, info.get(), size, &size) == STATUS_INFO_LENGTH_MISMATCH)
{
info.reset(new std::byte[size]); //awful.. but I know no other way..
}
SYSTEM_HANDLE_INFORMATION* shi = reinterpret_cast<SYSTEM_HANDLE_INFORMATION*>(info.get());
for (auto i = 0; i < shi->Count; ++i)
{
SYSTEM_HANDLE_ENTRY& sh = shi->Handle[i];
//Multiple options here.. Map the process and check what it does.. OR open it and check for RPM..
HANDLE hObj = nullptr;
HANDLE hProcess = OpenProcess(PROCESS_DUP_HANDLE | PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, sh.OwnerPid);
if (hProcess)
{
if (DuplicateHandle(hProcess, reinterpret_cast<HANDLE>(sh.HandleValue), GetCurrentProcess(), &hObj, STANDARD_RIGHTS_REQUIRED, FALSE, DUPLICATE_SAME_ACCESS))
{
//Scan for RPM or Hook RPM.. if it is reading us and reading structures it shouldn't be reading.. handle it.
CloseHandle(hObj);
}
CloseHandle(hProcess);
}
}
I guess you can use it for garbage and memory leak monitoring too..
P.S. This is how VAC does it as well. But anyway, let's not derail further with "what-ifs".
Last edited by Brandon; 10-01-2017 at 10:03 PM.
I am Ggzz..
Hackintosher
For mouse movements I use currently winapi stuff. There is alot of messages send to app if you look with spy++. But I could write directly into client mouse position, so windows mouse wouldn't be needed at all. Any issues with that?
How do you personally go about finding the vtable pointers (I assume that's what you use) after each update?
I wrote a program, Ulyaoth (C#), to match patterns, then I found reliable patterns:
I can't post my offsets.ini publicly since it contains copyrighted Jagex binary code from the client. Feel free to PM me.Code:using System; using System.Collections.Generic; using Gee.External.Capstone.X86; using IniParser; using IniParser.Model.Configuration; using PeNet; using PeNet.Structures; using Gee.External.Capstone; namespace Ulyaoth { class Pattern { List<int> m_pattern = new List<int>(); int m_offset; public int InstructionOffset => m_offset; public Pattern(string mask_pattern, int offset) { for (int i = 0; i + 1 < mask_pattern.Length; i += 2) { if (mask_pattern[i] == '.' && mask_pattern[i + 1] == '.') { m_pattern.Add(-1); } else { int value; if (Int32.TryParse( mask_pattern.Substring(i, 2), System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out value)) { m_pattern.Add(value); } } } m_offset = offset; } public bool TryMatch(byte[] data, int data_offset, out int result) { for (int i = 0; i + data_offset + m_pattern.Count < data.Length; ++i) { bool match = true; for (int j = 0; j < m_pattern.Count; ++j) { if (!(data[data_offset + i + j] == m_pattern[j] || m_pattern[j] < 0)) { match = false; break; } else { match = true; } } if (match) { result = i + m_offset; return true; } } result = 0; return false; } } class Program { static int Main(string[] arguments) { if (arguments.Length < 2) { Console.Error.WriteLine("ulyaoth <pattern> <executable>"); return 1; } Dictionary<string, Pattern> patterns = new Dictionary<string, Pattern>(); Dictionary<string, int> adjustments = new Dictionary<string, int>(); { var parser = new FileIniDataParser(); var definitions = parser.ReadFile(arguments[0]); foreach (var definition in definitions.Sections) { var mask_pattern = definition.Keys["signature"]; int offset; if (Int32.TryParse( definition.Keys["offset"].Substring(2), System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out offset)) { patterns.Add(definition.SectionName, new Pattern(mask_pattern, offset)); } if (definition.Keys.ContainsKey("adjust")) { var adjustValue = definition.Keys["adjust"]; int adjust; if (Int32.TryParse(adjustValue, out adjust)) { adjustments.Add(definition.SectionName, adjust); } else { adjustments.Add(definition.SectionName, 0); } } else { adjustments.Add(definition.SectionName, 0); } } } var data = System.IO.File.ReadAllBytes(arguments[1]); var executable = new PeFile(data); IMAGE_SECTION_HEADER text = null; foreach (var section in executable.ImageSectionHeaders) { var name = System.Text.Encoding.UTF8.GetString(section.Name).Trim('\0'); if (name == ".text") { text = section; } } if (text == null) { Console.Error.WriteLine("No .text section in executable."); return 1; } foreach (var pattern in patterns) { int offset; if (!pattern.Value.TryMatch(data, (int)text.PointerToRawData, out offset)) { Console.Error.WriteLine("Failed to match pattern {0}.", pattern.Key); Console.Error.WriteLine(); } else { Console.WriteLine("Matched pattern {0} (+{1:X}).", pattern.Key, offset + text.VirtualAddress); int otherMatch = 0; if (pattern.Value.TryMatch(data, (int)text.PointerToRawData + offset + 1, out otherMatch)) { Console.WriteLine("(Ambigious match.)"); } using (var disassembler = CapstoneDisassembler.CreateX86Disassembler(DisassembleMode.Bit64)) { disassembler.EnableDetails = true; disassembler.Syntax = DisassembleSyntaxOptionValue.Intel; var instructions = disassembler.DisassembleStream(data, offset + (int)text.PointerToRawData, text.VirtualAddress); foreach (var instruction in instructions) { Console.WriteLine("{0:X}: \t {1} \t {2}", instruction.Address, instruction.Mnemonic, instruction.Operand); Console.WriteLine("\t Id = {0}", instruction.Id); foreach (var operand in instruction.ArchitectureDetail.Operands) { string operandValue = null; switch (operand.Type) { case X86InstructionOperandType.FloatingPoint: operandValue = operand.FloatingPointValue.Value.ToString("X"); break; case X86InstructionOperandType.Immediate: operandValue = operand.ImmediateValue.Value.ToString("X"); break; case X86InstructionOperandType.Memory: operandValue = "-->"; break; case X86InstructionOperandType.Register: operandValue = operand.RegisterValue.Value.ToString(); break; } Console.WriteLine("\t\t {0} = {1}", operand.Type, operandValue); // Handle Memory Operand. // // ... if (operand.Type == X86InstructionOperandType.Memory) { Console.WriteLine("\t\t\t Base Register = {0} ", operand.MemoryValue.BaseRegister); Console.WriteLine("\t\t\t Displacement = {0:X} ", operand.MemoryValue.Displacement + offset + (int)text.PointerToRawData + instruction.Bytes.Length + 0xC00 + adjustments[pattern.Key]); Console.WriteLine("\t\t\t Index Register = {0}", operand.MemoryValue.IndexRegister); Console.WriteLine("\t\t\t Index Register Scale = {0}", operand.MemoryValue.IndexRegisterScale); Console.WriteLine("\t\t\t Segment Register = {0}", operand.MemoryValue.SegmentRegister); } Console.WriteLine(); } break; } } } } return 0; } } }
To find the offsets, I first found the target offset (e.g., vtable pointers) via Cheat Engine and did an xref to in IDA. From there, I used Radare2 to manually add the method and create the signature:
The format of the INI is like so:Code:af+ <hex offset to method from IDA> <name of function> f afb+ <name of function> 0 <size of function from IDA> zaf <name of function> z
My current offsets.ini works from mid-November to now, and most entries work all the way up to the update back the week of September 11 2017.Code:[entry_name] ; machine code (hexadecimal doublets) with .. as a masking character signature = XX......XXXX ; offset of op-code giving displacement (i.e., offset to value) offset = 0x0 ; adjust (optional): value to add to displacement adjust = 0
The value of "Displacement" gives you of the offset from the base of the executable.
Here's my research into the data structures:
I never began reverse engineering the client since I moved on to other things and stopped playing RuneScape. A good starting point would be the functions that instantiate the aforementioned objects and methods in the vtable.Code:# terms world units: 512x multiple of tile coordinates, usually floating point tile units: aka in-game tile ## types float: 4-byte single-precision floating point number short: 2-byte integer int: 4-byte integer pointer: 8-byte (native word) pointer to object string: EA STL string type # table [npc_table, object_table, player_table] - pool of used/unused objects - NPC/object/players all have tables (*_table in offsets.ini) +0x0000: pointer to pool +0x????: pointer to array of indices for used entities +0x????: pointer to array of indices for unused entities +0x????: number of entities currently used (+0x0008?) # item table ([global_item_table]) 0x0000: pointer to item list 0x0008: list count, int 0x000C: list capacity (?) # item table entry, not virtual - size: ? (didn't write it down) +0x0020: item ID, int +0x0024: quantity, int +0x0090: X (world units), float +0x0094: Y (world units), float +0x0098: Z (world units), float # NPC table entry, virtual [npc_vtable] - size: 0x2000 bytes 0x0128: name, string 0x035c: X (world units), float 0x0360: Y (world units), float 0x0364: Z (world units), float 0x05a7: animation ID (short) 0x0ff0: NPC ID #1 (sometimes different), int 0x0ff0: NPC ID #2 (always correct), int 0x1028: current health, int (? seems too high, maybe wrong) # Player table entry, virtual - size: 0x2000 bytes (?) 0x0128: name, string 0x035c: X (world units), float 0x0360: Y (world units), float 0x0364: Z (world units), float 0x05a7: animation ID (short) 0x1028: current health, int (? seems too high, maybe wrong) # Object table entry, virtual [object_vtable] - size: 0x298 bytes long +0x0134: X (tile units), int +0x0138: Z (tile units), int +0x013c: Y (world units), int +0x0100: pointer to structure +0x0020: object ID (valid for static objects, maybe dynamic) +0x0178: object ID (only valid for dynamic [?] objects) # heap [heap] - size: 0x130 bytes - there's a size field and reserved field somewhere, didn't write it down +0x0000: pointer to arena +0x0044: pointer to free stack head +0x0048: pointer to free stack tail
That's superb coding skills you have there. Hm as I understand you use PeNet library to get sections executable of something erm. Then something-something of disassemble it with Capstone library,..Er.
My thing is much much simple, find pattern, read whats there, although it's limited.
I wonder did you make something useful with it. Bot program of sorts or send data to simba.
It's quite stable now:
Inventory:
MiniMap:
Main issue is that there isn't much of a point to bot rs3. Its just too easy.
Hey guys even if I wanted to put my source up. It needs 2 additional libraries to work, but those are probably copyrighted. Blackbone for memory searching and imgui for debug. Can I zip whole stuff and upload?
https://github.com/DarthTon/Blackbone
https://github.com/ocornut/imgui
Scripts: ClarityNex | ClaritySlayer | ClarityElfThief | ClarityBurialArmour | ClarityMudRunes | ClarityWells | ClarityProTables | ClarityArmadyl | ClarityHarps
ClarityDominonTower | ClarityAltar | ClarityCitadel | ClarityBarrows | ClarityEsswraith | ChampionScrollCamperTools & Extensions: OpenGL ID Highlight Tool | SRL-6 Messaging System | SRL Companion | Item DTM Generator | BBCode Converter
Err forum upload limit is 100kb so mediafire it is.
Source:
It needs visual studio 2015 and windows sdk.
https://github.com/pp9999/MemoryError/tree/master
As source is kinda messy:
https://github.com/pp9999/MemoryError/releases
We can setup github if srl community is interested and fix the code.
Usage for nubs: Put MemoryError.dll into simbas plugins folder, you need simba 64 bit build and rs NXT client not old java.
Simba 64 bit build here:http://l0.lt/builders/master either Simba.x86_64-win64 7z or exe.
Script for RS3 festival area, super boring and 1 click activity.
festivalmining.simba
Simba Code:program Test;
//{$I srl-6/SRL.simba}
{$loadlib MemoryError}
var
Count:Int32;
begin
wait(random(199));
SetupRSReading(True,True);
repeat
wait(240+random(6500));
if (CheckPAnim3) then begin wait(100+random(11500)) end else
begin
Count:=Count+1;
if FindAobj([106662],1,20,0,0,0) then
begin
wait(2410+random(6500));
end;
end;
until(Count>1999)
end.
Log into your throwaway account and run the script.
It should take a minute max to find all needed addresses.
If debug has appeared there are some keys to see if it works correctly:
f1 = ground items
f2 = other players
f6 = inventory
f7 = active objects debug, above script uses this.
f8 = all objects debug
f9 = decor objects
f10 = npcs
With shift key they will be on minimap, no rotation however.
f11 = interfaces mess
f12 = hide always on debug
Page up = imps on minimap
shift+ctrl+f4 = varpbits
Variables order is:Code:FindAobj([106662],1,20,0,0,0)
1st ids separated by commas([106662,1,2,3])
2nd how many ids(1 if 1 in [ ])
3rd range in tiles how far look for (20)
4th and 5th click adjustments in pixels
6th is how to click: 0 left click,1 right click,2 move to,3 return true and do nothing.
Good work @alar82;
I'm curious about OffSets.h
Are these offsets not changed whenever an update changes anything concerning entities. Like for example if they add another attribute to an object, would this not mean that some of these pointer offsets get moved slightly or are they always added to the end? Alternatively does this page update whenever you run your "updater"?
Feel free to ask me any questions, I will do my best to answer them!
Previously known as YouPee.
I'm trying to understand your code. To find things of interest (e.g., NPCs), you:
1) Search the entire address space of the RuneScape process for a specific pattern.
2) When the pattern is matched, you extract relevant data and resume searching the address space from that point forward.
Is that right?
There is a better way of doing that. There is a table of NPCs stored at 0x6E3018. How the game keeps track of which NPCs are valid or not I don't know, but valid NPCs have a vtable pointer set to 0x52D370. (These values are relative to the base address of the process and thus change due to ASLR; they also change every update, but with the pattern matching tool I posted earlier, given the right pattern, the correct offsets can be regenerated without trouble after each update).
Thus, I am able to iterate over the NPCs without much of a performance penalty using ReadProcessMemory:
The same goes for players, objects, and ground items. I've yet to find where the root GUI node/widget(s) are stored, but the heirarchy can be generate from any widget downwards. (I don't think the parent widget is stored).Code:NPC 458 ('Father Urhney'): - location: 3209, 869, 3149 - animation: 65535 NPC 12348 ('Giant rat'): - location: 3218, 555, 3198 - animation: 65535 NPC 16898 ('Ysondria'): - location: 3220, 325, 3183 - animation: 65535 NPC 8828 ('Giant rat'): - location: 3225, 194, 3190 - animation: 65535 NPC 8828 ('Giant rat'): - location: 3238, 799, 3171 - animation: 65535 NPC 8829 ('Giant rat'): - location: 3230, 234, 3182 - animation: 65535
Here's a program you can run to see for yourself:
Enter the PID of the RuneScape 2 NXT client (64-bit only) before the next update. You must be logged in to a world (otherwise it'll try and deference a null pointer and everything fails). It's not very pretty but it was just a proof of concept.Code:using System; using System.Diagnostics; using System.Runtime.InteropServices; namespace Meeseek { class Program { static readonly IntPtr NPC_TABLE = new IntPtr(0x6E3018); static readonly IntPtr NPC_VTABLE = new IntPtr(0x52D370); [DllImport("kernel32.dll", SetLastError = true)] static extern bool ReadProcessMemory( IntPtr hProcess, IntPtr lpBaseAddress, [Out] byte[] lpBuffer, int dwSize, out IntPtr lpNumberOfBytesRead); static int Main(string[] args) { Console.WriteLine("Enter PID:"); int pid = Int32.Parse(Console.ReadLine()); var process = Process.GetProcessById(pid); var executable = process.Modules[0]; var npcTableAddress = new IntPtr(executable.BaseAddress.ToInt64() + NPC_TABLE.ToInt64()); var npcVTableAddress = new IntPtr(executable.BaseAddress.ToInt64() + NPC_VTABLE.ToInt64()); IntPtr numBytesRead; IntPtr npcTablePointer; { byte[] buffer = new byte[8]; ReadProcessMemory(process.Handle, npcTableAddress, buffer, 8, out numBytesRead); if (numBytesRead.ToInt64() < 8) { Console.WriteLine("Woops, didn't read enough bytes (NPC table pointer)!"); return 1; } npcTablePointer = new IntPtr(BitConverter.ToInt64(buffer, 0) + 0x20); } byte[] npcBuffer = new byte[0x10f0]; for (int i = 0; i < 255; ++i) { IntPtr npcPointer = new IntPtr(npcTablePointer.ToInt64() + i * npcBuffer.Length); if (!ReadProcessMemory(process.Handle, npcPointer, npcBuffer, npcBuffer.Length, out numBytesRead)) { Console.WriteLine("Woops, didn't read enough bytes (NPC {0})!", i); return 1; } // EASTL string but most NPC names are short enough IntPtr vtable = new IntPtr(BitConverter.ToInt64(npcBuffer, 0)); if (vtable != npcVTableAddress) { continue; } string name = ""; for (int j = 0; j < 0x20; ++j) { if (npcBuffer[j + 0x128] == 0) { break; } name += Char.ConvertFromUtf32(npcBuffer[j + 0x128]); } int x = (int)(BitConverter.ToSingle(npcBuffer, 0x35c) / 512); int y = (int)(BitConverter.ToSingle(npcBuffer, 0x360)); int z = (int)(BitConverter.ToSingle(npcBuffer, 0x364) / 512); ushort animation = BitConverter.ToUInt16(npcBuffer, 0xd48); int id = BitConverter.ToInt32(npcBuffer, 0xff4); Console.WriteLine("NPC {0} ('{1}'):", id, name); Console.WriteLine("- location: {0}, {1}, {2}", x, y, z); Console.WriteLine("- animation: {0}", animation); } return 0; } } }
Last edited by Kompromaus; 04-25-2018 at 03:18 AM.
Did you know that different nxt executable is loaded based on your operating system windows 7 vs 10.
However I see you take exe modulebase and add NPC_TABLE.
Using that info I came up with these numbers: RS base:13f830000+6E3018=13FF13018
Points to:
ce.jpg
Erm is it right spot?
Looks like random bytes=)
Edit: compiled your program, it gives error "Woops, didn't read enough bytes (NPC {0})!"
Changed bit a code to wait so i could see errors.
Result looks same, magical bytes?Code:using System; using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading; namespace Meeseek { class Program { static readonly IntPtr NPC_TABLE = new IntPtr(0x6E3018); static readonly IntPtr NPC_VTABLE = new IntPtr(0x52D370); [DllImport("kernel32.dll", SetLastError = true)] static extern bool ReadProcessMemory( IntPtr hProcess, IntPtr lpBaseAddress, [Out] byte[] lpBuffer, int dwSize, out IntPtr lpNumberOfBytesRead); static int Main(string[] args) { Console.WriteLine("Enter PID:"); int pid = Int32.Parse(Console.ReadLine()); var process = Process.GetProcessById(pid); var executable = process.Modules[0]; var npcTableAddress = new IntPtr(executable.BaseAddress.ToInt64() + NPC_TABLE.ToInt64()); Console.WriteLine("npcTableAddress:"+npcTableAddress.ToString("X")); var npcVTableAddress = new IntPtr(executable.BaseAddress.ToInt64() + NPC_VTABLE.ToInt64()); Console.WriteLine("npcVTableAddress:" + npcVTableAddress.ToString("X")); IntPtr numBytesRead; IntPtr npcTablePointer; { byte[] buffer = new byte[8]; ReadProcessMemory(process.Handle, npcTableAddress, buffer, 8, out numBytesRead); if (numBytesRead.ToInt64() < 8) { Console.WriteLine("Woops, didn't read enough bytes (NPC table pointer)!"); Thread.Sleep(15000); return 1; } //Console.WriteLine("buffer:" + buffer); npcTablePointer = new IntPtr(BitConverter.ToInt64(buffer, 0) + 0x20); Console.WriteLine("buffer:" + npcTablePointer.ToString("X")); } byte[] npcBuffer = new byte[0x10f0]; for (int i = 0; i < 255; ++i) { IntPtr npcPointer = new IntPtr(npcTablePointer.ToInt64() + i * npcBuffer.Length); if (!ReadProcessMemory(process.Handle, npcPointer, npcBuffer, npcBuffer.Length, out numBytesRead)) { Console.WriteLine("Woops, didn't read enough bytes (NPC {0})!", i); Thread.Sleep(15000); return 1; } // EASTL string but most NPC names are short enough IntPtr vtable = new IntPtr(BitConverter.ToInt64(npcBuffer, 0)); if (vtable != npcVTableAddress) { continue; } string name = ""; for (int j = 0; j < 0x20; ++j) { if (npcBuffer[j + 0x128] == 0) { break; } name += Char.ConvertFromUtf32(npcBuffer[j + 0x128]); } int x = (int)(BitConverter.ToSingle(npcBuffer, 0x35c) / 512); int y = (int)(BitConverter.ToSingle(npcBuffer, 0x360)); int z = (int)(BitConverter.ToSingle(npcBuffer, 0x364) / 512); ushort animation = BitConverter.ToUInt16(npcBuffer, 0xd48); int id = BitConverter.ToInt32(npcBuffer, 0xff4); Console.WriteLine("NPC {0} ('{1}'):", id, name); Console.WriteLine("- location: {0}, {1}, {2}", x, y, z); Console.WriteLine("- animation: {0}", animation); Thread.Sleep(15000); } return 0; } } }
pp.jpg
There are currently 1 users browsing this thread. (0 members and 1 guests)