• There is NO official Otland's Discord server and NO official Otland's server list. The Otland's Staff does not manage any Discord server or server list. Moderators or administrator of any Discord server or server lists have NO connection to the Otland's Staff. Do not get scammed!
  • 2026 staff recruitment is open! Check it out and consider applying!

C++ Real Tibia BUG (Vocation on ROOKGAARD)

TibiCAM

Veteran OT User
Joined
Feb 3, 2020
Messages
214
Reaction score
295
Someone was able to get a vocation in Rookgaard on real Tibia by supposedly creating an application that invokes a function on the address that handles the vocation window. In other words he could make a character and go to Rookgaard, and then call the function that shows the "Select vocation" dialog window, and pick any vocation.

Video:

On Linux, so far I have used Cutter to analyze the game client. It found 493377 strings. When I filter by "vocation" I see many that could potentially be connected to that "Select vocation" dialog window. It finds 183 strings. The first one in the list is this:

Address: 0x0182e640
String: tutorial/VocationSelectionPopup.qml

Further down we can see this:
Address: 0x01a99904
String: VocationCardWithBanner

And many, many more.
When I right-click on one, for example the first address, and select "Show X-Refs" it shows the following:

Code:
0x004aa82c      mov     rdi, qword [rbp - 0x1358]
0x004aa833      call    fcn.01512ec0 ; fcn.01512ec0
0x004aa838      mov     rdi, qword [r14 + 0x4f0]
0x004aa83f      movdqa  xmm4, xmmword [rbp - 0x70]
0x004aa844      movups  xmmword [r14 + 0x4e8], xmm4
0x004aa84c      test    rdi, rdi
0x004aa84f      je      0x4aa856
0x004aa851      call    fcn.00487730 ; fcn.00487730
0x004aa856      mov     rdi, qword [rbp - 0x48]
0x004aa85a      test    rdi, rdi
0x004aa85d      je      0x4aa864
0x004aa85f      call    fcn.00487730 ; fcn.00487730
0x004aa864      mov     rdi, qword [r14 + 0x248]
0x004aa86b      call    fcn.00604430 ; fcn.00604430
0x004aa870      mov     edi, 0x70  ; 'p'
0x004aa875      mov     r12, rax
0x004aa878      mov     rax, qword [r14 + 0x10]
0x004aa87c      mov     qword [rbp - 0x13a0], rax
0x004aa883      call    imp.operator new(unsigned long) ; sym.imp.operator_new_unsigned_long
0x004aa888      mov     rcx, qword [0x0226f108]
0x004aa88f      lea     rsi, [0x0182e640]
0x004aa896      mov     rdi, r13
0x004aa899      mov     rbx, rax
0x004aa89c      lea     rax, [rax + 0x10]
0x004aa8a0      mov     qword [rax - 8], rcx
0x004aa8a4      lea     rcx, [0x02795c58]
0x004aa8ab      mov     qword [rax - 0x10], rcx
0x004aa8af      mov     qword [rbp - 0x13b8], rax
0x004aa8b6      call    fcn.00486980 ; fcn.00486980
0x004aa8bb      mov     rdi, qword [rbp - 0x1328]
0x004aa8c2      xor     edx, edx
0x004aa8c4      mov     rsi, r13
0x004aa8c7      call    imp.QUrl::QUrl(QString const&, QUrl::ParsingMode) ; method.QUrl.QUrl_QString_const___QUrl::ParsingMode
0x004aa8cc      mov     rcx, qword [rbp - 0x1330]
0x004aa8d3      mov     rdx, qword [rbp - 0x13a0]
0x004aa8da      lea     rdi, [rbx + 0x10]
0x004aa8de      mov     rsi, qword [rbp - 0x1328]
0x004aa8e5      call    fcn.014f2a40 ; fcn.014f2a40
0x004aa8ea      mov     rdi, qword [rbp - 0x1328]
0x004aa8f1      call    imp.QUrl::~QUrl() ; method.QUrl._QUrl

