Simulate shellcode operation with Radare2

When we are writing assembly, there may be times when you need to see what is going on in the compiler. If you are troubleshooting shellcode problems, you need to run commands patiently and carefully.

This article will explore how to emulate 32-bit ARM shellcode on an x86_64 Ubuntu system. Since most laptops and workstations don't yet run ARM, what we need here is an alternative way to execute non-native instructions on the system. Also, raw shellcode binaries are not in an executable format and cannot be run by most tools, so we need a different way to execute these files.

Here we use Radare2, which is a console-driven framework that integrates a set of easy-to-use binary analysis tools. You can script these tools, or use the interactive command-line interface. To set this up on Ubuntu we just need a few simple commands.

mkdir ~/github
cd ~/github
git clone https://github.com/radareorg/radare2.git
cd radare2
sys/install.sh

If you have radare2 installed, make sure you are running the latest version. This tool is actively maintained and regularly updated. Also, there were some bugs before the June 2022 release, making it possible that this trial couldn't have been done better.

cd ~/github/radare2
git pull
sys/install.sh
r2 -V

In order to replicate the shellcode binary we will be using in this article, you can run the following at a bash prompt:

nemo@hammerhead:~$ echo -n -e '\x01\x30\x8f\xe2\x13\xff\x2f\xe1\x78\x46\x0c\x30\xc0\x46\x01\x90\x49\x1a\x92\x1a\x0b\x27\x01\xdf\x2f\x62\x69\x6e\x2f\x73\x68\x00' > shellcode-696.bin

nemo@hammerhead:~$ md5sum shellcode-696.bin 
42ba1c77446594cac3508b940926575d  shellcode-696.bin

Introduction to ESIL

Evaluable String Intermediate Language (ESIL) is an instruction abstracted from hardware used by radare2, which can "execute" machine instructions without considering the underlying hardware. This is ideal for executing non-native assembly instructions in an emulation environment.

In order to execute our shellcode using ESIL, we need to do the following:

  1. Load our shellcode binary
  2. Configure radare2 so it knows how to correctly interpret our shellcode binary
  3. Initialize ESIL
  4. Set registers as needed
  5. Verify its functionality with our assembly instructions

To help you study cybersecurity, you can receive a full set of information for free:
① Mind map of cybersecurity learning and growth path
② 60+ classic cybersecurity toolkits
③ 100+ SRC analysis reports
④ 150+ e-books on cybersecurity attack and defense techniques
⑤ The most authoritative CISSP Certification Exam Guide + Question Bank
⑥ More than 1800 pages of CTF Practical Skills Manual
⑦ Collection of the latest interview questions from network security companies (including answers)
⑧ APP Client Security Testing Guide (Android+IOS)

Executing ARM shellcode with ESIL

  1. Load our shellcode binary

When we run the "file" command on the shellcode binary, we see that Linux cannot determine its file format. Likewise, radare2 cannot determine what it is.

nemo@hammerhead:~/labs/shellcode/asm$ file shellcode-696.bin
shellcode-696.bin: data

Since it is just a binary file, we need to load it into radare2 and specify what we want to see. Here we modify some of the software's analysis settings so that we can correctly analyze our ARM file:

nemo@hammerhead:~/labs/shellcode/asm$ r2 shellcode-696.bin
[0x00000000]> e anal.arch = arm
[0x00000000]> e asm.arch = arm
[0x00000000]> e asm.bits = 32
[0x00000000]> e anal.armthumb=true
  1. Configure radare2 so it knows how to properly run our shellcode binary

Next we want to specify which instructions are ARM and which are THUMB. I found out that to do this, you need to define the function where the instruction type changes. In this particular shellcode, it switches between ARM and THUMB instructions.

[0x00000000]> af
[0x00000000]> pdf
┌ 8: fcn.00000000 ();
│ rg: 0 (vars 0, args 0)
│ bp: 0 (vars 0, args 0)
│ sp: 0 (vars 0, args 0)
│           0x00000000      01308fe2       add r3, pc, 1
└           0x00000004      13ff2fe1       bx r3

In this snippet of the radare2 command, I'm analyzing a function with address 0. There doesn't really exist a function here, but we do it so that our "function" can be specified as ARM or THUMB. The "pdf" command just prints the disassembly instruction of the function, which includes the add and bx instructions.

<p>[0x00000000]> s 8
[0x00000008]> af
[0x00000008]> pdf
┌ 24: fcn.00000008 (int32_t arg1, int32_t arg2);
│           ; arg int32_t arg1 @ r0
│           ; arg int32_t arg2 @ r1
│           0x00000008      78460c30       andlo r4, ip, r8, ror r6
│           0x0000000c      c0460190       andls r4, r1, r0, asr 13    ; arg2
│       ┌─< 0x00000010      491a921a       bne 0xfe48693c
│       │   0x00000014      0b2701df       svcle 0x1270b
│       │   0x00000018      2f62696e       cdpvs p2, 6, c6, c9, c15, 1
└       │   0x0000001c      2f736800       rsbeq r7, r8, pc, lsr 6
[0x00000008]> afB 16</p>

The next set of instructions starting at address 8 are the THUMB instructions. The "s 8 " instruction will look for 8 bytes in the file, jump to a place we want to go, and define the next "function". After creating the function with "af", when we try to display it with "pdf", it looks a bit weird. This is because the tools will still interpret these instructions as ARM.

