CCE 2020 Quals

Posted on Sep 28, 2020

이천쌀콘팬클럽으로 참여해서 simple*, TPU, nomorephp 정도 솔브를 냈다. 라업 좀 적어본당.

simple*

네.. 그냥 cliche pwnable

TPU

기본적인 형식은 Python으로 구현한 VM이고, 여기에 JNI 느낌으로다가 native.so 의 함수들을 끌어다가 쓴다.

TPU Class의 z 필드가 False면 native call을 하는데, vm 구현은 아래 정도 인스트럭션이 있다.

assign
add
sub
and
or
xor
not
shr
shl
mov (mode 0~6)
jmp
je
ret
switch native (e.z ^= True)

tpu.py 를 분석해보면, vm 코드를 입력받아 연산을 검증하는 Stage 1~4 를 지나 원하는 vm 코드를 계속 실행할 수 있게 해주는데 각 Stage의 내용은 아래와 같다.

Stage 1 : simple add

Stage 2 : implement toUpper()/toLower()

Stage 3 : implement mul

Stage 4 : find total

total = 0
for i in range(10):
    args.append(randint(0, 10))
    if total % 2:
        total = total + factorial(args[-1]) & 0xff
    else:
        total = total + sum(range(args[-1] + 1)) & 0xff

1~3은 금방 짰고, 4는 짜다가 암 걸릴것 같아서 좋은 방법을 찾다가 total의 범위가 0~0xff 이니까 그냥 익스를 256번 돌리기로 했다 zz

이러면 이제 exploit 만 남았는데, 누가봐도 memory corruption으로 쉘을 따라고 native call을 붙여놨기 때문에 native.so를 분석했다.

봐야할 부분은 정확히 아래 부분.

    def llll(e):
        m = e.e()
        s = e.e()
        d = e.e()
        if e.z:
            if m == 0:e.r[d] = e.a[s]
            if m == 1:e.r[d] = e.m[s]
            if m == 2:e.r[e.r[d]] = e.m[s]
            if m == 3:e.r[d] = e.m[e.r[s]]
            if m == 4:e.m[d] = e.r[s]
            if m == 5:e.m[e.r[d]] = e.r[s]
            if m == 6:e.m[d] = e.r[e.m[s]]

기본적으로 native.so에서는 모든 mode에 대해 oob check를 하고 있다.

void __noreturn oob()
{
  puts("Access Violation!");
  exit(0);
}

우회할 방법이 있겠지, 하고 생각하며 ida 디컴파일도 깨져서 나오길래 핸드레이를 하다가 취약점을 찾았다.

.text:0000000000001A53                 mov     rax, [rbp+var_60]
.text:0000000000001A57                 cmp     [rbp+var_18], rax
.text:0000000000001A5B                 jle     short loc_1A78

index 검사하는 루틴이 jle (less equal) 하나가 끝이길래 negative index로 oob가 터지겠다 생각해서 익스를 구상해봤다.

정리하자면 bytearray 앞의 python object map + elf + heap 에 aaw / aar이 존재하는 상황이다.

디버깅 붙여보니까 우리가 값을 쓸 수 있는 bytearray(e.m)이 heap 과 libc 사이에 할당되길래, 덮을 수 있는 부분은 ELF의 writeable한 영역 (다행히 partial relro) + heap으로 한정했다.

bytearray / libc 릭을 따고 보니까, python3 바이너리가 pie도 꺼져있길래 특정 got를 적당한 함수 plt로 덮을 생각을 했다.

그렇게 로컬에서는, strncpy@got를 gets 주소로 덮었을 떄 첫 인자가 stack 주소인 것을 활용해 ROP로 쉘을 땄는데, 리모트 따려고 도커 빌드하고나니까 strncpy 호출이 안된다;;

결국 또다시 수백개의 got를 뒤지다가 memchr@got를 발견했고, input('code : ') 가 rdi로 들어간다는 점을 이용해 system@plt로 덮어주면 쉘까지 따낼 수 있었다.

bytearray 오프셋 때문에 로컬 따고 리모트 따는데 까지 한 4시간은 걸린 것 같다.

Exploit : https://gist.github.com/st4nw/c391f32d7df46553d014a024eaddaadf

nomorephp

소스코드는 $_GET['code'] 를 eval 해주는 게 끝인데, disable_functions + open_basedir 때문에 rce가 안된다.

그런데

twitter

대회 무려 전날에 1 day poc가 떴다 zz

긁어서 썼더니 먹히더라.. 덕분에 꿀 빨았다.

대회 끝나고 이것저것 들어보니 tmpfile 함수를 사용하면 open_basedir을 씹고 /tmp에 파일을 올릴 수 있고, LD_PRELOAD 후킹해서 풀면 되는 것 같다.

이번 대회 많은 것 배워갑니다.