Am I now supposed to lookup each function (assuming it's the ones beginning with "fcn.xxxxxxxx") and try invoke that somehow?
I have no idea how to try invoke a function on a specific address via C++, so naturally I used AI like the C++ noob I am, and made this (Sorry for the AI slop code):
C++:
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <sys/mman.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <stdint.h>
// Shellcode to call function and trap (int3) to return control.
// Assembly: push rbp; mov rbp, rsp; call <addr>; int3; (simple, no args)
// Replace <addr> with runtime addr.
// Hex: \x55\x48\x89\xe5\xe8\x00\x00\x00\x00\xcc  (rel call placeholder)
unsigned char shellcode[] = {
    0x55,                   // push rbp
    0x48, 0x89, 0xe5,       // mov rbp, rsp
    0xe8, 0x00, 0x00, 0x00, 0x00,  // call rel32 (placeholder)
    0xcc                    // int3 (breakpoint to catch)
};
size_t shellcode_size = sizeof(shellcode);
// Function to write data to process memory
void poke_data(pid_t pid, uintptr_t addr, const void* data, size_t len) {
    const uint8_t* bytes = (const uint8_t*)data;
    size_t word_size = sizeof(long);
    for (size_t i = 0; i < len; i += word_size) {
        long word = 0;
        memcpy(&word, bytes + i, (len - i < word_size) ? (len - i) : word_size);
        if (ptrace(PTRACE_POKEDATA, pid, addr + i, word) == -1) {
            perror("ptrace(POKEDATA)");
            exit(1);
        }
    }
}
int main(int argc, char* argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <pid> <function_address_hex>\n", argv[0]);
        return 1;
    }
    pid_t pid = atoi(argv[1]);
    uintptr_t func_addr = strtoull(argv[2], NULL, 0);
    // Attach to process
    if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) == -1) {
        perror("ptrace(ATTACH)");
        return 1;
    }
    int status;
    waitpid(pid, &status, 0);
    // Get current registers
    struct user_regs_struct regs, orig_regs;
    if (ptrace(PTRACE_GETREGS, pid, NULL, &regs) == -1) {
        perror("ptrace(GETREGS)");
        return 1;
    }
    orig_regs = regs;
    // Find injectable memory (e.g., use /proc/<pid>/maps to find rw- region; simplify: assume near RIP or allocate via mmap injection)
    // For simplicity, overwrite at current RIP (dangerous! Backup first). Better: Inject into .text or alloc new.
    uintptr_t inject_addr = regs.rip;  // Overwrite current instruction pointer location (pause point)
    // Backup original code at inject_addr
    unsigned char orig_code[shellcode_size];
    for (size_t i = 0; i < shellcode_size; i += sizeof(long)) {
        errno = 0;
        long word = ptrace(PTRACE_PEEKDATA, pid, inject_addr + i, NULL);
        if (errno != 0) {
            perror("ptrace(PEEKDATA)");
            return 1;
        }
        memcpy(orig_code + i, &word, sizeof(long));
    }
    // Patch shellcode with call offset (relative to call instr + 5)
    int32_t call_offset = (int32_t)(func_addr - (inject_addr + 4 + 5));  // call is at offset 4, +5 for instr size
    memcpy(shellcode + 5, &call_offset, sizeof(int32_t));
    // Inject shellcode
    poke_data(pid, inject_addr, shellcode, shellcode_size);
    // Set RIP to inject_addr
    regs.rip = inject_addr;
    if (ptrace(PTRACE_SETREGS, pid, NULL, &regs) == -1) {
        perror("ptrace(SETREGS)");
        return 1;
    }
    // Continue to execute shellcode (calls function, hits int3)
    if (ptrace(PTRACE_CONT, pid, NULL, NULL) == -1) {
        perror("ptrace(CONT)");
        return 1;
    }
    waitpid(pid, &status, 0);  // Wait for int3 (SIGTRAP)
    // Restore original code and registers
    poke_data(pid, inject_addr, orig_code, shellcode_size);
    if (ptrace(PTRACE_SETREGS, pid, NULL, &orig_regs) == -1) {
        perror("ptrace(SETREGS restore)");
        return 1;
    }
    // Detach
    if (ptrace(PTRACE_DETACH, pid, NULL, NULL) == -1) {
        perror("ptrace(DETACH)");
        return 1;
    }
    printf("Function called successfully.\n");
    return 0;
}

I then compile it like so:
g++ tibia_vocation.cpp -o tibia_vocation

And then I launch it by giving it the PID and the address:

For example:
./tibia_vocation 37776 0x01940230

I've tried hundreds of addresses related to functions, but it either crashes the game client or just prints to the console that it successfully called the function (but nothing happens). I've tried going through the C++ code but I'm unsure what I'm doing. I was looking around the forums to see if anyone has any example code that shows how to invoke a function in the real Tibia client using C++, but could not find any.