We can specify that this "function" is THUMB by setting the number of bits to 16. That is, set asm.bits to 16, but only for this function. In a normal ARM binary, radare2 will try to do this distinction automatically, but since we only have this string of shellcode instructions, we still need to do it manually.

Note that
we can delete the first two instructions and use the full THUMB shellcode. If we do this, we can set "e asm.bits=16" when opening the file without having to redefine the function again. It's just that you can differentiate between the two instruction types if desired.

Radare2 also has a more convenient method, which is to use the "izz" command to display all strings in the binary file.

> izz
[Strings]
nth paddr      vaddr      len size section type  string
―――――――――――――――――――――――――――――――――――――――――――――――――――――――
0   0x00000008 0x00000008 4   5            ascii xF\f0
1   0x00000018 0x00000018 7   8            ascii /bin/sh</p>

We are now able to properly load our shellcode binary.

  1. Initialize ESIL

As mentioned earlier, radare2 has many built-in commands, adding "?" to the command prefix can list all related commands. The "ae?" command will list commands related to ESIL and emulation.

[0x00000000]> ae?
Usage: ae[idesr?] [arg]  ESIL code emulation
| ae [expr]                evaluate ESIL expression
| ae?                      show this help
| ae??                     show ESIL help
| aea[f] [count]           analyse n esil instructions accesses (regs, mem..)
| aeA[f] [count]           analyse n bytes for their esil accesses (regs, mem..)
| aeb ([addr])             emulate block in current or given address
| aeC[arg0 arg1..] @ addr  appcall in esil
| aec[?]                   continue until ^C
| aef [addr]               emulate function
| aefa [addr]              emulate function to find out args in given or current offset
| aeg [expr]               esil data flow graph
| aegf [expr] [register]   esil data flow graph filter
| aei[?]                   initialize ESIL VM state (aei- to deinitialize)
| aek[?] [query]           perform sdb query on ESIL.info
| aeL                      list ESIL plugins
| aep[?] [addr]            manage esil pin hooks (see “e cmd.esil.pin”)
| aepc [addr]              change esil PC to this address
| aer[?] [..]              handle ESIL registers like “ar” or “dr” does
| aes[?]                   perform emulated debugger step
| aets[?]                  esil Trace session
| aev [esil]        visual esil debugger for the given expression or current instruction
| aex [hex]                evaluate opcode expression

Here, we first need to initialize ESIL with the "aei" command. After that, we need to initialize a stack. Radare2 will automatically choose a stack location, but this can also be specified using the "aeim" command parameter.

[0x00000008]> aei
[0x00000008]> aeim
  1. Set registers as needed

Since our shellcode instructions are zero-based, we need to set our program counter (PC) to zero with the "aepc 0 " command. If we want to start execution at a location other than offset 0, we can use "aepc

" to set the starting address.

[0x00000008]> aepc 0

An instruction in our shellcode ("subs r1, r1, r1") will set r1 to 0. Since this register is already 0 by default, let's set it to 0xffff so we can see when we Changes that occur during stepping. To do this, we need to use the "aer" command.

[0x00000008]> aer r1 = 0xffff
  1. Verify its functionality with our setup

Ok, now we are all set. We can switch to the visual mode and enter the debugger panel. In visual mode there are multiple options (panels), so we need to hit "p" twice to get to the correct panel. If you want to exit visualization mode at any time, just press the escape key. You can also press "?" to see a list of available commands.

[0x00000008]> V
(hit “p” twice to get to the debugger panel)

Then you'll notice a set of registers near the top. It will look like this:

Any r2 command normally entered at the console can also be entered in visual mode. For example, if we want to print the string at offset 0x18, we need to execute the following command:

# Hit “:” while in visual mode.

> ps @0x18
/bin/sh
> # Hit enter on a blank line to return to visual mode.

Now, we can execute assembly instructions by using the "s" key. As you browse, you'll notice that the top registers are updated along with the stack data (starting at 0x00178000 in the first picture). You'll also notice that the address of the next instruction to execute (aka the PC) is highlighted in the assembly instruction (0x00000010 in the first picture).

Note that the picture above shows that the r1 register holds 0x0000ffff. Also notice that the next instruction will be executed, i.e. "subs r1, r1, r1". This instruction will subtract r1 from itself and store it back into r1, essentially making it 0.

Press the "s" key again to go to the next command.

Now everything is ready to be invoked through the daemon with the "svc 1 " command. We are now doing an "execve" call, so we should set 0xb in the r7 register. The first parameter in r0 should be a pointer to the path of the binary we want to execute. We can find that r0 holds 0x18. We can verify that it's pointing to by running:

# Hit “:” while at the “svc 1” instruction in visual mode.

> ps @r0
/bin/sh
>

Now we don't pass any parameters to "/bin/sh ", nor set any environment variables, so we can find that both r1 and r2 are set to 0.

Since we're not running on an ARM system, we can't use the daemon call ("svc 1") instruction correctly. Keep this in mind when you are testing more complex shellcode.

Summarize

Whether you're troubleshooting custom shellcode, or verifying static content you're seeing, sometimes you just need to see what the instructions are actually doing. Radare2 allows you to load non-native assembly files from an unknown file format (such as a shellcode binary or a firmware image) and execute instructions step by step.

Guess you like

Origin blog.csdn.net/qq_38154820/article/details/130529200