{"article":{"slug":"calling-a-function-in-c-without-naming-it","title":"Calling a function in C without naming it","subtitle":null,"summary":"A student describes getting shell access through a school's automated C grading system, which bans calling execve by name. Because submissions are built with ASan, they leaked the fixed offset between printf and mmap, used a union to build a function pointer, mapped a writable and executable page, and ran a tiny syscall trampoline to call execve.","content_type":"blog_post","language":"en","canonical_url":"https://wiro.world/posts/calling-c-func-without-naming-it/","author":{"name":null,"url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"wiro.world","url":"https://wiro.world/","listing_slug":null,"listing":null},"topics":[{"name":"Security","slug":"security","url":"https://listedarticles.com/topics/security"},{"name":"C","slug":"c","url":"https://listedarticles.com/topics/c"},{"name":"Systems Programming","slug":"systems-programming","url":"https://listedarticles.com/topics/systems-programming"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":1558,"reading_minutes":7,"published_at":"2026-10-05T00:00:00.000Z","added_at":"2026-10-08T02:11:27.912Z","updated_at":"2026-10-08T02:11:27.912Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/calling-a-function-in-c-without-naming-it","markdown_url":"https://listedarticles.com/articles/calling-a-function-in-c-without-naming-it.md","example":false,"citation":"wiro.world. \"Calling a function in C without naming it.\" 5 Oct 2026. https://wiro.world/posts/calling-c-func-without-naming-it/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://wiro.world/posts/calling-c-func-without-naming-it/"},"body_markdown":"# Calling a function in C without naming it\n\n05 October 2026\n\nMy school has a controlled remote code execution environment to automatically grade the code we submit. The system checks execution, verifies proper formatting and surely has other checks not shown to the user (e.g. a code plagiarism detection tool[1](#gconf-plagiarism)). The interface reports results mismatches and compilation errors.\n\nA friend and I started to search for a way to have a single solution that could solve every exercise. The first step in that direction was to have a primitive that could give us arbitrary shell code execution. We assumed there would be an `sh` binary on the grading [*VM*](https://en.wikipedia.org/wiki/Virtual_machine) such that we could simply call [`execve`](https://en.wikipedia.org/wiki/Exec_(system_call)).\n\nSo... are we done? You guessed it, no.\n\nRemember when I told that the grading system had other sanity checks? Well, one of them is to check that you only use functions allowed in the subject of the exercise. And `execve` is never one of them, so it fails the submission before your code even gets to run. From what we experienced, the system is a little more involved than a simple `grep` of the source code. My guess is that it uses `clang` to pre-process and parse the code and then does its checks against the resulting [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree).\n\nThis is where we get to the core of the problem. We need to call `execve` but we are not allowed to name the symbol. So how can we do it?\n\nZooming back a bit, we know that our C code essentially gets compiled to a binary and calling a function boils down to a jump to some place in memory. Could we not retrieve the address of `execve` in memory, hardcode it in our program and call it directly?\n\nIn modern laptop software, memory addresses are often execution-dependent. This happens because modern kernels implement [*Address Space Layout Randomization* (ASLR)](https://en.wikipedia.org/wiki/Address_space_layout_randomization) which randomizes addresses at which your binary, its libraries, heap and stack are placed. Nowadays, C code compiles using the `-pie` flag by default, this creates a [*position-independent executable*](https://en.wikipedia.org/wiki/Position-independent_code#Position-independent_executables) and means that your binary can indeed take advantage of *ASLR*. We could use the `-no-pie` C compiler flag to disable this behavior and force fixed addresses for functions in our binary. This doesn't help because we most often do not control the build process in our case. Second, the `execve` function is not directly part of our executable but dynamically loaded, so it still is at a random place.\n\nStatic addresses won't work. But *ASLR* only acts on the mapping of the segments, their content is left intact. Inside a segment, functions are still ordered in the same way. If we manage to know the static offset between the memory addresses of two functions fA and fB, obtaining the address of fA allows us to construct the address of fB and vice-versa.\n\nThe offset varies from a machine to another because of a different version of the C library, or maybe a different architecture, or maybe for some other reason. There are multiple ways to obtain the fixed offset between two functions on the target machine. One of them is to just leak the offset in an exercise where you can name symbols of both fA and fB.\n\nOur target function is still `execve` which is part of `libc`. We assume we always have access to `printf`. We just have to leak the offset between the two functions on the VM. Let's test this locally first:\n\n```\nprintf(\"%td\", (ptrdiff_t)((size_t)execve - (size_t)printf));\n```\n\nRunning the program a couple of times gives us different results. Didn't we just say that the offset was fixed? Let's open the binary to investigate.\n\n```\n$ objdump --dynamic-syms ./offset | grep -E \"\\s(execve|printf)\"\n0000000000000000      DF *UND*\t0000000000000000 (GLIBC_2.2.5) execve\n000000000004dd64  w   DF .text\t0000000000000005  Base        printf\n```\n\n`objdump` is useful for any kind of object file analysis. The `--dynamic-syms` option gives information about each symbol that will be loaded at runtime. The sixth column gives us a bit of information about where the symbol comes from. `execve` indeed comes from `glibc`, which is the implementation of the C standard library available on my laptop. But `printf` doesn't seem to come from `glibc`. Actually, the raw output of `objdump` has a bunch of `__interceptor_*` symbols which gives a clue on what's going on here.\n\nSince the beginning, I have been compiling all my code with [ASan](https://en.wikipedia.org/wiki/Code_sanitizer) (a.k.a `-fsanitize=address`) to catch memory errors early. *ASan* works by replacing a bunch of code, including memory reads and write, by a bunch of slow function calls that check the correctness of each action. It also replaces a bunch of functions in the C standard library like `malloc` or `printf`. By doing so, these functions do not live in the `glibc` object anymore as opposed to functions that aren't replaced, in our case `execve`. The automatic grader system also happens to check for memory errors and compiles with *ASan*.\n\nWe could try to use a different symbol than `printf` that lives in the same segment as `execve`, but we still need a symbol that is allowed most of the time. Most of these symbols live in the *ASan* replaced segment, so let's commit to it. Maybe `execve` is not the only way. Looking for other useful symbols in this segment, `mmap` catches my eye.\n\nWhy is `mmap` special? Because of the [W^X (write-xor-execute)](https://en.wikipedia.org/wiki/W%5EX) security policy, there is no section of memory that is writable and executable at the same time. Thus we cannot just write raw x86 instructions in a buffer and jump to it as if it was a function nor can we overwrite an existing function with our own code. But `mmap` solves this problem because we can just tell it to give us a page that has write and execute permissions.\n\nBut let's not get ahead of ourselves. We can leak the offset between `printf` and `mmap` on the VM via another exercise where we can name both symbols. To use the offset we could use a bunch of casts but the code we hand to the grading system has to compile with `-pedantic` and this disallows cast from, to and between function pointers types. Because function pointers are still pointers in the end, we can try to modify it without the compiler noticing. I started to modify memory based on unoptimized stack slots[2](#stack-snippet) but I was reminded that in C, *unsafety is a feature*, so we can actually just use unions.\n\n```\nunion notmmap_build {\n  int (*basefn)(const char *);\n  size_t addr;\n  void *(*fn)(void *addr, size_t length, int prot, int flags, int fd,\n              long offset);\n};\n\nunion notmmap_build notmmap = {.basefn = printf};\nnotmmap.addr += OFFSET; // whatever offset you leaked earlier\n```\n\nWe can just make a bad x86 assembly routine that lets us make Linux syscalls as if it was a C function. This routine allows us to call any syscall with up to 3 arguments.\n\n```\n000000000040106a <notsyscall>:\n  40106a:\t48 89 f8             \tmov    %rdi,%rax\n  40106d:\t48 89 f7             \tmov    %rsi,%rdi\n  401070:\t48 89 d6             \tmov    %rdx,%rsi\n  401073:\t48 89 ca             \tmov    %rcx,%rdx\n  401076:\t0f 05                \tsyscall\n  401078:\tc3                   \tret\n```\n\nCopy those bytes into a newly mapped writable and executable page.\n\n```\nunsigned char instructions[] = {\n  0x48, 0x89, 0xf8, 0x48, 0x89, 0xf7, 0x48, 0x89,\n  0xd6, 0x48, 0x89, 0xca, 0x0f, 0x05, 0xc3,\n};\n\nchar *code =\n  notmmap.fn(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC,\n             MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);\n```\n\nAnd call our routine as if nothing happened. You can check [chromium syscall docs](https://www.chromium.org/chromium-os/developer-library/reference/linux-constants/syscalls/) for the arguments Linux expects.\n\n```\nchar *argv[] = {\n    \"/bin/sh\",\n    \"-c\",\n    \"echo 'Hello, World!'\",\n    NULL,\n};\n\nunion notsyscall_build {\n  char *ptr;\n  // execve is syscall 59 on x86-64 Linux\n  int (*notexecve)(int sys, char *path, char **argv, char **envp);\n};\n\nunion notsyscall_build notsyscall = {.ptr = code};\n\nnotsyscall.notexecve(59, argv[0], argv, NULL);\n```\n\nRunning it on the target VM successfully prints `Hello, World!`.\n\nYou can find the finished proof-of-concept [on GitHub](https://github.com/mrnossiom/learning-asm/tree/802b8374e24bde9f699cc6c88520ed13e0a245fa/execve).\n\n# Conclusion\n\nBecause we know our code is compiled with `ASan`, we chose to leak the offset between `printf`, a function that is always allowed, and `mmap`, a function that gives us arbitrary assembly code execution. We can thus bypass the school's checks and call any syscall, in our case `execve` to get shell access.\n\nWe did not investigate further and simply reported this possible issue to the school.\n\nI see no trivial way of patching this. As we've seen *ASLR* is not going to help here. One possible idea would be to relink the C standard library with a different order for each execution, randomizing the functions order each time (OpenBSD does that at boot time [here](https://isopenbsdsecu.re/mitigations/libc_symbols_randomization/)). But, you have to keep in mind that *ASan* still replaces functions with its own (so it doesn't mitigate anything here).\n\nThe exploit is not that complicated but it was fun to understand the whole process by ourselves. It's in these kind of moment that all the rabbit holes you followed finally click together and help you move forward. Also the simplicity of the exploit resides in the fact that we already have quite liberal remote code execution (as a feature). Still, it involves *ASLR* bypass and requires a fair amount of understanding of what is going on.\n\n---\n\n1\n\nYou can [watch a talk](https://www.youtube.com/watch?v=kc5P3ep3ViQ) about it (in French) from the research lab of the school.\n\n2\n\nEnjoy this [snippet](https://godbolt.org/z/7oxb1PxsY).\n","body_html":"<h1 id=\"calling-a-function-in-c-without-naming-it\">Calling a function in C without naming it</h1>\n<p>05 October 2026</p>\n<p>My school has a controlled remote code execution environment to automatically grade the code we submit. The system checks execution, verifies proper formatting and surely has other checks not shown to the user (e.g. a code plagiarism detection tool<a href=\"#gconf-plagiarism\">1</a>). The interface reports results mismatches and compilation errors.</p>\n<p>A friend and I started to search for a way to have a single solution that could solve every exercise. The first step in that direction was to have a primitive that could give us arbitrary shell code execution. We assumed there would be an <code>sh</code> binary on the grading <a href=\"https://en.wikipedia.org/wiki/Virtual_machine\" rel=\"nofollow ugc noopener\"><em>VM</em></a> such that we could simply call <a href=\"https://en.wikipedia.org/wiki/Exec_(system_call)\" rel=\"nofollow ugc noopener\"><code>execve</code></a>.</p>\n<p>So... are we done? You guessed it, no.</p>\n<p>Remember when I told that the grading system had other sanity checks? Well, one of them is to check that you only use functions allowed in the subject of the exercise. And <code>execve</code> is never one of them, so it fails the submission before your code even gets to run. From what we experienced, the system is a little more involved than a simple <code>grep</code> of the source code. My guess is that it uses <code>clang</code> to pre-process and parse the code and then does its checks against the resulting <a href=\"https://en.wikipedia.org/wiki/Abstract_syntax_tree\" rel=\"nofollow ugc noopener\">AST</a>.</p>\n<p>This is where we get to the core of the problem. We need to call <code>execve</code> but we are not allowed to name the symbol. So how can we do it?</p>\n<p>Zooming back a bit, we know that our C code essentially gets compiled to a binary and calling a function boils down to a jump to some place in memory. Could we not retrieve the address of <code>execve</code> in memory, hardcode it in our program and call it directly?</p>\n<p>In modern laptop software, memory addresses are often execution-dependent. This happens because modern kernels implement <a href=\"https://en.wikipedia.org/wiki/Address_space_layout_randomization\" rel=\"nofollow ugc noopener\"><em>Address Space Layout Randomization</em> (ASLR)</a> which randomizes addresses at which your binary, its libraries, heap and stack are placed. Nowadays, C code compiles using the <code>-pie</code> flag by default, this creates a <a href=\"https://en.wikipedia.org/wiki/Position-independent_code#Position-independent_executables\" rel=\"nofollow ugc noopener\"><em>position-independent executable</em></a> and means that your binary can indeed take advantage of <em>ASLR</em>. We could use the <code>-no-pie</code> C compiler flag to disable this behavior and force fixed addresses for functions in our binary. This doesn&#39;t help because we most often do not control the build process in our case. Second, the <code>execve</code> function is not directly part of our executable but dynamically loaded, so it still is at a random place.</p>\n<p>Static addresses won&#39;t work. But <em>ASLR</em> only acts on the mapping of the segments, their content is left intact. Inside a segment, functions are still ordered in the same way. If we manage to know the static offset between the memory addresses of two functions fA and fB, obtaining the address of fA allows us to construct the address of fB and vice-versa.</p>\n<p>The offset varies from a machine to another because of a different version of the C library, or maybe a different architecture, or maybe for some other reason. There are multiple ways to obtain the fixed offset between two functions on the target machine. One of them is to just leak the offset in an exercise where you can name symbols of both fA and fB.</p>\n<p>Our target function is still <code>execve</code> which is part of <code>libc</code>. We assume we always have access to <code>printf</code>. We just have to leak the offset between the two functions on the VM. Let&#39;s test this locally first:</p>\n<pre><code>printf(&quot;%td&quot;, (ptrdiff_t)((size_t)execve - (size_t)printf));</code></pre>\n<p>Running the program a couple of times gives us different results. Didn&#39;t we just say that the offset was fixed? Let&#39;s open the binary to investigate.</p>\n<pre><code>$ objdump --dynamic-syms ./offset | grep -E &quot;\\s(execve|printf)&quot;\n0000000000000000      DF *UND*    0000000000000000 (GLIBC_2.2.5) execve\n000000000004dd64  w   DF .text    0000000000000005  Base        printf</code></pre>\n<p><code>objdump</code> is useful for any kind of object file analysis. The <code>--dynamic-syms</code> option gives information about each symbol that will be loaded at runtime. The sixth column gives us a bit of information about where the symbol comes from. <code>execve</code> indeed comes from <code>glibc</code>, which is the implementation of the C standard library available on my laptop. But <code>printf</code> doesn&#39;t seem to come from <code>glibc</code>. Actually, the raw output of <code>objdump</code> has a bunch of <code>__interceptor_*</code> symbols which gives a clue on what&#39;s going on here.</p>\n<p>Since the beginning, I have been compiling all my code with <a href=\"https://en.wikipedia.org/wiki/Code_sanitizer\" rel=\"nofollow ugc noopener\">ASan</a> (a.k.a <code>-fsanitize=address</code>) to catch memory errors early. <em>ASan</em> works by replacing a bunch of code, including memory reads and write, by a bunch of slow function calls that check the correctness of each action. It also replaces a bunch of functions in the C standard library like <code>malloc</code> or <code>printf</code>. By doing so, these functions do not live in the <code>glibc</code> object anymore as opposed to functions that aren&#39;t replaced, in our case <code>execve</code>. The automatic grader system also happens to check for memory errors and compiles with <em>ASan</em>.</p>\n<p>We could try to use a different symbol than <code>printf</code> that lives in the same segment as <code>execve</code>, but we still need a symbol that is allowed most of the time. Most of these symbols live in the <em>ASan</em> replaced segment, so let&#39;s commit to it. Maybe <code>execve</code> is not the only way. Looking for other useful symbols in this segment, <code>mmap</code> catches my eye.</p>\n<p>Why is <code>mmap</code> special? Because of the <a href=\"https://en.wikipedia.org/wiki/W%5EX\" rel=\"nofollow ugc noopener\">W^X (write-xor-execute)</a> security policy, there is no section of memory that is writable and executable at the same time. Thus we cannot just write raw x86 instructions in a buffer and jump to it as if it was a function nor can we overwrite an existing function with our own code. But <code>mmap</code> solves this problem because we can just tell it to give us a page that has write and execute permissions.</p>\n<p>But let&#39;s not get ahead of ourselves. We can leak the offset between <code>printf</code> and <code>mmap</code> on the VM via another exercise where we can name both symbols. To use the offset we could use a bunch of casts but the code we hand to the grading system has to compile with <code>-pedantic</code> and this disallows cast from, to and between function pointers types. Because function pointers are still pointers in the end, we can try to modify it without the compiler noticing. I started to modify memory based on unoptimized stack slots<a href=\"#stack-snippet\">2</a> but I was reminded that in C, <em>unsafety is a feature</em>, so we can actually just use unions.</p>\n<pre><code>union notmmap_build {\n  int (*basefn)(const char *);\n  size_t addr;\n  void *(*fn)(void *addr, size_t length, int prot, int flags, int fd,\n              long offset);\n};\n\nunion notmmap_build notmmap = {.basefn = printf};\nnotmmap.addr += OFFSET; // whatever offset you leaked earlier</code></pre>\n<p>We can just make a bad x86 assembly routine that lets us make Linux syscalls as if it was a C function. This routine allows us to call any syscall with up to 3 arguments.</p>\n<pre><code>000000000040106a &lt;notsyscall&gt;:\n  40106a:    48 89 f8                 mov    %rdi,%rax\n  40106d:    48 89 f7                 mov    %rsi,%rdi\n  401070:    48 89 d6                 mov    %rdx,%rsi\n  401073:    48 89 ca                 mov    %rcx,%rdx\n  401076:    0f 05                    syscall\n  401078:    c3                       ret</code></pre>\n<p>Copy those bytes into a newly mapped writable and executable page.</p>\n<pre><code>unsigned char instructions[] = {\n  0x48, 0x89, 0xf8, 0x48, 0x89, 0xf7, 0x48, 0x89,\n  0xd6, 0x48, 0x89, 0xca, 0x0f, 0x05, 0xc3,\n};\n\nchar *code =\n  notmmap.fn(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC,\n             MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);</code></pre>\n<p>And call our routine as if nothing happened. You can check <a href=\"https://www.chromium.org/chromium-os/developer-library/reference/linux-constants/syscalls/\" rel=\"nofollow ugc noopener\">chromium syscall docs</a> for the arguments Linux expects.</p>\n<pre><code>char *argv[] = {\n    &quot;/bin/sh&quot;,\n    &quot;-c&quot;,\n    &quot;echo &#39;Hello, World!&#39;&quot;,\n    NULL,\n};\n\nunion notsyscall_build {\n  char *ptr;\n  // execve is syscall 59 on x86-64 Linux\n  int (*notexecve)(int sys, char *path, char **argv, char **envp);\n};\n\nunion notsyscall_build notsyscall = {.ptr = code};\n\nnotsyscall.notexecve(59, argv[0], argv, NULL);</code></pre>\n<p>Running it on the target VM successfully prints <code>Hello, World!</code>.</p>\n<p>You can find the finished proof-of-concept <a href=\"https://github.com/mrnossiom/learning-asm/tree/802b8374e24bde9f699cc6c88520ed13e0a245fa/execve\" rel=\"nofollow ugc noopener\">on GitHub</a>.</p>\n<h1 id=\"conclusion\">Conclusion</h1>\n<p>Because we know our code is compiled with <code>ASan</code>, we chose to leak the offset between <code>printf</code>, a function that is always allowed, and <code>mmap</code>, a function that gives us arbitrary assembly code execution. We can thus bypass the school&#39;s checks and call any syscall, in our case <code>execve</code> to get shell access.</p>\n<p>We did not investigate further and simply reported this possible issue to the school.</p>\n<p>I see no trivial way of patching this. As we&#39;ve seen <em>ASLR</em> is not going to help here. One possible idea would be to relink the C standard library with a different order for each execution, randomizing the functions order each time (OpenBSD does that at boot time <a href=\"https://isopenbsdsecu.re/mitigations/libc_symbols_randomization/\" rel=\"nofollow ugc noopener\">here</a>). But, you have to keep in mind that <em>ASan</em> still replaces functions with its own (so it doesn&#39;t mitigate anything here).</p>\n<p>The exploit is not that complicated but it was fun to understand the whole process by ourselves. It&#39;s in these kind of moment that all the rabbit holes you followed finally click together and help you move forward. Also the simplicity of the exploit resides in the fact that we already have quite liberal remote code execution (as a feature). Still, it involves <em>ASLR</em> bypass and requires a fair amount of understanding of what is going on.</p>\n<hr />\n<p>1</p>\n<p>You can <a href=\"https://www.youtube.com/watch?v=kc5P3ep3ViQ\" rel=\"nofollow ugc noopener\">watch a talk</a> about it (in French) from the research lab of the school.</p>\n<p>2</p>\n<p>Enjoy this <a href=\"https://godbolt.org/z/7oxb1PxsY\" rel=\"nofollow ugc noopener\">snippet</a>.</p>","headings":[{"level":1,"text":"Calling a function in C without naming it","id":"calling-a-function-in-c-without-naming-it"},{"level":1,"text":"Conclusion","id":"conclusion"}]}}