minimal typing command line calculator - tcsh vs bash

纵然是瞬间 提交于 2019-12-05 10:25:24

At least preventing the expansion of * is possible using 'set -f' (following someone's blog post:

alias C='set -f -B; Cf '
function Cf () { echo  "$@" | tr -d ', \042-\047' | bc -l; set +f; };  

Turning it off in the alias, before the calculation, and back on afterwards

$ C 2 * 3
6

I downloaded the bash sources and looked very closely. It seems the parenthesis error occurs directly during the parsing of the command line, before any command is run or alias is expanded. And without any flag to turn it off. So it would be impossible to do it from a bash script.

This means, it is time to bring the heavy weapons. Before parsing the command line is read from stdin using readline. Therefore, if we intercept the call to readline, we can do whatever we want with the command line.

Unfortunately bash is statically linked against readline, so the call cannot be intercepted directly. But at least readline is a global symbol, so we can get the address of the function using dlsym, and with that address we can insert arbitrary instructions in readline.

Modifying readline directly is prune to errors, if readline is changed between the different bash version, so we modify the function calling readline, leading to following plan:

  1. Locate readline with dlsym
  2. Replace readline with our own function that uses the current stack to locate the function calling readline (yy_readline_get) on its first call and then restores the original readline
  3. Modify yy_readline_get to call our wrapper function
  4. Within the wrapper function: Replace the parentheses with non problematic symbols, if the input starts with "C "

Written in C for amd64, we get:

#include <string.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#ifndef __USE_GNU
#define __USE_GNU
#endif
#ifndef  __USE_MISC
#define  __USE_MISC
#endif
#include <dlfcn.h>
#include <unistd.h>
#include <sys/mman.h>
#include <errno.h>

//-----------Assembler helpers----------

#if (defined(x86_64) || defined(__x86_64__))

    //assembler instructions to read rdp, which we need to read the stack
#define MOV_EBP_OUT "mov %%rbp, %0"
    //size of a call instruction 
#define RELATIVE_CALL_INSTRUCTION_SIZE 5

#define IS64BIT (1)

    /*
      To replace a function with a new one, we use the push-ret trick, pushing the destination address on the stack and let ret jump "back" to it
      This has the advantage that we can set an additional return address in the same way, if the jump goes to a function

    This struct corresponds to the following assembler fragment:          
     68       ????  push                   <low_dword  (address)>
     C7442404 ????  mov DWORD PTR [rsp+4], <high_dword (address) )
     C3             ret
    */
typedef struct __attribute__((__packed__)) LongJump { 
  char push; unsigned int destinationLow;
  unsigned int mov_dword_ptr_rsp4; unsigned int destinationHigh;
  char ret;
//  char nopFiller[16];
} LongJump;

void makeLongJump(void* destination, LongJump* res) {
  res->push = 0x68;
  res->destinationLow = (uintptr_t)destination & 0xFFFFFFFF;
  res->mov_dword_ptr_rsp4 = 0x042444C7;
  res->destinationHigh = ((uintptr_t)(destination) >> 32) & 0xFFFFFFFF;
  res->ret = 0xC3;
}

//Macros to save and restore the rdi register, which is used to pass an address to readline (standard amd64 calling convention)
typedef unsigned long SavedParameter;
#define SAVE_PARAMETERS SavedParameter savedParameters;  __asm__("mov %%rdi, %0": "=r"(savedParameters)); 
#define RESTORE_PARAMETERS __asm__("mov %0, %%rdi": : "r"(savedParameters)); 

#else
#error only implmented for amd64...
#endif

//Simulates the effect of the POP instructions, popping from a passed "stack pointer" and returning the popped value
static void * pop(void** stack){
  void* temp = *(void**)(*stack);
  *stack += sizeof(void*); 
  return temp;
}

//Disables the write protection of an address, so we can override it
static int unprotect(void * POINTER){
  const int PAGESIZE = sysconf(_SC_PAGE_SIZE);;
  if (mprotect((void*)(((uintptr_t)POINTER & ~(PAGESIZE-1))), PAGESIZE, PROT_READ | PROT_WRITE | PROT_EXEC)) {
    fprintf(stderr, "Failed to set permission on %p\n", POINTER);
    return 1;
  }
  return 0;
}

//Debug stuff
static void fprintfhex(FILE* f, void * hash, int len) {
  for (int i=0;i<len;i++) {
    if ((uintptr_t)hash % 8 == 0 && (uintptr_t)i % 8 == 0 && i ) fprintf(f, " ");
    fprintf(f, "%.2x", ((unsigned char*)(hash))[i]);
  }
  fprintf(f, "\n");
}

//---------------------------------------


//Address of the original readline function
static char* (*real_readline)(const char*)=0; 

//The wrapper around readline we want to inject.
//It replaces () with [], if the command line starts with "C "
static char* readline_wrapper(const char* prompt){
  if (!real_readline) return 0;
  char* result = real_readline(prompt);
  char* temp = result; while (*temp == ' ') temp++;
  if (temp[0] == 'C' && temp[1] == ' ') 
    for (int len = strlen(temp), i=0;i<len;i++) 
      if (temp[i] == '(') temp[i] = '[';
      else if (temp[i] == ')') temp[i] = ']';
  return result;
}


//Backup of the changed readline part
static unsigned char oldreadline[2*sizeof(LongJump)] = {0x90};
//A wrapper around the readline wrapper, needed on amd64 (see below)
static LongJump* readline_wrapper_wrapper = 0;



static void readline_initwrapper(){
  SAVE_PARAMETERS
  if (readline_wrapper_wrapper) { fprintf(stderr, "ERROR!\n"); return; }

  //restore readline
  memcpy(real_readline, oldreadline, 2*sizeof(LongJump)); 

  //find call in yy_readline_get
  void * frame;
  __asm__(MOV_EBP_OUT: "=r"(frame)); //current stackframe
  pop(&frame); //pop current stackframe (??)
  void * returnToFrame = frame;
  if (pop(&frame) != real_readline) {  
    //now points to current return address
    fprintf(stderr, "Got %p instead of %p=readline, when searching caller\n", frame, real_readline); 
    return; 
  }
  void * caller = pop(&frame); //now points to the instruction following the call to readline
  caller -= RELATIVE_CALL_INSTRUCTION_SIZE; //now points to the call instruction
  //fprintf(stderr, "CALLER: %p\n", caller);
  //caller should point to 0x00000000004229e1 <+145>:   e8 4a e3 06 00  call   0x490d30 <readline>
  if (*(unsigned char*)caller != 0xE8) { fprintf(stderr, "Expected CALL, got: "); fprintfhex(stderr, caller, 16); return; }

  if (unprotect(caller)) return;

  //We can now override caller to call an arbitrary function instead of readline.
  //However, the CALL instruction accepts only a 32 parameter, so the called function has to be in the same 32-bit address space
  //Solution: Allocate memory at an address close to that CALL instruction and put a long jump to our real function there
  void * hint = caller;
  readline_wrapper_wrapper = 0;
  do { 
    if (readline_wrapper_wrapper) munmap(readline_wrapper_wrapper, 2*sizeof(LongJump));
    readline_wrapper_wrapper = mmap(hint, 2*sizeof(LongJump), PROT_EXEC | PROT_READ | PROT_WRITE, MAP_ANONYMOUS|MAP_PRIVATE, -1, 0); 
    if (readline_wrapper_wrapper == MAP_FAILED) { fprintf(stderr, "mmap failed: %i\n", errno);  return; }
    hint += 0x100000;
  } while ( IS64BIT && ( (uintptr_t)readline_wrapper_wrapper >= 0xFFFFFFFF + ((uintptr_t) caller) ) ); //repeat until we get an address really close to caller
  //fprintf(stderr, "X:%p\n", readline_wrapper_wrapper);
  makeLongJump(readline_wrapper, readline_wrapper_wrapper); //Write the long jump in the newly allocated space

   //fprintfhex(stderr, readline_wrapper_wrapper, 16);
   //fprintfhex(stderr, caller, 16);

  //patch caller to become call <readline_wrapper_wrapper>
  //called address is relative to address of CALL instruction
  *(uint32_t*)(caller+1) = (uint32_t) ((uintptr_t)readline_wrapper_wrapper - (uintptr_t)(caller + RELATIVE_CALL_INSTRUCTION_SIZE) ); 

   //fprintfhex(stderr, caller, 16);

   *(void**)(returnToFrame) = readline_wrapper_wrapper; //change stack to jump to wrapper instead real_readline (or it would not work on the first entered command)

   RESTORE_PARAMETERS
}




static void _calc_init(void) __attribute__ ((constructor));


static void _calc_init(void){
  if (!real_readline) {
    //Find readline
    real_readline = (char* (*)(const char*)) dlsym(RTLD_DEFAULT, "readline");
    if (!real_readline) return;
    //fprintf(stdout, "loaded %p\n", real_readline);
    //fprintf(stdout, "  => %x\n", * ((int*) real_readline));

    if (unprotect(real_readline)) { fprintf(stderr, "Failed to unprotect readline\n"); return; }
    memcpy(oldreadline, real_readline, 2*sizeof(LongJump)); //backup readline's instructions

    //Replace readline  with readline_initwrapper
    makeLongJump(real_readline, (LongJump*)real_readline); //add a push/ret long jump from readline to readline, to have readline's address on the stack in readline_initwrapper
    makeLongJump(readline_initwrapper, (LongJump*)((char*)real_readline + sizeof(LongJump) - 1)); //add a push/ret long jump from readline to readline_initwrapper, overriding the previous RET

  }
}

This can be compiled to an intercepting library with:

gcc -g -std=c99 -shared -fPIC  -o calc.so -ldl calc.c

and then loaded in bash with:

gdb --batch-silent -ex "attach $BASHPID" -ex 'print dlopen("calc.so", 0x101)' 

Now, when the previous alias extended with parenthesis replacement is loaded:

alias C='set -f -B; Cf '
function Cf () {  echo  "$@" | tr -d ', \042-\047' | tr [ '(' | tr ] ')' | bc -l; set +f; };  

We can write:

$  C  1 * 2
  2
$  C  2*(2+1)
  6
$  C  (2+1)*2
  6

Even better it becomes, if we switch from bc to qalculate:

 alias C='set -f -B; Cf '
 function Cf () {  echo  "$@" | tr -d ', \042-\047' | tr [ '(' | tr ] ')' | xargs qalc ; set +f; };

Then we can do:

$ C e ^ (i * pi)
  e^(i * pi) = -1

$ C 3 c 
  3 * speed_of_light = approx. 899.37737(km / ms)

If you're prepared to type C Enter instead of C Space, the sky's the limit. The C command can take input in whatever form you desire, unrelated to the shell syntax.

C () {
  local line
  read -p "Arithmetic: " -e line
  echo "$line" | tr -d \"-\', | bc -l
}

In zsh:

function C {
  local line=
  vared -p "Arithmetic: " line
  echo $line | tr -d \"-\', | bc -l
}

In zsh, you can turn off globbing for the arguments of a specific command with the noglob modifier. It is commonly hidden in an alias. This prevents *^() from begin interpreted literally, but not quotes or $.

quickie_arithmetic () {
  echo "$*" | tr -d \"-\', | bc -l
}
alias C='noglob quickie_arithmetic'
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!