Defcon CTF 2022: Self-Reflection
I didn’t do well on this Defcon, so this isn’t a writeup, but just to shared what I’ve learned during working on the Defcon, especially on the Luajit chall. I apologize in advance if there is some mistake on this article, as I just learn all of it during the Defcon.
During Defcon, I only work on three challenge with the help of my teammates, which are same_old, hash-it, and smuggler’s cove. We submit the flag for same_old and hash-it, and I spend the whole two days on the smuggler’s cove, but failed to solve it.
On this article, I’ll try to summarize what I learned during working on the chall, my failed attempts on smuggler’s cove, and the correct approach which I know after the CTF ended. I write this for my future notes in case I faced this kind of problem again.
Smuggler’s Cove
Initial Analysis
On this chall, we were given Dockerfile
, cove.c
, dig_up_the_loot.c
, and the binaries of the given source code. Let’s take a look on the Dockerfile
. The server is running the binary of cove.c
.
|
|
So the server was running the most recent of Ubuntu20.04, which got upgraded just on Friday (D-1 Defcon CTF). I think this will help us a lot, as I also use Ubuntu20.04 on my local, and I can simply run update & upgrade to replicate the server environment.
Now, checking the file dig_up_the_loot.c
|
|
Okay, so seems like we need to run ./dig_up_the_loot x marks the spot
in order to retrieve the flag. Let’s move to the main source code (cove.c
), which we should abuse.
|
|
I never knew what is Lua before working on this chall, so during working on this chall:
- I open the Lua documentation
- I clone the Luajit repo
- I debug it with GDB one by one to understand better about it.
Based on the file included header, I assume that we are dealing with LuaJIT instead of baseline lua. Which is why I try to clone the Luajit also to understand some method functionality.
Let’s try to analysis the source code first. We start by reading the main func
|
|
So the main function basically will init some initial state of the luajit (I don’t know yet what is lua state?), and we need to pass an arg (?). Let’s take a look on the run_code
first, so that we know what is the arg
value.
|
|
Ah, so turn out the arg is the lua script filename. And later, run_code
will try to load this code in the memory, and pass it to luaL_dostring
. Notes that there is a limit of the lua script size that we can submit. I assume that luaL_dostring
is method in the Luajit library, which will execute the given code.
Moving back, let’s take a look on what does the init_lua
do.
|
|
I don’t understand at all what this method do as this is the first time I learn Lua. So, what I do is checking the official documentation of Lua. This is what it says about lua_call
API.
Ah okay! So turn out, you need to push the function that you want to call + the args. So the first four lines is trying to to load LUA_JITLIBNAME
library, and then call set_jit_settings
, which set the jit configs. Details of what the config means can be found in here and here.
And then, let see the next code
|
|
Based on the documentation, basically it set global variable jit
to nil. So that means, we can’t use library jit
during passing our code to the binary.
And the last piece of code is
|
|
On this code, what it does is basically, if we call cargo
or print
in our input lua script, the Luajit will call the defined debug_jit
and print
c-function instead. Let’s take a look on the print
method first.
|
|
Nothing special, basically we can pass one lua element, convert it to string, and print it. Now, let’s take a look on the debug_jit
method
|
|
So based on that code, seems like debug_jit
will need 2 arguments. Let’s disass it one by one.
|
|
So, the first argument needs to be a lua function. Reading through the documentation of lua_topointer
What it does is generate a c pointer to the given function.
|
|
And then, we can see that the second argument is stored as offset
with type uint8_t
. So offset will never be a negative value. We will back to what offset
is actually doing later.
We see that from the retrieved pointer v
, we try to get the bytecode, get its op
and index
, and then the index
got passed to getTrace
function. And the result isn’t allowed to be nil.
I don’t know at all what is GCtrace
, and I don’t know how Luajit working. So let’s try to gather some knowledge on what it is, and what the code is doing.
Notes that what a JIT does is usually parsing the source code into their own bytecode, and then execute it. This bytecodes are machine agnostic. And then, some bytecode will be compiled and stored as native machine code as part of JIT optimization. By doing this, JIT can always ensure that it will always optimize the compiled code based on the target machine architecture.
Based on this nice article about Luajit, I learned that Luajit is a tracing-JIT instead of method-JIT. So, instead of optimizing hot method (method those are frequently called or executed), it will record sequence of executing operations frequency, and then only compile sequence which are frequently to be executed (hot traces). Based on wikipedia, tracing JIT assumes that hot loops (frequent loops or repetition of sequential operations) will make program spend a lot of time.
trace is an object which refer to the JIT-Compiled machine code version of the sequence of the bytecodes
Moving back to the given code, now we can deduce more about what it actually does. So, my deduction is, from our chosen function, it will check its bytecode of the function. Based on the Luajit bytecode documentation, there are two formats on how a bytecode stored. We can see in below image, that for function which is JIT-compiled, it will follow the second format. Now, we can understand better on what this snippet code does.
|
|
Basically, what it want to achieve is, it will try to retrieve the trace number (index) of the given function in the debug_jit
arg. And then, it will try to get the trace based on the stored trace’s index. Notes that because Luajit is tracing-JIT, based on my understanding, it is possible that a function didn’t get JIT-Compiled, and it is possible that some sequential operation inside of the method is the one who got traced.
Now, Let’s check the getTrace
method.
|
|
Ah from the code, we can deduce that, in the memory, there are an array of pointer, where each pointer points to a trace object (GCtrace
). In the given binary, basically this method will be used to return the method GCTrace
object if the method got JIT-Compiled. And the trace-index is retrieved from the function header bytecode (which we have retrieved in the previous snippet).
Let’s move to the final code
|
|
To understand it better, let’s check the GCTrace
struct definition from our cloned Luajit repository.
|
|
I think the comment code is already self-explanatory. What the code does is basically check whether:
- The returned
GCTrace
(which ist
) fromgetTrace
function is nil or not - The
mcode
(which is pointer to the start of the compiled machine code) is nil or not - The
szmcode
(which is the size of the compiled machine code) is 0 or not.
Now, the final part is where the offset
that we pass during calling the debug_jit
is used. Turn out, the debug_jit
method will allow us to shift the start of the compiled machine code, with restriction that the shift offset need to be less than the size of the compiled machine code. Finally, we understand what the given binary is actually doing.
Exploitation Plan
To summarize, so basically what the binary do is:
- Initialize a Luajit
- Read our given lua script
- Run it inside Luajit
What make the given binary special is the debug_jit
method, where we can use it to shift the start of our method JIT-compiled machine code (Notes that it only allowed us to shift the trace of Lua function which got JIT-Compiled).
So now, it is clear that, what we need to is somehow, use the debug_jit
method feature, to do RCE and execute command ./dig_up_the_loot x marks the spot
.
Reading this writeup from past CTF gave me a clear idea on how to do it. Basically, what we need to do is to craft a lua method, which the compiled machine code will have controllable constants. For example, what we aim to do is to have the trace machine code contains instruction like this
mov rdi, 0x22eb006a61c18348
cmp ebp, 0x6161616161
- etc
And then, reserve the last 2 byte as our jump instruction to the other constant, and use the remaining bytes to craft our shellcode, which will execute our target command.
Failure
Well, at the end, I failed to solve it during the CTF, because I couldn’t craft any payload which satisfy it. What I found during CTF is that if we able to produce bytecode int LE xxx
, we can generate an int constant. But somehow, I can only produce it by using for
loop, but as we know that Luajit is a tracing-method, it will only traced the for
loop instead of the method that I defined. I use luajit
to help me debugging it.
Above is the only payload that I found during the CTF that able to produce constants, yet I can’t use it because it is not a method trace (The trace is belong to the for loop sequence, not to the method that I defined).
Reflections
After the CTF end, the chall’s author tell me on the trick how to produce the constants. And also I read two great writeups in ctftime (here and here) which is super great.
I missed out that in Lua, every number is by default treated as floating point, so the hex that I should consider inside the compiled machine code is floating-point hex. And turn out, some tricks that we can use to produce constants:
- Define the constant as index of an array. Ex
m[100]=0
- Add
LL
suffix in comparison. Ex:if i == 0xdeadbeefcafebabeLL
I’ve actually tried the index of array trick, but as I mentioned before, my mistake is not treating it as floating point, which make me failed to see the constant during debugging with the luajit
. So when I specify m[0x61616161]=0
, I expect to see 0x61616161
in the compiled machine code, while what the compiled machine code is converting it to floating-point hex representation due to the fact that Lua treat all numbers as floating point by default, which makes me unable to realize that I actually able to produce constant. This is a great lesson for me, as I failed to notice this during working on the chall.
So after reading the writeup, I managed to try to implement it and solve it in my local. I feel sad because I didn’t solve this chall during working on the CTF, but on the bright part, I learned a lot of new things related to Luajit. I hope that if in the future I face a challenge related to Luajit, I’ll be able to tackle and solve it.
Thanks for the chall’s author and the writeup’s author for providing me a new knowledge about this.
Sources
- https://0xten.gitbook.io/public/defcon/2022/quals/smugglers-cove
- https://uz56764.tistory.com/55
- http://pwning.net/pwn/2012/05/21/jit-source-and-writeup/
- https://pwparchive.wordpress.com/2012/10/16/peeking-inside-luajit/
- https://www.lua.org/manual/5.1/
- http://wiki.luajit.org/Bytecode-2.0
Social Media
Follow me on twitter