<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged reverse-engineering</title>
<link>https://coderlegion.com/tag/reverse-engineering</link>
<description>Powered by Question2Answer</description>
<item>
<title>Reverse-Engineering NVIDIA: Changing a register operand</title>
<link>https://coderlegion.com/28618/reverse-engineering-nvidia-changing-a-register-operand</link>
<description>&lt;h3 id=&quot;motivation&quot;&gt;Motivation&lt;/h3&gt;
&lt;p&gt;In the &lt;a rel=&quot;nofollow&quot; href=&quot;../nvidia-reverse-engineer-gpu-code&quot;&gt;previous article&lt;/a&gt; I have&lt;br&gt;
started reverse-engineering a simple CUDA binary by modifying the value of&lt;br&gt;
a pointer variable.&lt;/p&gt;
&lt;p&gt;After identifying where the value was stored and successfully running the modified CUDA binary, it was time to find out what the corresponding assembly instructions actually looked like at the machine-code level.&lt;/p&gt;
&lt;h3 id=&quot;finding-the-immediate-size&quot;&gt;Finding the immediate size&lt;/h3&gt;
&lt;p&gt;Now, to be able to determine the size and format of the instruction (or at&lt;br&gt;
least the largest constant the instruction from the previous article can hold),&lt;br&gt;
I started to increase the value word-by-word. Instead of simple &lt;code&gt;0xcc&lt;/code&gt; from the&lt;br&gt;
last time, I did:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;lt;stdio.h&amp;gt;

__global__ void test(int *a) {
    a[0] = 0xecc;
}