Would love to see if anyone here has a clue on how to do this "trick" (bug). Thanks!
I don't mind if CipSoft will patch it soon. But this could be a good learning experience for me and others.


Cutter using their AppImage from Github repo:
Bash:
wget https://github.com/rizinorg/cutter/releases/download/v2.4.1/Cutter-v2.4.1-Linux-x86_64.AppImage
sudo chmod +x Cutter-v2.4.1-Linux-x86_64.AppImage
./Cutter-v2.4.1-Linux-x86_64.AppImage

I then set it to analzye the Tibia client file (inside ~/.local/share/CipSoft GmbH/Tibia/packages/Tibia/bin/client)
It took 1-2 minutes and then it revealed all Strings in the game client.
 
Last edited:
A lesson to never trust the client. Why did they not verify/validate server-side? ... crazy.

If you're experienced with C++, could you try see if you can get it working?

I'm unsure which address points to the function. And I'm unsure how to properly invoke it via C++

I've added info about how I used Cutter to get the Strings in the game client

Specifically it's this dialog I want to launch, which is triggered when a character on Newhaven (not Rookgaard) walks into the teleport.

dialog.webp
 
Ahh I see! I remember their old Dawnport tutorial island, they had specific tiles that you walked on to switch vocation (they split the island up into 4 quarters for each voc). So I guess this is the same, but they have this choice dialog pop up instead in Newhaven?

I now understand how it could happen but, it's crazy to not have any server-side checks.

I am not in a position at the moment to test this but I am sure someone will try and abuse this somehow lol! xD
 
Ahh I see! I remember their old Dawnport tutorial island, they had specific tiles that you walked on to switch vocation (they split the island up into 4 quarters for each voc). So I guess this is the same, but they have this choice dialog pop up instead in Newhaven?

I now understand how it could happen but, it's crazy to not have any server-side checks.

I am not in a position at the moment to test this but I am sure someone will try and abuse this somehow lol! xD

Exactly. Newhaven is the new beginner island, and they removed Dawnport in November last year. On Newhaven you step through a portal and get to choose your vocation. That's when this dialog shows up. But there's a NPC you can speak to, to get to Rookgaard. He went to Rookgaard first and then programmatically triggered this dialog.

It's insane they haven't made any server side checks for this. They also remade the entire NPC chat in that update and added a popup window/dialog to chat with NPCs instead. Possibly more bugs to be discovered through that one. They might have rushed things to get them into the Winter Update.

I haven't made any progress yet. I am clueless when it comes to this type of stuff. But from my understanding, there is a function that opens the "Select Vocation" popup. I just need to figure out its referenced address and then send a signal to it via an external app. OtLand has a lot of talented Tibia programmers. I hope someone can come up with something. It would be tun to test it out while it still works!

I also tried the "TibiaAPI" by jo3bingham, but when I used the "Record" app it only output the client version. I did not see any addresses, so it's most likely deprecated(?) as it's been quite a while since it was last updated.

cut1.webp

cut2.webp
Post automatically merged:

People over on Reddit are trying to figure it out. So far it seems only one guy from Brazil has succeeded. Who else will manage to do it? Lol
 
Last edited:
There we go. I can manage to open the dialog but as soon as it opens the game client crashes. Perhaps he is sending the vocation straight away, instead of manually clicking the button on the vocation he chose. But I suck at this type of stuff.
 
Last edited:
There we go. I can manage to open the dialog but as soon as it opens the game client crashes. Perhaps he is sending the vocation straight away, instead of manually clicking the button on the vocation he chose. But I suck at this type of stuff.
Hello,

You made it with TibiaApi ?
 
There we go. I can manage to open the dialog but as soon as it opens the game client crashes. Perhaps he is sending the vocation straight away, instead of manually clicking the button on the vocation he chose.
Interesting! Yeah, probably as you said, he's skipping the dialog completely and just sending the choice directly.

I saw this
and people have some wild theories in the comments lol

I hope you are able to figure it out! Make a video and post it here if so, I would love to see it!
 
I'm certain he is sending the vocation directly. I think it might be a number. E.g. "0" is like druid, "1" is knight, or something like that. And he most definitely has a script e.g. "./setVocation <pid> <vocationID>". I have gotten no further progress. I've used TibiaAPI but didn't get it working. I've used WireShark but couldn't get it working either, all traffic is encrypted and I don't know if there's a way to decrypt it. Cutter has worked best for me so far.

