CrossCTF Finals 2018 : Souvlaki Space Station (Pwn)
First Blood by : OSI Layer 8
Pictures and I've fallen Wonder why I'm here now
nc ctf.pwn.sg 4006
Creator - amon (@nn_amon)
Hint: Try examining the use of 'strlen()' and 'signal()'.
Static Analysis
~~Well what do you know, I'm really starting to hate ARM~~
Static analysis using IDA just fails. The printed code was 5 lines long and the function calls did not match what was actually being called in the function at runtime. Viewing the function in graph mode, there are pieces of code that IDA just didn't detect as part of the control flow graph, and goofed up.
I was determined to actually learn ARM assembly in the last 8 hours or so, but luckily the source was released soon after ;)
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
typedef struct state {
int admin;
char message[128];
char * presenter;
size_t length;
} state;
state global_state;
void sighandler(int signum)
{
signal(signum, SIG_DFL);
puts("souvlaki.c:10:5: warning: implicit declaration of function ‘exit’ [-Wimplicit-function-declaration]");
puts(" exit(1);");
puts("souvlaki.c:10:5: warning: incompatible implicit declaration of built-in function ‘exit’");
puts("souvlaki.c:10:5: note: include ‘<stdlib.h>’ or provide a declaration of ‘exit’");
if (global_state.admin) {
puts("To report this bug, please contact support@linux.org.");
execl("/usr/bin/vi", NULL);
}
exit(1);
}
void init() {
global_state.admin = 0;
strcpy(global_state.message, "P L A C E H O L D E R T E X T M A N");
global_state.presenter = "[EC2 (%lld/150)]: ";
global_state.length = strlen(global_state.message) + 1;
}
int main()
{
state * ptr = &global_state;
++ptr;
--ptr;
init();
// Disable buffering on stdin, stdout, and stderr
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
signal(SIGSEGV, sighandler);
uint64_t count = 1;
while (count <= 150) {
printf(global_state.presenter, count);
read(0, global_state.message, global_state.length);
global_state.length = strlen(global_state.message) + 1;
for (int i = 0; i < global_state.length; i++) {
if (global_state.message[i] == '\n') {
global_state.message[i] = 0;
}
}
for (int i = 0 ; i < global_state.length; i++) {
printf("%hhd ", global_state.message[i]);
}
puts("");
++count;
}
return 0;
}
We have a signal handler set for SIGSEGV
, segmentation fault, that runs vi
. You can actually get shell from vi
by going to command mode (type :
), then entering !sh
. Of couse, you need to set global_state.admin
to a non-zero value first.
There's a very subtle bug in the while loop in main(). From these:
read(0, global_state.message, global_state.length);
global_state.length = strlen(global_state.message) + 1;
We can see that global_state.length
is intended to be used as the actual string length in read()
, but we set it to actual string length (of the message) + 1 in the next line.
Thus, we can increase global_state.length
by 1 if we fill up the global_state.message
(send 'a' * the current global_state.length
).
Solution
If we can fill the entire 128 bytes of global_state.message
(and make sure not to send any '\n'), strlen(global_state.message)
will actually give us 128 + number of non-zero bytes of global_state.presenter
. Thus, we can edit the value of global_state.presenter
as long as the value we want to write isn't too big. Note that 37 bytes are already filled with a pre-existing message.
If we change it to the address of our input, we can try to do a format string attack, since global_state.presenter
is used as the formatting argument to printf.
Luckily, PIE isn't enabled, and the global_state
is stored in the .bss section, so we can write it with the address of our input, which is 0x98ca0
$ checksec souvlaki
Arch: arm-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x10000)
Now we need to set global_state.admin
to a non-zero value, and then crash the program.
Dynamic Analysis
Now, I knew very little about ARM architecture and how printf() worked with its registers and stack, so I made the input a long string of "%p ..." first.
Luckily the 7th argument printed out was the address of global_state.admin
(0x00098c9c
), and some arguments were very low values, like the 2nd argument (attempting to write to them would likely segfault).
Now, we do a typical format string attack in our input by sending "A%7$n" (the 'A' is there so that 1 byte is printed already, and %n will write 1 to the address that is the 7th argument), then just try to print to the 2nd argument using "%2$n", which will crash the program and run vi
for us. We have to pad this to 128 bytes, then finally send in our input's address.
Once vi
starts up, we type in :!sh
to get shell
Solution code in here