int main(int argc, const char* argv[]) {
    cudaError_t err;
    int *a;
    cudaMallocManaged(&amp;amp;a, sizeof(int));
    *a = 0x01;
    test&amp;lt;&amp;lt;&amp;lt;1, 1&amp;gt;&amp;gt;&amp;gt;(a);
    if ((err = cudaGetLastError()) != cudaSuccess)
        fprintf(stderr, &quot;launch: %s\n&quot;, cudaGetErrorString(err));

    cudaDeviceSynchronize();
    printf(&quot;*a = %x\n&quot;, *a);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And yes, it was there:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ xxd -e out/test-old.sm_50.cubin | grep ecc
00000690: ecc7f000 01000000 05070002 4c980780   ...............L
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now to try a bit larger value like '0xdecc':&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ xxd -e out/test-old.sm_50.cubin | grep decc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Searching for the complete &lt;code&gt;0xdecc&lt;/code&gt; pattern didn't find anything. Searching for&lt;br&gt;
the original &lt;code&gt;0xecc&lt;/code&gt; did, however, and the surrounding bytes had changed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ xxd -e out/test-old.sm_50.cubin | grep ecc
00000690: ecc7f000 0100000d 05070002 4c980780   ...............L
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And there it was. If we compare this line with the hex dump from above, we can&lt;br&gt;
see that we have an extra &lt;code&gt;d&lt;/code&gt; in &lt;code&gt;0100000d&lt;/code&gt;. So I tried increasing the word&lt;br&gt;
size in &lt;code&gt;xxd&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ xxd -e -g 8 out/test-old.sm_50.cubin | grep decc
00000690: 0100000decc7f000 4c98078005070002   ...............L
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the bytes looked much more like a structured &lt;code&gt;64&lt;/code&gt;-bit value rather than an&lt;br&gt;
arbitrary sequence of bytes. The next thing to do was simply increase the test&lt;br&gt;
value and see how much the apparent immediate field could hold.&lt;br&gt;
The result with the largest value was:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ xxd -e -g 8 out/test-old.sm_50.cubin | grep ffffffff
00000690: 010ffffffff7f000 4c98078005070002   ...............L
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After that, it was impossible to fit more into an &lt;code&gt;int&lt;/code&gt; value as &lt;code&gt;nvcc&lt;/code&gt;&lt;br&gt;
complained about truncation. I naturally tried to find out what will&lt;br&gt;
happen if I use &lt;code&gt;uint64_t&lt;/code&gt; instead of &lt;code&gt;int&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ xxd -e -g 8 out/test-old.sm_50.cubin | grep ffffffff -C1
00000680: 001fc400fe4007f1 010eeeeeeee7f004   ..@.............
00000690: 010ffffffff7f005 eedd200000070204   ............. ..
000006a0: 001f9c00fde007ef 50b0000000070f00   ...............P
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So we discovered something. We now had two apparent &lt;code&gt;MOV&lt;/code&gt;s and I wasn't sure&lt;br&gt;
whether they differed because they targeted different registers or because they&lt;br&gt;
loaded the high and low parts of a value.&lt;/p&gt;
&lt;h3 id=&quot;adding-a-static-variable&quot;&gt;Adding a static variable&lt;/h3&gt;
&lt;p&gt;After seeing how the &lt;code&gt;MOV&lt;/code&gt; behaved while changing the value, I tried a little&lt;br&gt;
experiment. I added one more variable with a lot of bytes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;lt;stdint.h&amp;gt;

__device__ uint64_t x = 0xdaec;

__global__ void test(uint64_t *a) {
        x = 0xccccccccdddddddd;
        a[0] = 0xaaaaaaaabbbbbbbb;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After looking for my usual patterns, I got:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ xxd -e -g 8 out/test-old.sm_50.cubin | grep bbbb -B5 -A7
00000780: 001fc400fe2007f6 4c98078000870001   .. ............L
00000790: 010000000007f002 010000000007f003   ................
000007a0: 001fc000fe4007f1 010dddddddd7f008   ..@.............
000007b0: 010cccccccc7f009 4c98078005070004   ...............L
000007c0: 001fc400fc2007f1 eedd200000070208   .. .......... ..
000007d0: 4c98078005170005 010bbbbbbbb7f006   .......L........
000007e0: 001fbc00fe2007f2 010aaaaaaaa7f007   .. .............
000007f0: eedd200000070406 50b0000000070f00   ..... .........P
00000800: 001ffc00fc4007ef 50b0000000070f00   ..@............P
00000810: 50b0000000070f00 e30000000007000f   .......P........
00000820: 001f8000fc0007ff e2400fffff07000f   ..............@.
00000830: 50b0000000070f00 50b0000000070f00   .......P.......P
00000840: 000000000000daec 0000000000000000   ................
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Looking at this we can filter some of these out. We can filter by patterns I&lt;br&gt;
created. Also, I noticed that the line beginning with &lt;code&gt;0xeed&lt;/code&gt; appeared a lot&lt;br&gt;
in my dumps. Here it appeared two times, so I filtered useful stuff out:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ xxd -c 8 -e -g 8 out/test-old.sm_50.cubin | grep 'bbbb\|cccc\|aaaa\|dddd\|eedd'
000007a8: 010dddddddd7f008   ........
000007b0: 010cccccccc7f009   ........
000007c8: eedd200000070208   ..... ..
000007d8: 010bbbbbbbb7f006   ........
000007e8: 010aaaaaaaa7f007   ........
000007f0: eedd200000070406   ..... ..
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The interesting part was that the final byte changed systematically as I introduced different values. This made it look increasingly likely that the final byte represented a register operand.&lt;/p&gt;
&lt;p&gt;This is current test output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ ./test-old 
*a = aaaaaaaabbbbbbbb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Can we make it display the value of variable &lt;code&gt;x&lt;/code&gt;? Let's try hacking &lt;code&gt;0xeed2&lt;/code&gt;&lt;br&gt;
instructions. My hypothesis was that the low bits of these instructions encode&lt;br&gt;
the register operand, similarly to what we observed in the &lt;code&gt;0x10&lt;/code&gt; lines:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ xxd -c 8 -e -g 8 test-old | grep eedd2
0008a848: eedd200000070208   ..... ..
0008a870: eedd200000070406   ..... ..
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I used &lt;code&gt;bvi&lt;/code&gt; to change the final byte of the second instruction from &lt;code&gt;06&lt;/code&gt; to&lt;br&gt;
&lt;code&gt;08&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;xxd -c 8 -e -g 8 test-old | grep eedd2
0008a848: eedd200000070208   ..... ..
0008a870: eedd200000070408   ..... ..
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And (not) surprisingly, I got:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ ./test-old
*a = ccccccccdddddddd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We actually switched registers from which the value for &lt;code&gt;a[0]&lt;/code&gt; was loaded.&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;For now we can identify (and modify to a certain extent) two instructions:&lt;/p&gt;
&lt;p&gt;The first instruction appears to be a &lt;code&gt;MOV&lt;/code&gt;-like instruction, with a format that&lt;br&gt;
currently looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;010VVVVVVVV7f00R
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;VVVVVVVV&lt;/code&gt; appears to represent an immediate value, while &lt;code&gt;R&lt;/code&gt; appears to&lt;br&gt;
identify the destination register.&lt;/p&gt;
&lt;p&gt;The second instruction appears to select a register containing the value that is&lt;br&gt;
ultimately written to &lt;code&gt;a[0]&lt;/code&gt;. Its encoding currently appears to be:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;eedX200000070Y0R
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;where &lt;code&gt;R&lt;/code&gt; appears to identify the register. &lt;code&gt;X/Y&lt;/code&gt; remain unknown; I suspect one&lt;br&gt;
of them encodes information about the operand size, since I observed different&lt;br&gt;
values when experimenting with 32-bit &lt;code&gt;int&lt;/code&gt; versus 64-bit &lt;code&gt;uint64_t&lt;/code&gt; values.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28618/reverse-engineering-nvidia-changing-a-register-operand</guid>
<pubDate>Fri, 25 Sep 2026 14:41:55 +0000</pubDate>
</item>
<item>
<title>Reverse-Engineering NVIDIA: Modifying a CUDA binary</title>
<link>https://coderlegion.com/26347/reverse-engineering-nvidia-modifying-a-cuda-binary</link>
<description>&lt;h3 id=&quot;motivation-1&quot;&gt;Motivation&lt;/h3&gt;
&lt;p&gt;For quite a while I wanted to find out how NVIDIA really works under the hood.&lt;br&gt;
One of the greatest unknowns for me was how GPU code is compiled, packaged and&lt;br&gt;
loaded. So I accepted my own challenge: let's try and decode machine code&lt;br&gt;
running on GPU.&lt;/p&gt;
&lt;h3 id=&quot;test-code&quot;&gt;Test code&lt;/h3&gt;
&lt;p&gt;I needed to start really simple and to get as much data as possible. My first&lt;br&gt;
CUDA program to try and decipher was simply:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;lt;stdio.h&amp;gt;

__global__ void test(int *a) {
        a[0] = 0x00;
}

int main(int argc, const char* argv[]) {
        cudaError_t err;
        int *a;
        cudaMallocManaged(&amp;amp;a, sizeof(int));
        *a = 0x01;
        test&amp;lt;&amp;lt;&amp;lt;1, 1&amp;gt;&amp;gt;&amp;gt;(a);
        if ((err = cudaGetLastError()) != cudaSuccess)
                fprintf(stderr, &quot;launch: %s\n&quot;, cudaGetErrorString(err));

        cudaDeviceSynchronize();
        printf(&quot;*a = %x\n&quot;, *a);

        return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I won't get into too many details here (I am not a CUDA expert), but essentially, the function marked with &lt;code&gt;__global__&lt;/code&gt; is the kernel that will run on the device. The &lt;code&gt;cudaMallocManaged()&lt;/code&gt; function takes care of allocating memory that can be accessed by both the host and the device, saving us some additional memory-management code. The &lt;code&gt;test&amp;lt;&amp;lt;&amp;lt;1, 1&amp;gt;&amp;gt;&amp;gt;(a)&lt;/code&gt; syntax launches the &lt;code&gt;test&lt;/code&gt; kernel with one block containing one thread, while the CUDA runtime takes care of loading the kernel for execution on the GPU. Finally, &lt;code&gt;cudaDeviceSynchronize()&lt;/code&gt; waits for the GPU to finish executing the kernel, ensuring that the result written to &lt;code&gt;a&lt;/code&gt; is available when we access it from the host.&lt;/p&gt;
&lt;h4 id=&quot;build&quot;&gt;Build&lt;/h4&gt;
&lt;p&gt;I also created a reusable build command that will also give me something to&lt;br&gt;
start with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;nvcc -arch=sm_50 -lineinfo --keep --keep-dir out -o test test.cu
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note: for this to work, the &lt;code&gt;out&lt;/code&gt; directory needs to be created before running&lt;br&gt;
&lt;code&gt;nvcc&lt;/code&gt; compiler. Now I got a folder full of really useful files:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;stjepan@stjepan-Aspire-F5-573G:~/Develop/NVIDIA$ ls out/
test.cpp1.ii    test.cudafe1.cpp     test_dlink.fatbin    test_dlink.reg.c        test.fatbin.c   test.ptx
test.cpp4.ii    test.cudafe1.gpu     test_dlink.fatbin.c  test_dlink.sm_50.cubin  test.module_id  test.sm_50.cubin
test.cudafe1.c  test.cudafe1.stub.c  test_dlink.o         test.fatbin             test.o
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;test.sm_50.cubin&lt;/code&gt; file sounded like exactly what I was looking for:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;stjepan@stjepan-Aspire-F5-573G:~/Develop/NVIDIA$ file out/test.sm_50.cubin 
out/test.sm_50.cubin: ELF 64-bit LSB executable, NVIDIA CUDA architecture,, statically linked, not stripped
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;first-hack&quot;&gt;First hack&lt;/h3&gt;
&lt;p&gt;Now I wanted to try changing the value of &lt;code&gt;a&lt;/code&gt; in the &lt;code&gt;test&lt;/code&gt; function and see how&lt;br&gt;
&lt;code&gt;cubin&lt;/code&gt; file will behave. I had no idea what I was dealing with, so I started&lt;br&gt;
checking the file with random small values to get the one with least repetitions&lt;br&gt;
in the CUDA binary. After some random attempts I realized that the best one is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;stjepan@stjepan-Aspire-F5-573G:~/Develop/NVIDIA$ hexdump out/test.sm_50.cubin | grep bc
0000680 07f1 fde0 bc00 001f 02ff 0007 2000 eedc
00008f0 0000 0000 0000 0000 04bc 0000 0000 0000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So I decided to change my test function to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;__global__ void test(int *a) {
        a[0] = 0xbc;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If everything went well, I would see an additional line here containing my &lt;code&gt;bc&lt;/code&gt;.&lt;br&gt;
The reason I started with really small values is because I had no idea what kind&lt;br&gt;
of byte ordering I will encounter. After rebuilding the binary, I now got:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;stjepan@stjepan-Aspire-F5-573G:~/Develop/NVIDIA$ hexdump out/test.sm_50.cubin | grep bc
0000310 f1bc 06d4 02fc 0000 0209 0000 0000 0000
0000690 f000 0bc7 0000 0100 0002 0507 0780 4c98
00006a0 07f2 fe20 bc00 001f 0003 0517 0780 4c98
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Well, I definitely got a bit more different hex dump. So I tried just changing&lt;br&gt;
it again, this time to &lt;code&gt;0xcc&lt;/code&gt; (just randomly) to see what I will get. And I got:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;stjepan@stjepan-Aspire-F5-573G:~/Develop/NVIDIA$ hexdump out/test.sm_50.cubin | grep cc
0000690 f000 0cc7 0000 0100 0002 0507 0780 4c98
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This was promising. Notice that this is the same as address &lt;code&gt;0x0000690&lt;/code&gt; in the&lt;br&gt;
previous run, except we got &lt;code&gt;0cc7&lt;/code&gt; instead of &lt;code&gt;0bc7&lt;/code&gt;. Now I could try and find&lt;br&gt;
this kind of pattern in the real binary:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;stjepan@stjepan-Aspire-F5-573G:~/Develop/NVIDIA$ hexdump test | grep 0cc7
000cc70 528b 4830 e789 5c89 1024 c748 2444 0008
008a700 0001 0087 0780 4c98 f000 0cc7 0000 0100
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can see our value on address &lt;code&gt;0x008a700&lt;/code&gt;. Right now, if we run the binary,&lt;br&gt;
we get:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;stjepan@stjepan-Aspire-F5-573G:~/Develop/NVIDIA$ ./test 
*a = cc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, I used &lt;code&gt;bvi&lt;/code&gt; to quickly find the address and changed &lt;code&gt;0cc7&lt;/code&gt; to &lt;code&gt;0dd7&lt;/code&gt;.&lt;br&gt;
So the result was:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;stjepan@stjepan-Aspire-F5-573G:~/Develop/NVIDIA$ hexdump test | grep 0dd7
000dd70 01bb 0000 e900 ff76 ffff 0f66 441f 0000
008a700 0001 0087 0780 4c98 f000 0dd7 0000 0100
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Well, hopefully, I didn't corrupt the binary. When I tried running it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;stjepan@stjepan-Aspire-F5-573G:~/Develop/NVIDIA$ ./test 
*a = dd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bingo. We didn't recompile the CUDA source, regenerate the CUBIN, or even touch&lt;br&gt;
the host executable's source code. We changed bytes inside the compiled binary,&lt;br&gt;
and the GPU executed the modified instruction.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final thoughts&lt;/h3&gt;
&lt;p&gt;We managed to modify code running on the GPU without recompiling it. More&lt;br&gt;
importantly, we have found what appears to be an instruction field containing&lt;br&gt;
our constant. Given what the kernel does, it is likely part of a &lt;code&gt;mov&lt;/code&gt;-like&lt;br&gt;
instruction, although we haven't decoded it yet.&lt;/p&gt;
&lt;p&gt;So the next question is obvious: what is the real encoding of the instruction?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26347/reverse-engineering-nvidia-modifying-a-cuda-binary</guid>
<pubDate>Sun, 06 Sep 2026 18:41:28 +0000</pubDate>
</item>
</channel>
</rss>