With Cutter I was able to find addresses of strings inside the game client. And using the "X-Refs" tool inside Cutter I could go through the code line by line and just guess where the referenced functions are. They are not far away from where the text strings are used. And I'm using ChatGPT and Grok to help me, as I know absolutely nothing about this type of low level stuff.

The furthest I get is to make the vocation dialog pop-up, but without the images of the 5 different vocations. It's like a skeleton of the dialog. I see its frame and buttons, but no images. And the client crashes directly after it. I have no idea how to go from here and I think I'm looking at the wrong place. I believe the right way is to send the vocation ID directly.

I wish someone more skilled joined in here and tried. I'm sure OTLand has many people who knows how to do it.
 
I'm certain he is sending the vocation directly. I think it might be a number. E.g. "0" is like druid, "1" is knight, or something like that. And he most definitely has a script e.g. "./setVocation <pid> <vocationID>". I have gotten no further progress. I've used TibiaAPI but didn't get it working. I've used WireShark but couldn't get it working either, all traffic is encrypted and I don't know if there's a way to decrypt it. Cutter has worked best for me so far.

With Cutter I was able to find addresses of strings inside the game client. And using the "X-Refs" tool inside Cutter I could go through the code line by line and just guess where the referenced functions are. They are not far away from where the text strings are used. And I'm using ChatGPT and Grok to help me, as I know absolutely nothing about this type of low level stuff.

The furthest I get is to make the vocation dialog pop-up, but without the images of the 5 different vocations. It's like a skeleton of the dialog. I see its frame and buttons, but no images. And the client crashes directly after it. I have no idea how to go from here and I think I'm looking at the wrong place. I believe the right way is to send the vocation ID directly.

I wish someone more skilled joined in here and tried. I'm sure OTLand has many people who knows how to do it.
Just for curiosity, you managed to make the vocation selection screen appear, but the client crashes. You think you could make it disappear/disable when the game pops the vocation selecton screen up?
 
Just for curiosity, you managed to make the vocation selection screen appear, but the client crashes. You think you could make it disappear/disable when the game pops the vocation selecton screen up?

I don't see the point in that. If you don't want a vocation on Newhaven you can just have someone else push your character 3-4sqm when the vocation dialog appears. Then press the "X" on Tibia window, and choose "Logout", and log back in. Then you have no vocation on Newhaven.

However, as soon as you step near that teleporter area, it will appear again. Same with the "temple" (shrine) in the town.

As a non-vocation player on Newhaven, you can't exit the city though. But you can get to level 5 by turning in a book at the NPC.
 
I don't see the point in that. If you don't want a vocation on Newhaven you can just have someone else push your character 3-4sqm when the vocation dialog appears. Then press the "X" on Tibia window, and choose "Logout", and log back in. Then you have no vocation on Newhaven.

However, as soon as you step near that teleporter area, it will appear again. Same with the "temple" (shrine) in the town.

As a non-vocation player on Newhaven, you can't exit the city though. But you can get to level 5 by turning in a book at the NPC.
That's the point... If it's possible you can pass to the hunting ground with no vocation. But I don't know if would work, beacuse the character is also teleported.
 
May have possibly been patched today as they had downtime for the second day in a row.

Well, only one way to find out
Post automatically merged:

The new packet is 0x6E with a single byte value for the vocation ID (e.g., Knight = 1).

Edit: I should mention this is the packet the client sends to the server.

How would you do this with a C++ or C# application? I struggle to find any good examples of how this is done (to send a packet). Any time I've tried the game client ends up crashing. Would love a good example by you if you have the time. Thank you!

I love how hundreds of players in Tibia have been trying to figure out the bug on Reddit, OTLand, YouTube, and then @jo3bingham shows up out of nowhere and is like "Here you go guys!" hahah. You're a Godsend.
 
Last edited:
Bem, só há uma maneira de descobrir.
Post automatically merged:



Como você faria isso com um aplicativo em C++ ou C#? Estou com dificuldades para encontrar bons exemplos de como enviar um pacote. Sempre que tento, o cliente do jogo acaba travando. Gostaria muito de um bom exemplo seu, se tiver tempo. Obrigado!

Adoro como centenas de jogadores de Tibia estavam tentando descobrir o bug no Reddit, OTLand, YouTube, e então @jo3bingham aparece do nada e diz "Aqui está, pessoal!" hahaha. Você é um enviado dos céus.
keep us updated with your attempts
 
Back
Top